La orientación a objetos me ha ayudado mucho en la implementación de muchos algoritmos. Sin embargo, los lenguajes orientados a objetos a veces lo guían en un enfoque "directo" y dudo que este enfoque sea siempre algo bueno.
OO es realmente útil para codificar algoritmos de manera rápida y fácil. Pero, ¿podría esta OOP ser una desventaja para el software basado en el rendimiento, es decir, qué tan rápido se ejecuta el programa?
Por ejemplo, almacenar nodos de gráficos en una estructura de datos parece "sencillo" en primer lugar, pero si los objetos Node contienen muchos atributos y métodos, ¿podría esto conducir a un algoritmo lento?
En otras palabras, ¿podrían muchas referencias entre muchos objetos diferentes, o el uso de muchos métodos de muchas clases, dar como resultado una implementación "pesada"?
fuente
Respuestas:
La orientación a objetos puede evitar ciertas optimizaciones algorítmicas, debido a la encapsulación. Dos algoritmos pueden funcionar particularmente bien juntos, pero si están ocultos detrás de las interfaces OO, se pierde la posibilidad de usar su sinergia.
Mira las bibliotecas numéricas. Muchos de ellos (no solo los escritos en los años 60 o 70) no son OOP. Hay una razón para eso: los algoritmos numéricos funcionan mejor como un conjunto de desacoplamientos
modules
que como jerarquías OO con interfaces y encapsulación.fuente
¿Qué determina el rendimiento?
Los fundamentos: estructuras de datos, algoritmos, arquitectura de computadoras, hardware. Más gastos generales.
Un programa OOP puede diseñarse para alinearse exactamente con la elección de estructuras de datos y algoritmos que la teoría CS considera óptimos. Tendrá la misma característica de rendimiento que el programa óptimo, más algunos gastos generales. La sobrecarga generalmente se puede minimizar.
Sin embargo, un programa que inicialmente está diseñado solo con preocupaciones de OOP, sin importar los fundamentos, puede ser inicialmente subóptimo. La suboptimidad a veces se puede eliminar refactorizando; a veces no lo es, lo que requiere una reescritura completa.
Advertencia: ¿importa el rendimiento en el software empresarial?
Sí, pero el tiempo de comercialización (TTM) es más importante, por órdenes de magnitud. El software empresarial pone el énfasis en la adaptabilidad del código a las complejas reglas comerciales. Las mediciones de rendimiento deben tomarse durante todo el ciclo de vida del desarrollo. (Consulte la sección: ¿qué significa el rendimiento óptimo? ). Solo se deben realizar mejoras comercializables, y se deben introducir gradualmente en versiones posteriores.
¿Qué significa un rendimiento óptimo?
En general, el problema con el rendimiento del software es que: para demostrar que "existe una versión más rápida", primero debe existir esa versión más rápida (es decir, no hay otra prueba que no sea ella misma).
A veces, esa versión más rápida se ve por primera vez en un lenguaje o paradigma diferente. Esto debe tomarse como una pista para mejorar, no como un juicio de inferioridad de otros lenguajes o paradigmas.
¿Por qué estamos haciendo OOP si puede dificultar nuestra búsqueda de un rendimiento óptimo?
OOP introduce gastos generales (en espacio y ejecución), a cambio de mejorar la "trabajabilidad" y, por lo tanto, el valor comercial del código. Esto reduce el costo de un mayor desarrollo y optimización. Ver @MikeNakis .
¿Qué partes de OOP pueden fomentar un diseño inicialmente subóptimo?
Las partes de OOP que (i) fomentan la simplicidad / intuición, (ii) el uso de métodos de diseño coloquial en lugar de los fundamentos, (iii) desalienta implementaciones personalizadas múltiples con el mismo propósito.
La aplicación estricta de algunas pautas de OOP (encapsulación, transmisión de mensajes, hacer una cosa bien) dará como resultado un código más lento al principio. Las mediciones de rendimiento ayudarán a diagnosticar esos problemas. Siempre que la estructura de datos y el algoritmo se alineen con el diseño óptimo predicho por la teoría, la sobrecarga generalmente se puede minimizar.
¿Cuáles son las mitigaciones comunes para los gastos generales de OOP?
Como se indicó anteriormente, el uso de estructuras de datos que son óptimas para el diseño.
Algunos idiomas admiten la alineación de código que puede recuperar algo de rendimiento en tiempo de ejecución.
¿Cómo podríamos adoptar OOP sin sacrificar el rendimiento?
Aprenda y aplique tanto la POO como los fundamentos.
Es cierto que el cumplimiento estricto de OOP puede impedir que escriba una versión más rápida. A veces, una versión más rápida solo se puede escribir desde cero. Es por eso que ayuda a escribir múltiples versiones de código usando diferentes algoritmos y paradigmas (OOP, genérico, funcional, matemático, spaghetti), y luego usar herramientas de optimización para hacer que cada versión se acerque al rendimiento máximo observado.
¿Hay tipos de código que no se beneficiarán de OOP?
(Ampliado de la discusión entre [@quant_dev], [@ SK-logic] y [@MikeNakis])
* *
fuente
En realidad no se trata de la orientación a objetos, sino de los contenedores. Si usó una lista de doble enlace para almacenar píxeles en su reproductor de video, sufrirá.
Sin embargo, si usa el contenedor correcto, no hay razón para que un std :: vector sea más lento que una matriz, y dado que ya tiene todos los algoritmos comunes escritos para él, por expertos, es probable que sea más rápido que el código de la matriz de su casa.
fuente
Obviamente, OOP es una buena idea y, como cualquier buena idea, se puede usar en exceso. En mi experiencia, es muy usado. Bajo rendimiento y bajo resultado de mantenimiento.
No tiene nada que ver con la sobrecarga de llamar a funciones virtuales, y no tiene mucho que ver con lo que hace el optimizador / jitter.
Tiene todo que ver con estructuras de datos que, si bien tienen el mejor rendimiento de big-O, tienen factores constantes muy malos. Esto se hace suponiendo que si hay algún problema de limitación de rendimiento en la aplicación, está en otro lugar.
Una forma en que esto se manifiesta es la cantidad de veces por segundo que se realiza una nueva , que se supone que tiene un rendimiento de O (1), pero puede ejecutar de cientos a miles de instrucciones (incluida la eliminación correspondiente o el tiempo de GC). Eso se puede mitigar guardando objetos usados, pero eso hace que el código sea menos "limpio".
Otra forma en que se manifiesta es la forma en que se alienta a las personas a escribir funciones de propiedad, manejadores de notificaciones, llamadas a funciones de clase base, todo tipo de llamadas a funciones subterráneas que existen para tratar de mantener la coherencia. Para mantener la consistencia tienen un éxito limitado, pero tienen un enorme éxito en los ciclos de pérdida. Los programadores entienden el concepto de datos normalizados, pero tienden a aplicarlo solo al diseño de bases de datos. No lo aplican al diseño de la estructura de datos, al menos en parte porque OOP les dice que no tienen que hacerlo. Tan simple como configurar un bit modificado en un objeto puede provocar un tsunami de actualizaciones que se ejecutan a través de la estructura de datos, porque ninguna clase que valga su código toma una llamada modificada y simplemente la almacena .
Tal vez el rendimiento de una aplicación determinada está bien tal como está escrito.
Por otro lado, si hay un problema de rendimiento, aquí hay un ejemplo de cómo hago para ajustarlo. Es un proceso de varias etapas. En cada etapa, alguna actividad en particular representa una gran fracción de tiempo y podría reemplazarse por algo más rápido. (No dije "cuello de botella". Este no es el tipo de cosas que los perfiladores son buenos para encontrar). Este proceso a menudo requiere, para obtener la aceleración, el reemplazo total de la estructura de datos. A menudo, esa estructura de datos está ahí solo porque se recomienda la práctica de OOP.
fuente
En teoría, podría conducir a la lentitud, pero incluso entonces, no sería un algoritmo lento, sería una implementación lenta. En la práctica, la orientación a objetos le permitirá probar varios escenarios hipotéticos (o volver a visitar el algoritmo en el futuro) y, por lo tanto, proporcionarle mejoras algorítmicas , que nunca podría esperar lograr si lo hubiera escrito en forma de espagueti al principio lugar, porque la tarea sería desalentadora. (Esencialmente, tendría que reescribir todo el asunto).
Por ejemplo, al dividir las diversas tareas y entidades para cortar objetos de forma limpia, es posible que pueda ingresar más tarde y, por ejemplo, incrustar una instalación de almacenamiento en caché entre algunos objetos (transparente para ellos) que podría generar mil pliegue de mejora.
En general, los tipos de mejoras que puede lograr mediante el uso de un lenguaje de bajo nivel (o trucos ingeniosos con un lenguaje de alto nivel) proporcionan mejoras de tiempo constantes (lineales), que no figuran en términos de notación big-oh. Con mejoras algorítmicas, puede lograr mejoras no lineales. Eso no tiene precio.
fuente
A menudo sí !!! PERO...
No necesariamente. Esto depende del idioma / compilador. Por ejemplo, un compilador optimizador de C ++, siempre que no use funciones virtuales, a menudo reducirá la sobrecarga de su objeto a cero. Puede hacer cosas como escribir un contenedor sobre un
int
puntero inteligente allí o un puntero inteligente sobre un puntero antiguo que funciona tan rápido como el uso directo de estos tipos de datos antiguos.En otros lenguajes como Java, hay un poco de sobrecarga en un objeto (a menudo bastante pequeño en muchos casos, pero astronómico en algunos casos raros con objetos realmente pequeños). Por ejemplo,
Integer
es considerablemente menos eficiente queint
(toma 16 bytes en lugar de 4 en 64 bits). Sin embargo, esto no es solo un desperdicio descarado o algo por el estilo. A cambio, Java ofrece cosas como la reflexión sobre cada tipo definido por el usuario de manera uniforme, así como la capacidad de anular cualquier función que no esté marcada comofinal
.Sin embargo, tomemos el mejor de los casos: el compilador optimizador de C ++ que puede optimizar las interfaces de objetos hasta cero sobrecarga. Incluso entonces, la OOP a menudo degradará el rendimiento y evitará que alcance el pico. Eso puede sonar como una paradoja completa: ¿cómo podría ser? El problema radica en:
Diseño de interfaz y encapsulación
El problema es que incluso cuando un compilador puede aplastar la estructura de un objeto a cero sobrecarga (que al menos es muy cierto para optimizar los compiladores de C ++), el diseño de encapsulación e interfaz (y las dependencias acumuladas) de objetos de grano fino a menudo impedirán Representaciones de datos más óptimas para objetos que están destinados a ser agregados por las masas (que a menudo es el caso del software crítico para el rendimiento).
Toma este ejemplo:
Digamos que nuestro patrón de acceso a la memoria es simplemente recorrer estas partículas secuencialmente y moverlas alrededor de cada cuadro repetidamente, rebotando en las esquinas de la pantalla y luego renderizando el resultado.
Ya podemos ver un deslumbrante relleno de 4 bytes por encima requerido para alinear el
birth
miembro correctamente cuando las partículas se agregan contiguamente. Ya ~ 16.7% de la memoria se desperdicia con el espacio muerto utilizado para la alineación.Esto puede parecer discutible porque tenemos gigabytes de DRAM en estos días. Sin embargo, incluso las máquinas más bestiales que tenemos hoy en día solo tienen apenas 8 megabytes cuando se trata de la región más lenta y grande de la caché de la CPU (L3). Cuanto menos podamos encajar allí, más pagaremos en términos de acceso DRAM repetido, y las cosas se volverán más lentas. De repente, perder el 16,7% de la memoria ya no parece un trato trivial.
Podemos eliminar fácilmente esta sobrecarga sin ningún impacto en la alineación del campo:
Ahora hemos reducido la memoria de 24 megas a 20 megas. Con un patrón de acceso secuencial, la máquina ahora consumirá estos datos un poco más rápido.
Pero veamos este
birth
campo un poco más de cerca. Digamos que registra el tiempo de inicio cuando nace (se crea) una partícula. Imagine que solo se accede al campo cuando se crea una partícula por primera vez, y cada 10 segundos para ver si una partícula debe morir y renacer en una ubicación aleatoria en la pantalla. En ese caso,birth
es un campo frío. No se accede a él en nuestros bucles de rendimiento crítico.Como resultado, los datos críticos de rendimiento real no son 20 megabytes, sino un bloque contiguo de 12 megabytes. ¡La memoria activa real a la que accedemos con frecuencia se ha reducido a la mitad de su tamaño! Espere aceleraciones significativas con respecto a nuestra solución original de 24 megabytes (no es necesario medirla, ya se ha hecho este tipo de cosas miles de veces, pero si tiene dudas, siéntase libre).
Sin embargo, note lo que hicimos aquí. Rompimos por completo la encapsulación de este objeto de partículas. Su estado ahora se divide entre
Particle
los campos privados de un tipo y una matriz paralela separada. Y ahí es donde el diseño granular orientado a objetos se interpone en el camino.No podemos expresar la representación de datos óptima cuando se limita al diseño de la interfaz de un solo objeto muy granular como una sola partícula, un solo píxel, incluso un solo vector de 4 componentes, posiblemente incluso un solo objeto "criatura" en un juego , etc. Se perderá la velocidad de un guepardo si está parado en una pequeña isla de 2 metros cuadrados, y eso es lo que a menudo hace el diseño orientado a objetos muy granular en términos de rendimiento. Limita la representación de datos a una naturaleza subóptima.
Para llevar esto más lejos, digamos que dado que solo estamos moviendo partículas, podemos acceder a sus campos x / y / z en tres bucles separados. En ese caso, podemos beneficiarnos de las intrínsecas SIMD de estilo SoA con registros AVX que pueden vectorizar 8 operaciones SPFP en paralelo. Pero para hacer esto, ahora debemos usar esta representación:
Ahora estamos volando con la simulación de partículas, pero mira lo que sucedió con nuestro diseño de partículas. Se ha demolido por completo, y ahora estamos viendo 4 matrices paralelas y ningún objeto para agregarlas en absoluto. Nuestro
Particle
diseño orientado a objetos se ha vuelto sayonara.Esto me sucedió muchas veces trabajando en campos críticos para el rendimiento donde los usuarios exigen velocidad, y solo la corrección es lo único que exigen más. Estos pequeños diseños orientados a objetos pequeños tuvieron que ser demolidos, y las roturas en cascada a menudo requerían que usáramos una estrategia de desaprobación lenta hacia el diseño más rápido.
Solución
El escenario anterior solo presenta un problema con los diseños granulares orientados a objetos. En esos casos, a menudo terminamos teniendo que demoler la estructura para expresar representaciones más eficientes como resultado de repeticiones de SoA, división de campo caliente / frío, reducción de relleno para patrones de acceso secuencial (el relleno a veces es útil para el rendimiento con acceso aleatorio patrones en casos de AoS, pero casi siempre un obstáculo para los patrones de acceso secuencial), etc.
Sin embargo, podemos tomar esa representación final en la que nos decidimos y aún modelar una interfaz orientada a objetos:
Ahora estamos bien. Podemos obtener todas las golosinas orientadas a objetos que nos gustan. El guepardo tiene un país entero para cruzar tan rápido como pueda. Nuestros diseños de interfaz ya no nos atrapan en una esquina de cuello de botella.
ParticleSystem
potencialmente puede incluso ser abstracto y usar funciones virtuales. Ahora es discutible, estamos pagando los gastos generales en el nivel de recolección de partículas en lugar de en un nivel por partícula . La sobrecarga es 1 / 1,000,000 de lo que sería de lo contrario si estuviéramos modelando objetos a nivel de partícula individual.Así que esa es la solución en áreas verdaderamente críticas para el rendimiento que manejan una carga pesada, y para todo tipo de lenguajes de programación (esta técnica beneficia a C, C ++, Python, Java, JavaScript, Lua, Swift, etc.). Y no se puede etiquetar fácilmente como "optimización prematura", ya que esto se relaciona con el diseño y la arquitectura de la interfaz . No podemos escribir una base de código que modele una sola partícula como un objeto con una gran cantidad de dependencias del cliente en un
Particle's
interfaz pública y luego cambiar de opinión más tarde. Lo he hecho mucho cuando se me llama para optimizar las bases de código heredadas, y eso puede terminar tomando meses de reescribir cuidadosamente decenas de miles de líneas de código para usar el diseño más voluminoso. Esto idealmente afecta cómo diseñamos las cosas por adelantado, siempre que podamos anticipar una carga pesada.Sigo haciendo eco de esta respuesta de una forma u otra en muchas preguntas de rendimiento, y especialmente en aquellas relacionadas con el diseño orientado a objetos. El diseño orientado a objetos aún puede ser compatible con las necesidades de rendimiento de mayor demanda, pero tenemos que cambiar un poco la forma en que pensamos al respecto. Tenemos que darle a ese guepardo algo de espacio para que corra lo más rápido posible, y eso a menudo es imposible si diseñamos pequeños objetos que apenas almacenan ningún estado.
fuente
Sí, la mentalidad orientada a objetos definitivamente puede ser neutral o negativa cuando se trata de programación de alto rendimiento, tanto a nivel algorítmico como de implementación. Si OOP reemplaza el análisis algorítmico, puede llevarlo a una implementación prematura y, en el nivel más bajo, las abstracciones de OOP deben dejarse de lado.
El problema surge del énfasis de OOP en pensar en instancias individuales. Creo que es justo decir que la forma de pensar de un OOP sobre un algoritmo es pensar en un conjunto específico de valores e implementarlo de esa manera. Si ese es su camino de más alto nivel, es poco probable que se dé cuenta de una transformación o reestructuración que conduzca a grandes ganancias de O.
A nivel algorítmico, a menudo se piensa en el panorama más amplio y en las restricciones o relaciones entre los valores que conducen a grandes ganancias de O. Un ejemplo podría ser que no hay nada en la mentalidad de OOP que lo lleve a transformar "sumar un rango continuo de enteros" de un ciclo a
(max + min) * n/2
En el nivel de implementación, aunque las computadoras son "lo suficientemente rápidas" para la mayoría de los algoritmos de nivel de aplicación, en el código de bajo nivel de rendimiento crítico uno se preocupa mucho por la localidad. Nuevamente, el énfasis de OOP en pensar en una instancia individual y los valores de un paso a través del ciclo pueden ser negativos. En el código de alto rendimiento, en lugar de escribir un bucle directo, es posible que desee desenrollar parcialmente el bucle, agrupar varias instrucciones de carga en la parte superior, luego transformarlas en un grupo y luego escribirlas en un grupo. Todo el tiempo estarías prestando atención a los cálculos intermedios y, enormemente, al acceso a la memoria caché y; problemas donde las abstracciones OOP ya no son válidas. Y, si se sigue, puede ser engañoso: en este nivel, debe conocer y pensar sobre las representaciones a nivel de máquina.
Cuando observa algo como las primitivas de rendimiento de Intel, tiene literalmente miles de implementaciones de la Transformada rápida de Fourier, cada una ajustada para que funcione mejor para un tamaño de datos específico y una arquitectura de máquina. (Fascinantemente, resulta que la mayor parte de estas implementaciones son generadas por la máquina: Markus Püschel Automatic Performance Programming )
Por supuesto, como la mayoría de las respuestas han dicho, para la mayoría del desarrollo, para la mayoría de los algoritmos, la POO es irrelevante para el rendimiento. Mientras no esté "pesimizando prematuramente" y agregando muchas llamadas no locales, el
this
puntero no está ni aquí ni allá.fuente
Está relacionado, y a menudo se pasa por alto.
No es una respuesta fácil, depende de lo que quieras hacer.
Algunos algoritmos son mejores en rendimiento usando programación estructurada simple, mientras que otros son mejores usando orientación a objetos.
Antes de la orientación a objetos, muchas escuelas enseñan el diseño de algoritmos (ed) con programación estructurada. Hoy en día, muchas escuelas enseñan programación orientada a objetos, ignorando el diseño y el rendimiento de algoritmos.
Por supuesto, allí donde las escuelas que enseñan programación estructurada, que no se preocupaban por los algoritmos, en absoluto.
fuente
El rendimiento se reduce a los ciclos de CPU y memoria al final. Pero la diferencia porcentual entre la sobrecarga de mensajes y encapsulación de OOP y una semántica de programación más abierta puede o no ser un porcentaje lo suficientemente significativo como para marcar una diferencia notable en el rendimiento de su aplicación. Si una aplicación está unida al disco o a la memoria caché de datos, cualquier sobrecarga de OOP puede perderse completamente en el ruido.
Pero, en los bucles internos del procesamiento de señales e imágenes en tiempo real y otras aplicaciones de cómputo numérico, la diferencia puede ser un porcentaje significativo de los ciclos de CPU y memoria, lo que puede hacer que la sobrecarga de OOP sea mucho más costosa de ejecutar.
La semántica de un lenguaje OOP en particular puede o no exponer suficientes oportunidades para que el compilador optimice esos ciclos, o para que los circuitos de predicción de rama de la CPU siempre adivinen correctamente y cubran esos ciclos con pre-captación y canalización.
fuente
Un buen diseño orientado a objetos me ayudó a acelerar considerablemente una aplicación. A tuvo que generar gráficos complejos de forma algorítmica. Lo hice a través de la automatización de Microsoft Visio. Trabajé, pero fue increíblemente lento. Afortunadamente, había insertado un nivel adicional de abstracción entre la lógica (el algoritmo) y las cosas de Visio. Mi componente Visio expuso su funcionalidad a través de una interfaz. Esto me permitió reemplazar fácilmente el componente lento con otro que crea archivos SVG, ¡eso fue al menos 50 veces más rápido! Sin un enfoque orientado a objetos limpio, los códigos para el algoritmo y el control de la Visión se habrían enredado de alguna manera, lo que habría convertido el cambio en una pesadilla.
fuente