Un tema recurrente en SE que he notado en muchas preguntas es el argumento continuo de que C ++ es más rápido y / o más eficiente que los lenguajes de nivel superior como Java. El contraargumento es que JVM o CLR modernos pueden ser tan eficientes gracias a JIT y demás para un número creciente de tareas y que C ++ es cada vez más eficiente si sabes lo que estás haciendo y por qué haces las cosas de cierta manera merecerá aumentos de rendimiento. Eso es obvio y tiene mucho sentido.
Me gustaría saber una explicación básica (si existe tal cosa ...) sobre por qué y cómo ciertas tareas son más rápidas en C ++ que JVM o CLR. ¿Es simplemente porque C ++ se compila en código de máquina mientras que JVM o CLR todavía tienen la sobrecarga de procesamiento de la compilación JIT en tiempo de ejecución?
Cuando trato de investigar el tema, todo lo que encuentro son los mismos argumentos que he esbozado anteriormente sin ninguna información detallada para comprender exactamente cómo se puede utilizar C ++ para la informática de alto rendimiento.
fuente
Respuestas:
Se trata de la memoria (no del JIT). La 'ventaja de JIT sobre C' se limita principalmente a la optimización de llamadas virtuales o no virtuales a través de la inserción, algo en lo que la CPU BTB ya está trabajando arduamente.
En las máquinas modernas, el acceso a la RAM es realmente lento (en comparación con cualquier cosa que haga la CPU), lo que significa que las aplicaciones que usan las memorias caché tanto como sea posible (que es más fácil cuando se usa menos memoria) pueden ser hasta cien veces más rápidas que las que no lo hagas Y hay muchas maneras en que Java usa más memoria que C ++ y hace que sea más difícil escribir aplicaciones que exploten completamente la caché:
Algunos otros factores relacionados con la memoria pero no con la memoria caché:
Algunas de estas cosas son compensaciones (no tener que hacer una gestión manual de la memoria vale la pena renunciar a un gran rendimiento para la mayoría de las personas), algunas son probablemente el resultado de tratar de mantener Java simple, y algunas son errores de diseño (aunque posiblemente solo en retrospectiva , es decir, UTF-16 era una codificación de longitud fija cuando se creó Java, lo que hace que la decisión de elegirla sea mucho más comprensible).
Vale la pena señalar que muchas de estas compensaciones son muy diferentes para Java / JVM que para C # / CIL. .NET CIL tiene estructuras de tipo de referencia, asignación / paso de pila, matrices empaquetadas de estructuras y genéricos de tipo instanciado.
fuente
Parcialmente, pero en general, suponiendo un compilador JIT de vanguardia absolutamente fantástico, el código C ++ adecuado aún tiende a funcionar mejor que el código Java por DOS razones principales:
1) Las plantillas C ++ proporcionan mejores facilidades para escribir código que es genérico Y eficiente . Las plantillas proporcionan al programador de C ++ una abstracción muy útil que tiene una sobrecarga de tiempo de ejecución CERO. (Las plantillas son básicamente tipeos en tiempo de compilación). En contraste, lo mejor que obtienes con los genéricos de Java son básicamente funciones virtuales. Las funciones virtuales siempre tienen una sobrecarga de tiempo de ejecución, y generalmente no se pueden alinear.
En general, la mayoría de los lenguajes, incluidos Java, C # e incluso C, le permiten elegir entre eficiencia y generalidad / abstracción. Las plantillas de C ++ le ofrecen ambas (a costa de tiempos de compilación más largos).
2) El hecho de que el estándar C ++ no tenga mucho que decir sobre el diseño binario de un programa compilado de C ++ le da a los compiladores de C ++ mucho más margen de maniobra que un compilador de Java, lo que permite mejores optimizaciones (a veces a costa de una mayor dificultad de depuración). ) De hecho, la naturaleza misma de la especificación del lenguaje Java impone una penalización de rendimiento en ciertas áreas. Por ejemplo, no puede tener una matriz contigua de objetos en Java. Solo puede tener una matriz contigua de punteros de objetos(referencias), lo que significa que iterar sobre una matriz en Java siempre incurre en el costo de la indirección. Sin embargo, la semántica de valor de C ++ permite matrices contiguas. Otra diferencia es el hecho de que C ++ permite que los objetos se asignen en la pila, mientras que Java no, lo que significa que, en la práctica, dado que la mayoría de los programas de C ++ tienden a asignar objetos en la pila, el costo de la asignación suele ser cercano a cero.
Un área donde C ++ podría estar rezagado con respecto a Java es cualquier situación en la que se deban asignar muchos objetos pequeños en el montón. En este caso, el sistema de recolección de basura de Java probablemente dará como resultado un mejor rendimiento que el estándar
new
ydelete
en C ++ porque el GC de Java permite la desasignación masiva. Pero, de nuevo, un programador de C ++ puede compensar esto utilizando un grupo de memoria o un asignador de losas, mientras que un programador de Java no tiene ningún recurso cuando se enfrenta a un patrón de asignación de memoria para el que el tiempo de ejecución de Java no está optimizado.Además, vea esta excelente respuesta para obtener más información sobre este tema.
fuente
std::vector<int>
lo tanto, es una matriz dinámica diseñada solo para ints, y el compilador puede optimizarla en consecuencia. AC #List<int>
sigue siendo solo unList
.List<int>
usa unint[]
, no unObject[]
Java como lo hace. Ver stackoverflow.com/questions/116988/…vector<N>
donde, para el caso específico devector<4>
, debería usarse mi implementación SIMD codificada a manoLo que las otras respuestas (6 hasta ahora) parecen haber olvidado mencionar, pero lo que considero muy importante para responder esto, es una de las filosofías de diseño muy básicas de C ++, que fue formulada y empleada por Stroustrup desde el día 1:
No pagas por lo que no usas.
Hay algunos otros principios de diseño subyacentes importantes que moldearon en gran medida C ++ (como que no debería forzarse a un paradigma específico), pero no paga por lo que no usa, es uno de los más importantes.
En su libro The Design and Evolution of C ++ (generalmente conocido como [D&E]), Stroustrup describe qué necesidad tenía que lo hizo pensar en C ++ en primer lugar. En mis propias palabras: para su tesis doctoral (algo que tiene que ver con simulaciones de red, IIRC), implementó un sistema en SIMULA, que le gustó mucho, porque el lenguaje era muy bueno al permitirle expresar sus pensamientos directamente en código. Sin embargo, el programa resultante corrió demasiado lento, y para obtener un título, reescribió la cosa en BCPL, un predecesor de C. Escribir el código en BCPL que describe como una molestia, pero el programa resultante fue lo suficientemente rápido para entregar resultados, lo que le permitió terminar su doctorado.
Después de eso, quería un lenguaje que permitiera traducir los problemas del mundo real en código de la manera más directa posible, pero también permitiría que el código fuera muy eficiente.
En la búsqueda de eso, creó lo que más tarde se convertiría en C ++.
Por lo tanto, el objetivo citado anteriormente no es simplemente uno de varios principios fundamentales de diseño subyacentes, está muy cerca de la razón de ser de C ++. Y se puede encontrar en casi todas partes en el idioma: las funciones son solo
virtual
cuando lo desea (porque llamar a funciones virtuales viene con una ligera sobrecarga) Los POD solo se inicializan automáticamente cuando lo solicita explícitamente, las excepciones solo le cuestan rendimiento cuando realmente tirarlos (mientras que era un objetivo de diseño explícito permitir que la configuración / limpieza de los marcos de pila sea muy barata), no se ejecuta GC cuando lo desee, etc.C ++ eligió explícitamente no brindarle algunas comodidades ("¿tengo que hacer que este método sea virtual aquí?") A cambio de rendimiento ("no, no lo hago, y ahora el compilador puede
inline
hacerlo y optimizar el heck del ¡todo! "), y, como era de esperar, esto de hecho resultó en mejoras de rendimiento en comparación con los idiomas que son más convenientes.fuente
¿Conoces el trabajo de investigación de Google sobre ese tema?
De la conclusión:
Esto es al menos parcialmente una explicación, en el sentido de "porque los compiladores de C ++ del mundo real producen un código más rápido que los compiladores de Java por medidas empíricas".
fuente
Este no es un duplicado de sus preguntas, pero la respuesta aceptada responde a la mayoría de sus preguntas: una revisión moderna de Java
Para resumir:
Entonces, dependiendo de con qué otro idioma compare C ++, puede obtener o no la misma respuesta.
En C ++ tienes:
Estas son las características o efectos secundarios de la definición del lenguaje que la hacen teóricamente más eficiente en memoria y velocidad que cualquier lenguaje que:
La alineación agresiva de C ++ del compilador reduce o elimina muchas indirecciones. La capacidad de generar un pequeño conjunto de datos compactos lo hace compatible con la memoria caché si no distribuye estos datos por toda la memoria en lugar de empaquetarlos (ambos son posibles, C ++ simplemente le permite elegir). RAII hace que el comportamiento de la memoria C ++ sea predecible, eliminando muchos problemas en caso de simulaciones en tiempo real o semi-en tiempo real, que requieren alta velocidad. Los problemas de localidad, en general, se pueden resumir en esto: cuanto más pequeño es el programa / datos, más rápida es la ejecución. C ++ proporciona diversas formas de asegurarse de que sus datos estén donde desea que estén (en un grupo, una matriz o lo que sea) y que sean compactos.
Obviamente, hay otros lenguajes que pueden hacer lo mismo, pero son menos populares porque no proporcionan tantas herramientas de abstracción como C ++, por lo que son menos útiles en muchos casos.
fuente
Se trata principalmente de memoria (como dijo Michael Borgwardt) con un poco de ineficiencia JIT agregada.
Una cosa que no se menciona es el caché: para usar el caché por completo, necesita que sus datos se presenten de manera contigua (es decir, todos juntos). Ahora con un sistema GC, la memoria se asigna en el montón de GC, lo cual es rápido, pero a medida que se usa la memoria, el GC se activará regularmente y eliminará los bloques que ya no se usan y luego se compactará el resto. Ahora, aparte de la obvia lentitud de mover esos bloques usados juntos, esto significa que los datos que está utilizando pueden no estar unidos. Si tiene una matriz de 1000 elementos, a menos que los haya asignado todos a la vez (y luego haya actualizado su contenido en lugar de eliminar y crear nuevos, que se crearán al final del montón), estos se dispersarán por todo el montón, por lo tanto, requiere varios golpes de memoria para leerlos todos en la memoria caché de la CPU. La aplicación AC / C ++ probablemente asignará la memoria para estos elementos y luego actualizará los bloques con los datos. (ok, hay estructuras de datos como una lista que se comportan más como las asignaciones de memoria del GC, pero la gente sabe que son más lentas que los vectores).
Puede ver esto en funcionamiento simplemente reemplazando cualquier objeto StringBuilder con String ... Stringbuilders funciona preasignando memoria y llenándola, y es un truco de rendimiento conocido para sistemas java / .NET.
No olvide que el paradigma 'eliminar viejas y asignar nuevas copias' se usa mucho en Java / C #, simplemente porque a las personas se les dice que las asignaciones de memoria son realmente rápidas debido al GC, por lo que el modelo de memoria dispersa se usa en todas partes ( a excepción de los creadores de cadenas, por supuesto), por lo que todas sus bibliotecas tienden a desperdiciar memoria y la usan mucho, ninguna de las cuales obtiene el beneficio de la contigüidad. Culpa al bombo de GC por esto: te dijeron que la memoria estaba libre, jajaja.
El GC en sí es obviamente otro golpe de rendimiento: cuando se ejecuta, no solo tiene que barrer el montón, sino que también tiene que liberar todos los bloques no utilizados, y luego debe ejecutar cualquier finalizador (aunque esto solía hacerse por separado). la próxima vez con la aplicación detenida) (No sé si todavía es un éxito, pero todos los documentos que leo dicen que solo usan finalizadores si es realmente necesario) y luego tienen que mover esos bloques a su posición para que el montón esté compactado y actualizar la referencia a la nueva ubicación del bloque. Como puede ver, ¡es mucho trabajo!
Los éxitos de rendimiento para la memoria C ++ se reducen a asignaciones de memoria: cuando necesita un nuevo bloque, debe recorrer el montón buscando el siguiente espacio libre que sea lo suficientemente grande, con un montón muy fragmentado, esto no es tan rápido como un GC `` solo asigne otro bloque al final '', pero creo que no es tan lento como todo el trabajo que realiza la compactación del GC y puede mitigarse mediante el uso de múltiples bloques de bloques de tamaño fijo (también conocidos como grupos de memoria).
Hay más ... como cargar ensamblajes desde el GAC que requieren verificación de seguridad, rutas de sondeo (¡ activa sxstrace y solo mira lo que se está haciendo!) Y otras ingenierías generales que parecen ser mucho más populares con java / .net que C / C ++.
fuente
"¿Es simplemente porque C ++ se compila en código ensamblador / máquina mientras que Java / C # todavía tiene la sobrecarga de procesamiento de la compilación JIT en tiempo de ejecución?" Básicamente sí!
Nota rápida, sin embargo, Java tiene más gastos generales que solo la compilación JIT. Por ejemplo, realiza muchas más comprobaciones por usted (que es cómo hace cosas como
ArrayIndexOutOfBoundsExceptions
yNullPointerExceptions
). El recolector de basura es otro gasto general significativo.Hay una comparación bastante detallada aquí .
fuente
Tenga en cuenta que lo siguiente solo compara la diferencia entre la compilación nativa y JIT, y no cubre los detalles de ningún lenguaje o marco en particular. Puede haber razones legítimas para elegir una plataforma en particular más allá de esto.
Cuando afirmamos que el código nativo es más rápido, estamos hablando del caso de uso típico del código compilado de forma nativa versus el código compilado JIT, donde el uso típico de una aplicación compilada JIT debe ser ejecutado por el usuario, con resultados inmediatos (por ejemplo, no esperando en el compilador primero). En ese caso, no creo que nadie pueda afirmar con franqueza, que el código compilado JIT puede igualar o superar el código nativo.
Supongamos que tenemos un programa escrito en algún lenguaje X, y podemos compilarlo con un compilador nativo, y nuevamente con un compilador JIT. Cada flujo de trabajo tiene las mismas etapas involucradas, que se pueden generalizar como (Código -> Representación intermedia -> Código de máquina -> Ejecución). La gran diferencia entre dos es qué etapas son vistas por el usuario y cuáles son vistas por el programador. Con la compilación nativa, el programador ve todo menos la etapa de ejecución, pero con la solución JIT, el usuario ve la compilación en código máquina, además de la ejecución.
La afirmación de que A es más rápido que B se refiere al tiempo que tarda el programa en ejecutarse, como lo ve el usuario . Si suponemos que ambos fragmentos de código funcionan de manera idéntica en la etapa de ejecución, debemos suponer que el flujo de trabajo JIT es más lento para el usuario, ya que también debe ver el tiempo T de la compilación al código de máquina, donde T> 0. Entonces , para cualquier posibilidad de que el flujo de trabajo JIT funcione igual que el flujo de trabajo nativo, para el usuario, debemos disminuir el tiempo de Ejecución del código, de modo que la Ejecución + Compilación al código de máquina, sea inferior a solo la etapa de Ejecución del flujo de trabajo nativo. Esto significa que debemos optimizar el código mejor en la compilación JIT que en la compilación nativa.
Sin embargo, esto es bastante inviable, ya que para realizar las optimizaciones necesarias para acelerar la ejecución, debemos pasar más tiempo en la etapa de compilación en código de máquina y, por lo tanto, cada vez que ahorramos como resultado del código optimizado se pierde realmente, ya que lo agregamos a la compilación. En otras palabras, la "lentitud" de una solución basada en JIT no se debe simplemente al tiempo adicional para la compilación JIT, sino que el código producido por esa compilación funciona más lentamente que una solución nativa.
Usaré un ejemplo: Asignación de registro. Dado que el acceso a la memoria es miles de veces más lento que el acceso al registro, idealmente queremos usar registros siempre que sea posible y tener la menor cantidad de accesos a la memoria que podamos, pero tenemos un número limitado de registros y debemos verter el estado en la memoria cuando lo necesitemos. un registro Si utilizamos un algoritmo de asignación de registros que requiere 200 ms para calcular, y como resultado ahorramos 2 ms de tiempo de ejecución, no estamos haciendo el mejor uso del tiempo para un compilador JIT. Las soluciones como el algoritmo de Chaitin, que puede producir código altamente optimizado, no son adecuadas.
La función del compilador JIT es lograr el mejor equilibrio entre el tiempo de compilación y la calidad del código producido, sin embargo, con un gran sesgo en el tiempo de compilación rápido, ya que no desea dejar al usuario esperando. El rendimiento del código que se ejecuta es más lento en el caso de JIT, ya que el compilador nativo no está limitado (mucho) por el tiempo en la optimización del código, por lo que es libre de usar los mejores algoritmos. La posibilidad de que la compilación general + la ejecución de un compilador JIT solo supere el tiempo de ejecución para el código compilado de forma nativa es efectivamente 0.
Pero nuestras máquinas virtuales no se limitan simplemente a la compilación JIT. Emplean técnicas de compilación anticipadas, almacenamiento en caché, intercambio en caliente y optimizaciones adaptativas. Así que modifiquemos nuestra afirmación de que el rendimiento es lo que ve el usuario, y limítelo al tiempo necesario para la ejecución del programa (supongamos que hemos compilado AOT). Efectivamente, podemos hacer que el código de ejecución sea equivalente al compilador nativo (¿o quizás mejor?). Un gran reclamo para las máquinas virtuales es que pueden producir código de mejor calidad que un compilador nativo, porque tiene acceso a más información, la del proceso en ejecución, como la frecuencia con la que se puede ejecutar una determinada función. Luego, la VM puede aplicar optimizaciones adaptativas al código más esencial a través del intercambio en caliente.
Sin embargo, hay un problema con este argumento: se supone que la optimización guiada por perfil y similares es algo exclusivo de las máquinas virtuales, lo que no es cierto. También podemos aplicarlo a la compilación nativa: compilando nuestra aplicación con el perfil habilitado, registrando la información y luego recompilando la aplicación con ese perfil. Probablemente también valga la pena señalar que el intercambio en caliente de código no es algo que solo un compilador JIT pueda hacer, podemos hacerlo para el código nativo, aunque las soluciones basadas en JIT para hacerlo están más disponibles y son mucho más fáciles para el desarrollador. Entonces, la gran pregunta es: ¿Puede una VM ofrecernos información que la compilación nativa no puede, lo que puede aumentar el rendimiento de nuestro código?
No puedo verlo yo mismo. También podemos aplicar la mayoría de las técnicas de una máquina virtual típica al código nativo, aunque el proceso es más complicado. Del mismo modo, podemos aplicar cualquier optimización de un compilador nativo a una VM que utiliza compilación AOT u optimizaciones adaptativas. La realidad es que la diferencia entre el código ejecutado de forma nativa y el que se ejecuta en una máquina virtual no es tan grande como se nos ha hecho creer. En última instancia, conducen al mismo resultado, pero adoptan un enfoque diferente para llegar allí. La VM utiliza un enfoque iterativo para producir código optimizado, donde el compilador nativo lo espera desde el principio (y se puede mejorar con un enfoque iterativo).
Un programador de C ++ podría argumentar que necesita las optimizaciones desde el principio, y no debería estar esperando a que una VM descubra cómo hacerlo, si es que lo hace. Sin embargo, este es probablemente un punto válido con nuestra tecnología actual, ya que el nivel actual de optimizaciones en nuestras máquinas virtuales es inferior a lo que pueden ofrecer los compiladores nativos, pero eso no siempre puede ser el caso si las soluciones AOT en nuestras máquinas virtuales mejoran, etc.
fuente
Este artículo es un resumen de un conjunto de publicaciones de blog que intentan comparar la velocidad de c ++ frente a c # y los problemas que debe superar en ambos idiomas para obtener un código de alto rendimiento. El resumen es "su biblioteca importa mucho más que nada, pero si está en c ++ puede superar eso". o 'los idiomas modernos tienen mejores bibliotecas y, por lo tanto, obtienen resultados más rápidos con un menor esfuerzo' dependiendo de su inclinación filosófica.
fuente
Creo que la verdadera pregunta aquí no es "¿cuál es más rápido?" pero "¿cuál tiene el mejor potencial para un mayor rendimiento?". Visto en esos términos, C ++ claramente gana: está compilado en código nativo, no hay JITting, es un nivel más bajo de abstracción, etc.
Eso está lejos de la historia completa.
Debido a que C ++ está compilado, cualquier optimización del compilador debe hacerse en tiempo de compilación, y las optimizaciones del compilador que son apropiadas para una máquina pueden ser completamente incorrectas para otra. También es el caso de que cualquier optimización de compilador global puede y favorecerá ciertos algoritmos o patrones de código sobre otros.
Por otro lado, un programa JITted se optimizará en el momento JIT, por lo que puede hacer algunos trucos que un programa precompilado no puede y puede hacer optimizaciones muy específicas para la máquina en la que realmente se está ejecutando y el código que realmente se está ejecutando. Una vez que supera la sobrecarga inicial del JIT, en algunos casos tiene el potencial de ser más rápido.
En ambos casos, una implementación sensata del algoritmo y otras instancias de que el programador no sea estúpido probablemente serán factores mucho más significativos, sin embargo, por ejemplo, es perfectamente posible escribir un código de cadena completamente cerebral en C ++ que incluso se anulará Un lenguaje de script interpretado.
fuente
-march=native
). - "es un nivel inferior de abstracción" no es realmente cierto. C ++ usa abstracciones de tan alto nivel como Java (o, de hecho, las más altas: ¿programación funcional? ¿Metaprogramación de plantilla?), Solo implementa las abstracciones de manera menos "limpia" que Java.La compilación JIT en realidad tiene un impacto negativo en el rendimiento. Si diseña un compilador "perfecto" y un compilador JIT "perfecto", la primera opción siempre ganará en rendimiento.
Tanto Java como C # se interpretan en lenguajes intermedios y luego se compilan en código nativo en tiempo de ejecución, lo que reduce el rendimiento.
Pero ahora la diferencia no es tan obvia para C #: Microsoft CLR produce diferentes códigos nativos para diferentes CPU, lo que hace que el código sea más eficiente para la máquina en la que se ejecuta, lo que no siempre hacen los compiladores de C ++.
PS C # está escrito de manera muy eficiente y no tiene muchas capas de abstracción. Esto no es cierto para Java, que no es tan eficiente. Entonces, en este caso, con su gran CLR, los programas C # a menudo muestran un mejor rendimiento que los programas C ++. Para más información sobre .Net y CLR, eche un vistazo a "CLR vía C #" de Jeffrey Richter .
fuente