Comparación de paradigmas de programación - Comparison of programming paradigms

Este artículo intenta establecer las diversas similitudes y diferencias entre los diversos paradigmas de programación como un resumen en formato gráfico y tabular con enlaces a las discusiones separadas sobre estas similitudes y diferencias en los artículos existentes de Wikipedia.

Enfoques del paradigma principal

Hay dos enfoques principales para la programación:

Los siguientes son ampliamente considerados los principales paradigmas de programación, como se ve al medir la popularidad de los lenguajes de programación :

Los siguientes son tipos comunes de programación que se pueden implementar usando diferentes paradigmas:

Las subrutinas que implementan métodos OOP pueden codificarse en última instancia en un estilo imperativo, funcional o de procedimiento que puede, o no, alterar directamente el estado en nombre del programa que invoca. Existe cierta superposición entre paradigmas, inevitablemente, pero las principales características o diferencias identificables se resumen en esta tabla:

Paradigma Descripción Rasgos principales Paradigmas relacionados Crítica Ejemplos de
Imperativo Programas como declaraciones que cambian directamente el estado calculado ( campos de datos ) Asignaciones directas , estructuras de datos comunes , variables globales Edsger W. Dijkstra , Michael A. Jackson C , C ++ , Java , Kotlin , PHP , Python , Ruby
Estructurado Un estilo de programación imperativa con una estructura de programa más lógica. Estructogramas , sangría , uso limitado o nulo de declaraciones goto Imperativo C , C ++ , Java , Kotlin , Pascal , PHP , Python
Procesal Derivado de la programación estructurada, basado en el concepto de programación modular o la llamada a procedimiento Variables locales , secuencia, selección, iteración y modularización Estructurado, imperativo C , C ++ , Lisp , PHP , Python
Funcional Trata la computación como la evaluación de funciones matemáticas evitando estados y datos mutables Cálculo lambda , composicionalidad , fórmula , recursividad , transparencia referencial , sin efectos secundarios Declarativo C ++ , C # , Clojure , CoffeeScript , Elixir , Erlang , F # , Haskell , Java (desde la versión 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Impulsado por eventos, incluido el tiempo El flujo de control está determinado principalmente por eventos , como clics del mouse o interrupciones, incluido el temporizador. Bucle principal , controladores de eventos, procesos asincrónicos Procedimiento, flujo de datos JavaScript , ActionScript , Visual Basic , Elm
Orientado a objetos Trata los campos de datos como objetos manipulados a través de métodos predefinidos únicamente. Objetos , métodos, transmisión de mensajes , ocultación de información , abstracción de datos , encapsulación , polimorfismo , herencia , serialización- ordenación Procesal Wikipedia , otros Common Lisp , C ++ , C # , Eiffel , Java , Kotlin , PHP , Python , Ruby , Scala , JavaScript
Declarativo Define la lógica del programa, pero no el flujo de control detallado Lenguajes de cuarta generación , hojas de cálculo , generadores de programas de informes SQL , expresiones regulares , Prolog , OWL , SPARQL , Datalog , XSLT
Programación basada en autómatas Trata los programas como un modelo de una máquina de estados finitos o cualquier otro autómata formal. Enumeración de estado , variable de control , cambios de estado , isomorfismo , tabla de transición de estado Imperativo, impulsado por eventos Lenguaje de máquina de estado abstracto

Diferencias en terminología

A pesar de los múltiples (tipos de) paradigmas de programación que existen en paralelo (con definiciones a veces aparentemente conflictivas), muchos de los componentes fundamentales subyacentes siguen siendo más o menos los mismos ( constantes , variables , campos de datos , subrutinas , llamadas, etc.) y de alguna manera deben ser inevitablemente. incorporado en cada paradigma separado con atributos o funciones igualmente similares. La tabla anterior no pretende ser una guía de similitudes precisas, sino más bien un índice de dónde buscar más información, según los diferentes nombres de estas entidades, dentro de cada paradigma. Lo que complica aún más las cosas son las implementaciones no estandarizadas de cada paradigma, en muchos lenguajes de programación , especialmente lenguajes que soportan múltiples paradigmas , cada uno con su propia jerga .

Ayuda de idioma

El azúcar sintáctico es la mejora de la funcionalidad del programa mediante la introducción de características del lenguaje que facilitan un uso determinado, incluso si el resultado final podría lograrse sin ellas. Podría decirse que un ejemplo de azúcar sintáctico pueden ser las clases utilizadas en los lenguajes de programación orientados a objetos. El lenguaje imperativo C puede admitir la programación orientada a objetos a través de sus instalaciones de punteros de función , conversión de tipos y estructuras. Sin embargo, lenguajes como C ++ tienen como objetivo hacer que la programación orientada a objetos sea más conveniente al introducir una sintaxis específica para este estilo de codificación. Además, la sintaxis especializada trabaja para enfatizar el enfoque orientado a objetos. De manera similar, las funciones y la sintaxis de bucle en C (y otros lenguajes de programación estructurados y de procedimiento) podrían considerarse azúcar sintáctico. El lenguaje ensamblador puede admitir programación estructurada o procedimental a través de sus funciones para modificar los valores de registro y la ejecución de bifurcaciones según el estado del programa. Sin embargo, lenguajes como C introdujeron una sintaxis específica para estos estilos de codificación para hacer más conveniente la programación estructurada y procedimental. Las características del lenguaje C # (C Sharp), como las propiedades y las interfaces, tampoco permiten nuevas funciones, pero están diseñadas para hacer que las buenas prácticas de programación sean más prominentes y naturales.

Algunos programadores sienten que estas características no son importantes o incluso frívolas. Por ejemplo, Alan Perlis bromeó una vez, en una referencia a los lenguajes delimitados por corchetes , que "el azúcar sintáctico causa cáncer de punto y coma " (ver Epigramas sobre programación ).

Una extensión de esto es la sacarina sintáctica , o sintaxis gratuita que no facilita la programación.

Comparación de rendimiento

Solo en la longitud total de la ruta de instrucción , un programa codificado en un estilo imperativo, sin subrutinas, tendría el recuento más bajo. Sin embargo, el tamaño binario de dicho programa puede ser mayor que el del mismo programa codificado usando subrutinas (como en la programación funcional y de procedimientos) y haría referencia a más instrucciones físicas no locales que pueden aumentar las pérdidas de caché y la sobrecarga de búsqueda de instrucciones en los procesadores modernos .

Los paradigmas que usan subrutinas de manera extensiva (incluidos los funcionales, procedimentales y orientados a objetos) y no usan también una expansión en línea significativa (alineación, a través de optimizaciones del compilador ) utilizarán, en consecuencia, una fracción mayor de los recursos totales en los enlaces de subrutina. Programas orientados a objetos que no lo hacen deliberadamente alter estado del programa directamente, en lugar de utilizar métodos de mutador (o emisores ) para encapsular estos cambios de estado, Will, como consecuencia directa, tener más sobrecarga. Esto se debe a que el paso de mensajes es esencialmente una llamada de subrutina, pero con tres gastos generales adicionales: asignación de memoria dinámica , copia de parámetros y envío dinámico . Obtener memoria del montón y copiar parámetros para el paso de mensajes puede implicar recursos importantes que superan con creces los necesarios para el cambio de estado. Los descriptores de acceso (o captadores ) que simplemente devuelven los valores de las variables de miembros privados también dependen de subrutinas de paso de mensajes similares, en lugar de utilizar una asignación (o comparación) más directa, que se suman a la longitud total de la ruta.

Código administrado

Para los programas que se ejecutan en un entorno de código administrado , como .NET Framework , muchos problemas afectan el rendimiento y se ven afectados significativamente por el paradigma del lenguaje de programación y las diversas características del lenguaje utilizadas.

Ejemplos de pseudocódigo que comparan varios paradigmas

Una comparación de pseudocódigo de enfoques imperativos, procedimentales y orientados a objetos utilizados para calcular el área de un círculo (πr²), asumiendo que no hay alineación de subrutinas , no hay preprocesadores de macros , aritmética de registros y ponderando cada 'paso' de instrucción como solo 1 instrucción, como un medida bruta de la longitud de la ruta de instrucción - se presenta a continuación. El paso de instrucción que realiza conceptualmente el cambio de estado se resalta en negrita en cada caso. Las operaciones aritméticas utilizadas para calcular el área del círculo son las mismas en los tres paradigmas, con la diferencia de que los paradigmas procedimentales y orientados a objetos envuelven esas operaciones en una llamada de subrutina que hace que el cálculo sea general y reutilizable. El mismo efecto podría lograrse en un programa puramente imperativo utilizando un preprocesador de macros a solo el costo de un mayor tamaño del programa (solo en cada sitio de invocación de macros) sin un costo de tiempo de ejecución prorrateado correspondiente (proporcional a n invocaciones, que puede estar situado dentro de un bucle interno, por ejemplo). Por el contrario, la inserción de subrutinas por parte de un compilador podría reducir los programas de procedimiento a algo similar en tamaño al código puramente imperativo. Sin embargo, para los programas orientados a objetos, incluso con inlining, los mensajes aún deben construirse (a partir de copias de los argumentos) para procesarlos mediante los métodos orientados a objetos. La sobrecarga de las llamadas, virtuales o de otro tipo, no está dominada por la alteración del flujo de control , sino por los costos de la convención de llamadas circundantes , como el código de prólogo y epílogo , la configuración de la pila y el paso de argumentos (consulte aquí para obtener una longitud de ruta de instrucción más realista, pila y otros costos asociados con las llamadas en una plataforma x86 ). Consulte también aquí una presentación de diapositivas de Eric S. Roberts ("La asignación de memoria a variables", capítulo 7), que ilustra el uso de memoria de pila y pila al sumar tres números racionales en el lenguaje orientado a objetos de Java .

Imperativo Procesal Orientado a objetos
 load r;                      1
 r2 = r * r;                  2
 result = r2 * "3.142";       3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.... storage .............
result variable
constant "3.142"
area proc(r2,res):
   push stack                                 5
   load r2;                                   6
   r3 = r2 * r2;                              7
   res = r3 * "3.142";                        8
   pop stack                                  9
   return;                                   10
...............................................
main proc:
   load r;                                    1
   call area(r,result);
    +load p = address of parameter list;      2
    +load v = address of subroutine 'area';   3
    +goto v with return;                      4
.
.
.
.
.... storage .............
result variable
constant "3.142"
parameter list variable
function pointer (==>area)
stack storage
circle.area method(r2):
   push stack                                 7
   load r2;                                   8
   r3 = r2 * r2;                              9
   res = r3 * "3.142";                       10
   pop stack                                 11
   return(res);                           12,13
...............................................
main proc:
   load r;                                    1
   result = circle.area(r);
      +allocate heap storage;                 2
      +copy r to message;                     3
      +load p = address of message;           4
      +load v = addr. of method 'circle.area' 5
      +goto v with return;                    6
.
.
.... storage .............
result variable (assumed pre-allocated)
immutable variable "3.142" (final)
(heap) message variable for circle method call
vtable(==>area)
stack storage

Las ventajas de la abstracción procedimental y el polimorfismo de estilo orientado a objetos están mal ilustradas por un pequeño ejemplo como el anterior. Este ejemplo está diseñado principalmente para ilustrar algunas diferencias de rendimiento intrínsecas, no abstracción o reutilización de código.

Subrutina, sobrecarga de llamada de método

La presencia de una subrutina (llamada) en un programa no aporta nada adicional a la funcionalidad del programa independientemente del paradigma, pero puede contribuir en gran medida a la estructuración y generalidad del programa, haciéndolo mucho más fácil de escribir, modificar y extender. La medida en que los diferentes paradigmas usan subrutinas (y sus consiguientes requisitos de memoria) influye en el rendimiento general del algoritmo completo, aunque, como señaló Guy Steele en un artículo de 1977, una implementación de lenguaje de programación bien diseñada puede tener unos gastos generales muy bajos para la abstracción de procedimientos. (pero lamenta, en la mayoría de las implementaciones, que rara vez logran esto en la práctica, siendo "más bien irreflexivos o descuidados a este respecto"). En el mismo artículo, Steele también hace un caso considerado para la programación basada en autómatas (usando llamadas a procedimientos con recursividad de cola ) y concluye que "deberíamos tener un respeto saludable por las llamadas a procedimientos" (porque son poderosas) pero sugirió "usarlas con moderación "

En la frecuencia de las llamadas a subrutinas:

  • Para la programación de procedimientos, la granularidad del código está determinada en gran medida por el número de procedimientos o módulos discretos .
  • Para la programación funcional, las llamadas frecuentes a las subrutinas de la biblioteca son comunes, pero el compilador de optimización puede incluirlas a menudo
  • Para la programación orientada a objetos, el número de llamadas a métodos invocadas también está determinado en parte por la granularidad de las estructuras de datos y, por lo tanto, puede incluir muchos accesos de solo lectura a objetos de bajo nivel que están encapsulados y, por lo tanto, accesibles en ningún otro, más directo, camino. Dado que una mayor granularidad es un requisito previo para una mayor reutilización del código , la tendencia es hacia estructuras de datos de grano fino y un aumento correspondiente en el número de objetos discretos (y sus métodos) y, en consecuencia, llamadas a subrutinas. Se desaconseja activamente la creación de objetos divinos . Los constructores también se suman al recuento, ya que también son llamadas a subrutinas (a menos que estén en línea). Es posible que los problemas de rendimiento causados ​​por una granularidad excesiva no se hagan evidentes hasta que la escalabilidad se convierta en un problema.
  • Para otros paradigmas, donde se puede emplear una combinación de los paradigmas anteriores, el uso de subrutinas es menos predecible.

Asignación de memoria dinámica para el almacenamiento de mensajes y objetos

Excepcionalmente, el paradigma orientado a objetos implica la asignación de memoria dinámica desde el almacenamiento dinámico para la creación de objetos y el paso de mensajes. Un punto de referencia de 1994 - "Costos de asignación de memoria en grandes programas C y C ++" realizado por Digital Equipment Corporation en una variedad de software, utilizando una herramienta de generación de perfiles a nivel de instrucción, midió cuántas instrucciones se requerían por asignación de almacenamiento dinámico. Los resultados mostraron que el número absoluto más bajo de instrucciones ejecutadas promedió alrededor de 50, pero otras llegaron hasta 611. Ver también "Heap: Placeres y dolores" de Murali R. Krishnan que dice que "las implementaciones de Heap tienden a ser generales para todas las plataformas, y por lo tanto, tienen gastos generales pesados ​​". El artículo de IBM de 1996 "Escalabilidad de los algoritmos de asignación de almacenamiento dinámico" de Arun Iyengar de IBM demuestra varios algoritmos de almacenamiento dinámico y sus respectivos recuentos de instrucciones. Incluso el algoritmo MFLF I recomendado (HS Stone, RC 9674) muestra recuentos de instrucciones en un rango entre 200 y 400. El ejemplo de pseudocódigo anterior no incluye una estimación realista de esta longitud de ruta de asignación de memoria o los gastos generales del prefijo de memoria involucrados y la basura asociada posterior gastos generales de recogida. Un microasignador de software de código abierto , del desarrollador de juegos John W. Ratcliff , que sugiere firmemente que la asignación de montones es una tarea no trivial , consta de casi 1000 líneas de código.

Llamadas de mensajes enviadas dinámicamente v. Gastos generales de llamadas de procedimiento directo

En su Resumen " Optimización de programas orientados a objetos mediante análisis de jerarquía de clases estática ", Jeffrey Dean, David Grove y Craig Chambers del Departamento de Ciencias de la Computación e Ingeniería de la Universidad de Washington , afirman que "El uso intensivo de la herencia y de forma dinámica Es probable que los mensajes vinculados hagan que el código sea más extensible y reutilizable, pero también impone una sobrecarga de rendimiento significativa, en relación con un programa equivalente pero no extensible escrito de una manera no orientada a objetos. En algunos dominios, como los paquetes de gráficos estructurados , el costo de rendimiento de la flexibilidad adicional proporcionada por el uso de un estilo fuertemente orientado a objetos es aceptable. Sin embargo, en otros dominios, como bibliotecas de estructura de datos básica, paquetes de computación numérica, bibliotecas de renderizado y marcos de simulación basados ​​en rastreo, el costo de el paso de mensajes puede ser demasiado grande, lo que obliga al programador a evitar la programación orientada a objetos en los "puntos calientes" de su aplicación ".

Serializar objetos

La serialización impone grandes gastos generales al pasar objetos de un sistema a otro, especialmente cuando la transferencia se realiza en formatos legibles por humanos, como Extensible Markup Language ( XML ) y JavaScript Object Notation ( JSON ). Esto contrasta con los formatos binarios compactos para datos no orientados a objetos. Tanto la codificación como la decodificación del valor de los datos del objeto y sus atributos están involucrados en el proceso de serialización, que también incluye el conocimiento de cuestiones complejas como la herencia, la encapsulación y la ocultación de datos.

Computación paralela

El profesor de la Universidad Carnegie-Mellon, Robert Harper, en marzo de 2011 escribió: "Este semestre, Dan Licata y yo enseñamos conjuntamente un nuevo curso sobre programación funcional para futuros estudiantes de informática de primer año ... La programación orientada a objetos se elimina por completo del plan de estudios introductorio , porque es a la vez anti-modular y anti-paralelo por su propia naturaleza y, por lo tanto, inadecuado para un plan de estudios moderno de informática. Se ofrecerá un nuevo curso propuesto sobre metodología de diseño orientado a objetos en el nivel de segundo año para aquellos estudiantes que deseen estudiar este tema."

Ver también

Referencias

Otras lecturas

enlaces externos