(Esto está dirigido principalmente a aquellos que tienen un conocimiento específico de los sistemas de baja latencia, para evitar que las personas simplemente respondan con opiniones sin fundamento).
¿Siente que hay una compensación entre escribir código orientado a objetos "agradable" y escribir código de baja latencia muy rápido? Por ejemplo, ¿evitar funciones virtuales en C ++ / la sobrecarga del polimorfismo, etc., reescribir código que parece desagradable, pero es muy rápido, etc.?
Es lógico, a quién le importa si se ve feo (siempre que sea mantenible), si necesita velocidad, ¿necesita velocidad?
Me interesaría saber de personas que han trabajado en tales áreas.
Respuestas:
Sí.
Por eso existe la frase "optimización prematura". Existe para obligar a los desarrolladores a medir su rendimiento y solo optimizar ese código que marcará una diferencia en el rendimiento, al tiempo que diseña de manera sensata su arquitectura de aplicación desde el principio para que no caiga bajo una gran carga.
De esa manera, en la mayor medida posible, puede mantener su código bonito, bien diseñado y orientado a objetos, y solo optimizar con código feo esas pequeñas porciones que importan.
fuente
Sí, el ejemplo que doy no es C ++ vs. Java, sino Assembly vs. COBOL, ya que es lo que sé.
Ambos idiomas son muy rápidos, pero incluso COBOL cuando se compila tiene muchas más instrucciones que se colocan en el conjunto de instrucciones que no necesariamente tienen que estar allí en lugar de escribir esas instrucciones usted mismo en la Asamblea.
La misma idea se puede aplicar directamente a su pregunta de escribir "código de aspecto feo" versus usar herencia / polimorfismo en C ++. Creo que es necesario escribir un código de aspecto feo, si el usuario final necesita plazos de transacción inferiores a un segundo, entonces es nuestro trabajo como programadores darles eso sin importar cómo suceda.
Dicho esto, el uso liberal de los comentarios aumenta en gran medida la funcionalidad y la capacidad de mantenimiento del programador, sin importar cuán feo sea el código.
fuente
Sí, existe una compensación. Con esto quiero decir que el código que es más rápido y más feo no es necesariamente mejor: los beneficios cuantitativos del "código rápido" deben sopesarse contra la complejidad de mantenimiento de los cambios de código necesarios para lograr esa velocidad.
La compensación proviene del costo comercial. El código que es más complejo requiere programadores más hábiles (y los programadores con un conjunto de habilidades más enfocado, como aquellos con arquitectura de CPU y conocimiento de diseño), toma más tiempo leer y comprender el código y corregir errores. El costo comercial de desarrollar y mantener dicho código podría estar en el rango de 10x - 100x sobre el código escrito normalmente.
Este costo de mantenimiento es justificable en algunas industrias , en las que los clientes están dispuestos a pagar una prima muy alta por un software muy rápido.
Algunas optimizaciones de velocidad hacen un mejor retorno de la inversión (ROI) que otras. Es decir, algunas técnicas de optimización se pueden aplicar con un menor impacto en la capacidad de mantenimiento del código (preservar la estructura de nivel superior y la legibilidad de nivel inferior) en comparación con el código escrito normalmente.
Por lo tanto, el dueño de un negocio debe:
Estas compensaciones son muy específicas de las circunstancias.
No se pueden decidir de manera óptima sin la participación de gerentes y propietarios de productos.
Estos son altamente específicos para las plataformas. Por ejemplo, las CPU de escritorio y móviles tienen diferentes consideraciones. Las aplicaciones de servidor y cliente también tienen diferentes consideraciones.
Sí, generalmente es cierto que el código más rápido se ve diferente del código escrito normalmente. Cualquier código diferente tomará más tiempo para leer. Si eso implica fealdad está en los ojos del espectador.
Las técnicas con las que tengo cierta exposición son: (sin intentar reclamar ningún nivel de experiencia) optimización de vector corto (SIMD), paralelismo de tareas específicas, preasignación de memoria y reutilización de objetos.
SIMD generalmente tiene graves impactos en la legibilidad de bajo nivel, a pesar de que generalmente no requiere cambios estructurales de mayor nivel (siempre que la API esté diseñada teniendo en cuenta la prevención de cuellos de botella).
Algunos algoritmos se pueden transformar en SIMD fácilmente (el vergonzosamente vectorizable). Algunos algoritmos requieren más reordenamientos de cómputo para usar SIMD. En casos extremos, como el paralelismo SIMD de frente de onda, se deben escribir algoritmos completamente nuevos (e implementaciones patentables) para aprovecharlos.
La paralelización de tareas de grano fino requiere la reorganización de algoritmos en gráficos de flujo de datos, y aplica repetidamente la descomposición funcional (computacional) al algoritmo hasta que no se pueda obtener un mayor margen de beneficio. Las etapas descompuestas generalmente se encadenan con un estilo de continuación, un concepto tomado de la programación funcional.
Por descomposición funcional (computacional), los algoritmos que podrían haberse escrito normalmente en una secuencia lineal y conceptualmente clara (líneas de código que son ejecutables en el mismo orden en que se escriben) deben dividirse en fragmentos y distribuirse en múltiples funciones o clases. (Consulte la objetivación del algoritmo, a continuación.) Este cambio impedirá en gran medida a los programadores que no estén familiarizados con el proceso de diseño de descomposición que dio lugar a dicho código.
Para que dicho código sea mantenible, los autores de dicho código deben escribir documentaciones elaboradas del algoritmo, mucho más allá del tipo de comentario de código o diagramas UML realizados para el código escrito normalmente. Esto es similar a la forma en que los investigadores escriben sus trabajos académicos.
No, el código rápido no necesita estar en contradicción con la orientación a objetos.
Dicho de otra manera, es posible implementar un software muy rápido que todavía está orientado a objetos. Sin embargo, hacia el extremo inferior de esa implementación (en el nivel básico donde ocurre la mayoría de los cálculos), el diseño del objeto puede diferir significativamente de los diseños obtenidos del diseño orientado a objetos (OOD). El diseño de nivel inferior está orientado a la objetivación de algoritmos.
Algunos beneficios de la programación orientada a objetos (OOP), como la encapsulación, el polimorfismo y la composición, aún se pueden obtener de la objetivación de algoritmos de bajo nivel. Esta es la principal justificación para usar OOP en este nivel.
La mayoría de los beneficios del diseño orientado a objetos (OOD) se pierden. Lo más importante, no hay intuición en el diseño de bajo nivel. Un programador no puede aprender a trabajar con el código de nivel inferior sin comprender primero cómo se había transformado y descompuesto el algoritmo en primer lugar, y esta comprensión no se puede obtener del código resultante.
fuente
Sí, a veces el código tiene que ser "feo" para que funcione en el tiempo requerido, aunque no todo el código tiene que ser feo. El rendimiento debe probarse y perfilarse antes para encontrar los bits de código que deben ser "feos" y esas secciones deben anotarse con un comentario para que los futuros desarrolladores sepan qué es feo a propósito y qué es solo pereza. Si alguien está escribiendo un montón de código mal diseñado alegando razones de rendimiento, haga que lo prueben.
La velocidad es tan importante como cualquier otro requisito de un programa, dar correcciones incorrectas a un misil guiado es equivalente a proporcionar las correcciones correctas después del impacto. La mantenibilidad es siempre una preocupación secundaria para el código de trabajo.
fuente
Algunos de los estudios de los que he visto extractos indican que el código limpio y fácil de leer suele ser más rápido que el código complejo más difícil de leer. En parte, esto se debe a la forma en que se diseñan los optimizadores. Tienden a ser mucho mejores para optimizar una variable en un registro, que hacer lo mismo con un resultado intermedio de un cálculo. Las secuencias largas de asignaciones que usan un solo operador que conducen al resultado final pueden optimizarse mejor que una ecuación larga y complicada. Los optimizadores más nuevos pueden haber reducido la diferencia entre el código limpio y el complicado, pero dudo que lo hayan eliminado.
Se pueden agregar otras optimizaciones, como el desenrollado de bucle, de manera limpia cuando sea necesario.
Cualquier optimización añadida para mejorar el rendimiento debe ir acompañada de un comentario apropiado. Esto debe incluir una declaración de que se agregó como una optimización, preferiblemente con medidas de rendimiento antes y después.
He descubierto que la regla 80/20 se aplica al código que he optimizado. Como regla general, no optimizo nada que no me lleve al menos el 80% del tiempo. Luego apunto (y generalmente logro) un aumento de rendimiento de 10 veces. Esto mejora el rendimiento aproximadamente 4 veces. La mayoría de las optimizaciones que he implementado no han hecho que el código sea significativamente menos "hermoso". Su experiencia puede ser diferente.
fuente
Si por feo te refieres a difícil de leer / entender en el nivel en que otros desarrolladores lo reutilizarán o necesitarán entenderlo, entonces diría que un código elegante y fácil de leer casi siempre te dará un aumento de rendimiento a largo plazo en una aplicación que debe mantener.
De lo contrario, a veces hay una victoria de rendimiento suficiente para que valga la pena ponerlo feo en una hermosa caja con una interfaz asesina, pero en mi experiencia, este es un dilema bastante raro.
Piense en evitar el trabajo básico a medida que avanza. Guarde los trucos arcanos para cuando realmente se presente un problema de rendimiento. Y si tiene que escribir algo que alguien solo pueda entender a través de la familiaridad con la optimización específica, haga lo que pueda para que al menos lo feo sea fácil de entender desde la reutilización de su punto de vista del código. El código que funciona miserablemente rara vez lo hace porque los desarrolladores estaban pensando demasiado en lo que el próximo tipo heredaría, pero si los cambios frecuentes son la única constante de una aplicación (la mayoría de las aplicaciones web en mi experiencia), un código rígido / inflexible que es difícil de modificar es prácticamente suplicar que los desordenes en pánico comiencen a aparecer en toda su base de código. Limpio y delgado es mejor para el rendimiento a largo plazo.
fuente
Complejo y feo no son lo mismo. El código que tiene muchos casos especiales, que está optimizado para buscar hasta la última gota de rendimiento, y que al principio parece una maraña de conexiones y dependencias , de hecho, puede ser diseñado con mucho cuidado y bastante hermoso una vez que lo entienda. De hecho, si el rendimiento (ya sea medido en términos de latencia u otra cosa) es lo suficientemente importante como para justificar un código muy complejo, entonces el código debe estar bien diseñado. Si no es así, no puede estar seguro de que toda esa complejidad sea realmente mejor que una solución más simple.
El código feo, para mí, es un código descuidado, mal considerado y / o innecesariamente complicado. No creo que quieras ninguna de esas características en el código que tiene que funcionar.
fuente
Trabajo en un campo que está un poco más centrado en el rendimiento que en la latencia, pero es muy crítico para el rendimiento y diría "algo así" .
Sin embargo, un problema es que muchas personas tienen sus nociones de rendimiento completamente equivocadas. Los principiantes a menudo se equivocan casi por completo, y todo su modelo conceptual de "costo computacional" necesita una revisión, y solo la complejidad algorítmica es lo único que pueden hacer bien. Los intermedios hacen muchas cosas mal. Los expertos se equivocan en algunas cosas.
Medir con herramientas precisas que pueden proporcionar métricas como errores de caché y predicciones erróneas de sucursales es lo que mantiene a todas las personas de cualquier nivel de experiencia en el campo bajo control.
La medición también es lo que indica qué no optimizar . Los expertos a menudo dedican menos tiempo a la optimización que los novatos, ya que están optimizando puntos de acceso medidos verdaderos y no están tratando de optimizar las puñaladas salvajes en la oscuridad basadas en corazonadas sobre lo que podría ser lento (lo que, en forma extrema, podría tentar a uno a micro-optimizar solo sobre cualquier otra línea en la base de código).
Diseñando para el rendimiento
Con eso aparte, la clave para diseñar para el rendimiento proviene de la parte de diseño , como en el diseño de interfaz. Uno de los problemas con la inexperiencia es que tiende a haber un cambio temprano en las métricas de implementación absoluta, como el costo de una llamada de función indirecta en algún contexto generalizado, como si el costo (que se entiende mejor en un sentido inmediato desde el punto de vista del optimizador es una razón para evitarlo en toda la base de código.
Los costos son relativos . Si bien hay un costo para una llamada de función indirecta, por ejemplo, todos los costos son relativos. Si está pagando ese costo una vez para llamar a una función que recorre millones de elementos, preocuparse por este costo es como pasar horas regateando centavos por la compra de un producto de mil millones de dólares, solo para concluir que no comprar ese producto porque Era un centavo demasiado caro.
Diseño de interfaz más grueso
El aspecto del diseño de la interfaz del rendimiento a menudo busca antes llevar estos costos a un nivel más grueso. En lugar de pagar los costos de abstracción de tiempo de ejecución para una sola partícula, por ejemplo, podríamos llevar ese costo al nivel del sistema / emisor de partículas, convirtiendo efectivamente una partícula en un detalle de implementación y / o simplemente datos sin procesar de esta colección de partículas.
Por lo tanto, el diseño orientado a objetos no tiene que ser incompatible con el diseño para el rendimiento (ya sea latencia o rendimiento), pero puede haber tentaciones en un lenguaje que se enfoca en él para modelar objetos granulares cada vez más pequeños, y allí el último optimizador no puede ayuda. No puede hacer cosas como fusionar una clase que representa un solo punto de una manera que produce una representación eficiente de SoA para los patrones de acceso a la memoria del software. Una colección de puntos con el diseño de interfaz modelado a nivel de grosería ofrece esa oportunidad y permite iterar hacia soluciones cada vez más óptimas según sea necesario. Tal diseño está diseñado para memoria masiva *.
Una gran cantidad de diseños críticos para el rendimiento en realidad pueden ser muy compatibles con la noción de diseños de interfaz de alto nivel que son fáciles de entender y usar para los humanos. La diferencia es que "alto nivel" en este contexto se trataría de la agregación masiva de memoria, una interfaz modelada para colecciones de datos potencialmente grandes y con una implementación oculta que puede ser de nivel bastante bajo. Una analogía visual podría ser un automóvil que es realmente cómodo y fácil de manejar y manejar, y muy seguro a la velocidad del sonido, pero si abres el capó, hay pequeños demonios que escupen fuego dentro.
Con un diseño más grueso también tiende a ser una forma más fácil de proporcionar patrones de bloqueo más eficientes y explotar el paralelismo en el código (el subprocesamiento múltiple es un tema exhaustivo que voy a omitir aquí).
Pool de memoria
Un aspecto crítico de la programación de baja latencia probablemente será un control muy explícito sobre la memoria para mejorar la localidad de referencia, así como solo la velocidad general de asignación y desasignación de memoria. Una memoria de agrupación de asignadores personalizados en realidad se hace eco del mismo tipo de mentalidad de diseño que describimos. Está diseñado para granel ; Está diseñado en un nivel grueso. Preasigna memoria en bloques grandes y agrupa la memoria ya asignada en pequeños fragmentos.
La idea es exactamente la misma de impulsar cosas costosas (asignar una porción de memoria contra un asignador de propósito general, por ejemplo) a un nivel más y más grueso. Un grupo de memoria está diseñado para manejar la memoria en masa .
Sistemas de tipo segregar memoria
Una de las dificultades con el diseño orientado a objetos granular en cualquier lenguaje es que a menudo quiere introducir una gran cantidad de tipos y estructuras de datos pequeños definidos por el usuario. Esos tipos pueden querer asignarse en pequeños fragmentos si se asignan dinámicamente.
Un ejemplo común en C ++ sería para los casos en que se requiere polimorfismo, donde la tentación natural es asignar cada instancia de una subclase contra un asignador de memoria de propósito general.
Esto termina separando los diseños de memoria posiblemente contiguos en pequeños bits y pedazos dispersos en el rango de direccionamiento que se traduce en más fallas de página y errores de caché.
Los campos que exigen la respuesta determinista de menor latencia, sin tartamudeos, son probablemente el único lugar donde los puntos críticos no siempre se reducen a un solo cuello de botella, donde las pequeñas ineficiencias pueden realmente acumularse (algo que mucha gente imagina sucede incorrectamente con un generador de perfiles para mantenerlos bajo control, pero en los campos controlados por la latencia, en realidad puede haber algunos casos raros donde se acumulan pequeñas ineficiencias). Y muchas de las razones más comunes para tal acumulación pueden ser esta: la asignación excesiva de pequeños fragmentos de memoria en todo el lugar.
En lenguajes como Java, puede ser útil usar más matrices de tipos de datos antiguos simples cuando sea posible para áreas con embotellamiento (áreas procesadas en bucles estrechos) como una matriz de
int
(pero aún detrás de una interfaz voluminosa de alto nivel) en lugar de, digamos , unaArrayList
deInteger
objetos definidos por el usuario . Esto evita la segregación de memoria que típicamente acompañaría a este último. En C ++, no tenemos que degradar tanto la estructura si nuestros patrones de asignación de memoria son eficientes, ya que los tipos definidos por el usuario pueden asignarse de manera contigua allí e incluso en el contexto de un contenedor genérico.Fusionar memoria de nuevo juntos
Una solución aquí es alcanzar un asignador personalizado para tipos de datos homogéneos, y posiblemente incluso a través de tipos de datos homogéneos. Cuando pequeños tipos de datos y estructuras de datos se aplanan en bits y bytes en la memoria, adquieren una naturaleza homogénea (aunque con algunos requisitos de alineación variables). Cuando no los miramos desde una mentalidad centrada en la memoria, el sistema tipo de lenguajes de programación "quiere" dividir / segregar regiones de memoria potencialmente contiguas en pequeños fragmentos dispersos.
La pila utiliza este enfoque centrado en la memoria para evitar esto y potencialmente almacenar cualquier posible combinación mixta de instancias de tipo definidas por el usuario dentro de ella. Utilizar la pila más es una gran idea cuando sea posible, ya que la parte superior de la misma casi siempre está en una línea de caché, pero también podemos diseñar asignadores de memoria que imiten algunas de estas características sin un patrón LIFO, fusionando la memoria a través de tipos de datos dispares en forma contigua. fragmentos incluso para patrones de asignación de memoria y desasignación más complejos.
El hardware moderno está diseñado para estar en su apogeo al procesar bloques contiguos de memoria (accediendo repetidamente a la misma línea de caché, la misma página, por ejemplo). La palabra clave allí es contigüidad, ya que esto solo es beneficioso si hay datos de interés en torno. Por lo tanto, gran parte de la clave (pero también la dificultad) del rendimiento es fusionar fragmentos de memoria segregados nuevamente en bloques contiguos a los que se accede en su totalidad (todos los datos circundantes son relevantes) antes del desalojo. El rico sistema de tipos de tipos especialmente definidos por el usuario en lenguajes de programación puede ser el mayor obstáculo aquí, pero siempre podemos alcanzar y resolver el problema a través de un asignador personalizado y / o diseños más voluminosos cuando sea apropiado.
Feo
"Feo" es difícil de decir. Es una métrica subjetiva, y alguien que trabaja en un campo muy crítico para el rendimiento comenzará a cambiar su idea de "belleza" a una que esté mucho más orientada a los datos y se centre en interfaces que procesen cosas en masa.
Peligroso
"Peligroso" podría ser más fácil. En general, el rendimiento tiende a querer alcanzar el código de nivel inferior. Implementar un asignador de memoria, por ejemplo, es imposible sin llegar por debajo de los tipos de datos y trabajar en el nivel peligroso de bits y bytes sin procesar. Como resultado, puede ayudar a aumentar el enfoque en un procedimiento de prueba cuidadoso en estos subsistemas críticos para el rendimiento, escalando la minuciosidad de las pruebas con el nivel de optimizaciones aplicadas.
Belleza
Sin embargo, todo esto estaría en el nivel de detalle de implementación. Tanto en una mentalidad veterana como a gran escala y crítica del rendimiento, la "belleza" tiende a cambiar hacia diseños de interfaz en lugar de detalles de implementación. Se convierte en una prioridad exponencialmente mayor buscar interfaces "hermosas", utilizables, seguras y eficientes en lugar de implementaciones debido a roturas de acoplamiento y cascada que pueden ocurrir ante un cambio en el diseño de la interfaz. Las implementaciones se pueden cambiar en cualquier momento. Por lo general, iteramos hacia el rendimiento según sea necesario y como lo indican las mediciones. La clave con el diseño de la interfaz es modelar a un nivel lo suficientemente grueso como para dejar espacio para tales iteraciones sin romper todo el sistema.
De hecho, sugeriría que el enfoque de un veterano en el desarrollo crítico para el rendimiento a menudo tenderá a centrarse principalmente en la seguridad, las pruebas, la mantenibilidad, solo el discípulo de SE en general, ya que una base de código a gran escala que tiene una serie de resultados Los subsistemas críticos (sistemas de partículas, algoritmos de procesamiento de imágenes, procesamiento de video, retroalimentación de audio, trazadores de rayos, motores de malla, etc.) deberán prestar mucha atención a la ingeniería del software para evitar ahogarse en una pesadilla de mantenimiento. No es una mera coincidencia que a menudo los productos más asombrosamente eficientes que existen también pueden tener la menor cantidad de errores.
TL; DR
De todos modos, esa es mi opinión sobre el tema, que abarca desde las prioridades en campos genuinamente críticos para el rendimiento, lo que puede reducir la latencia y causar que se acumulen pequeñas ineficiencias, y lo que en realidad constituye "belleza" (al mirar las cosas de manera más productiva).
fuente
No para ser diferente, pero esto es lo que hago:
Escríbalo limpio y mantenible.
Haga un diagnóstico de rendimiento y solucione los problemas que le indica, no los que usted adivina. Garantizado, serán diferentes de lo que espera.
Puede hacer estas correcciones de una manera que sea clara y fácil de mantener, pero deberá agregar comentarios para que las personas que miran el código sepan por qué lo hicieron de esa manera. Si no lo haces, lo deshacerán.
Entonces, ¿hay una compensación? Realmente no lo creo.
fuente
Puede escribir código feo que sea muy rápido y también puede escribir código hermoso que sea tan rápido como su código feo. El cuello de botella no estará en la belleza / organización / estructura de su código, sino en las técnicas que elija. Por ejemplo, ¿está utilizando enchufes sin bloqueo? ¿Está utilizando un diseño de subproceso único? ¿Está utilizando una cola sin bloqueo para la comunicación entre subprocesos? ¿Estás produciendo basura para el GC? ¿Está realizando alguna operación de bloqueo de E / S en el hilo crítico? Como puede ver, esto no tiene nada que ver con la belleza.
fuente
¿Qué le importa al usuario final?
Caso 1: código incorrecto optimizado
Caso 2: buen código no optimizado
¿Solución?
Fácil, optimice el rendimiento de piezas de código críticas
p.ej:
Un programa que consta de 5 métodos , 3 de ellos para la gestión de datos, 1 para la lectura del disco y el otro para la escritura del disco.
Estos 3 métodos de gestión de datos utilizan los dos métodos de E / S y dependen de ellos
Optimizaríamos los métodos de E / S.
Motivo: es menos probable que se modifiquen los métodos de E / S, ni afectan el diseño de la aplicación, y en general, todo en ese programa depende de ellos, y por lo tanto parecen críticos para el rendimiento, usaríamos cualquier código para optimizarlos .
Esto significa que obtenemos un buen código y un diseño manejable del programa mientras lo mantenemos rápido al optimizar ciertas partes del código
Estoy pensando..
Creo que un código incorrecto dificulta que los humanos optimicen el pulido y los pequeños errores pueden empeorarlo aún más, por lo que un buen código para un principiante / principiante sería mejor si solo escribiera ese código feo.
fuente