¿Qué respalda la afirmación de que C ++ puede ser más rápido que un JVM o CLR con JIT? [cerrado]

119

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.

Anónimo
fuente
El rendimiento también depende de la complejidad del programa.
pandu
23
Añadiría a "C ++ es cada vez más eficiente si sabes lo que estás haciendo y por qué hacer las cosas de cierta manera merecerá un aumento en el rendimiento". Al decir que no es solo una cuestión de conocimiento, es una cuestión de tiempo del desarrollador. No siempre es eficiente maximizar la optimización. Esta es la razón por la que existen lenguajes de nivel superior como Java y Python (entre otras razones), para disminuir la cantidad de tiempo que un programador tiene que dedicar a la programación para realizar una tarea determinada a expensas de la optimización altamente ajustada.
Joel Cornett
44
@ Joel Cornett: estoy totalmente de acuerdo. Definitivamente soy más productivo en Java que en C ++ y solo considero C ++ cuando necesito escribir código realmente rápido. Por otro lado, he visto que el código de C ++ mal escrito es muy lento: C ++ es menos útil en manos de programadores no calificados.
Giorgio el
3
Cualquier salida de compilación que puede ser producida por un JIT puede ser producida por C ++, pero el código que C ++ puede producir no necesariamente puede ser producido por un JIT. Por lo tanto, las capacidades y características de rendimiento de C ++ son un superconjunto de las de cualquier lenguaje de nivel superior. QED
tylerl
1
@Doval Técnicamente cierto, pero por regla general puede contar los posibles factores de tiempo de ejecución que afectan el rendimiento de un programa por un lado. Por lo general, sin usar más de dos dedos. Entonces, en el peor de los casos, envía múltiples archivos binarios ... excepto que resulta que ni siquiera necesita hacerlo porque la aceleración potencial es insignificante, por lo que nadie se molesta.
tylerl

Respuestas:

200

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é:

  • Hay una sobrecarga de memoria de al menos 8 bytes para cada objeto, y el uso de objetos en lugar de primitivos se requiere o se prefiere en muchos lugares (es decir, las colecciones estándar).
  • Las cadenas constan de dos objetos y tienen una sobrecarga de 38 bytes.
  • UTF-16 se usa internamente, lo que significa que cada carácter ASCII requiere dos bytes en lugar de uno (Oracle JVM introdujo recientemente una optimización para evitar esto para cadenas ASCII puras).
  • No hay un tipo de referencia agregado (es decir, estructuras) y, a su vez, no hay matrices de tipos de referencia agregados. Un objeto Java, o matriz de objetos Java, tiene una localidad de caché L1 / L2 muy pobre en comparación con las estructuras C y las matrices.
  • Los genéricos de Java usan el borrado de tipo, que tiene una localidad de caché deficiente en comparación con la instanciación de tipo.
  • La asignación de objetos es opaca y debe hacerse por separado para cada objeto, por lo que es imposible que una aplicación diseñe deliberadamente sus datos de una manera amigable con la caché y aún así los trate como datos estructurados.

Algunos otros factores relacionados con la memoria pero no con la memoria caché:

  • No hay asignación de pila, por lo que todos los datos no primitivos con los que trabaja tienen que estar en el montón y pasar por la recolección de basura (algunos JIT recientes hacen la asignación de pila detrás de escena en ciertos casos).
  • Debido a que no hay tipos de referencia agregados, no se apilan los tipos de referencia agregados. (Piense en pasar eficientemente los argumentos de Vector)
  • La recolección de basura puede dañar el contenido de la memoria caché L1 / L2, y las pausas de detención del mundo de GC perjudican la interactividad.
  • La conversión entre tipos de datos siempre requiere copia; no puede llevar un puntero a un conjunto de bytes que obtuvo de un socket e interpretarlos como flotantes.

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.

Michael Borgwardt
fuente
37
+1 - en general, esta es una buena respuesta. Sin embargo, no estoy seguro de que la viñeta "no hay asignación de pila" sea completamente precisa. Los JIT de Java a menudo hacen análisis de escape para permitir la asignación de la pila cuando sea posible; tal vez lo que debería decir es que el lenguaje Java no permite que el programador decida cuándo un objeto se asigna en pila frente a en pila. Además, si un recolector de basura generacional (que utilizan todas las JVM modernas) está en uso, "asignación de montón" significa algo completamente diferente (con características de rendimiento completamente diferentes) que en un entorno C ++.
Daniel Pryden
55
Creo que hay otras dos cosas, pero en su mayoría trabajo con cosas a un nivel mucho más alto, así que dime si me equivoco. Realmente no puede escribir C ++ sin desarrollar un conocimiento más general de lo que realmente está sucediendo en la memoria y cómo funciona realmente el código de máquina, mientras que los scripts o los lenguajes de máquina virtual abstraen todas esas cosas de su atención. También tiene un control mucho más detallado sobre cómo funcionan las cosas, mientras que en una VM o lenguaje interpretado depende de lo que los autores de la biblioteca central pueden haber optimizado para un escenario demasiado específico.
Erik Reppen
18
+1. Una cosa más que agregaría (pero no estoy dispuesto a enviar una nueva respuesta): la indexación de matrices en Java siempre implica la verificación de límites. Con C y C ++, este no es el caso.
Riwalk
77
Vale la pena señalar que la asignación de almacenamiento dinámico de Java es significativamente más rápida que una versión ingenua con C ++ (debido a la agrupación interna y otras cosas), pero la asignación de memoria en C ++ puede ser significativamente mejor si sabe lo que está haciendo.
Brendan Long
10
@BrendanLong, cierto ... pero solo si la memoria está limpia: una vez que una aplicación se esté ejecutando durante un tiempo, la asignación de memoria será más lenta debido a la necesidad de GC que ralentiza las cosas drásticamente, ya que tiene que liberar memoria, ejecutar finalizadores y luego compacto. Es una compensación que beneficia a los puntos de referencia, pero (IMHO) en general ralentiza las aplicaciones.
gbjbaanb
67

¿Es simplemente porque C ++ está compilado 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?

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 newy deleteen 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.

Charles Salvia
fuente
66
Buena respuesta, pero un punto menor: "Las plantillas de C ++ te dan ambas (a costa de tiempos de compilación más largos)". También agregaría a costa de un programa de mayor tamaño. No siempre puede ser un problema, pero si se desarrolla para dispositivos móviles, definitivamente puede serlo.
Leo
99
@luiscubal: no, a este respecto, los genéricos de C # son muy parecidos a Java (en el sentido de que se toma la misma ruta de código "genérico" sin importar qué tipos se pasen). El truco para las plantillas de C ++ es que el código se instancia una vez para cada tipo al que se aplica. Por 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 un List.
jalf
12
@jalf C # List<int>usa un int[], no un Object[]Java como lo hace. Ver stackoverflow.com/questions/116988/…
luiscubal
55
@luiscubal: su terminología no está clara. El JIT no actúa en lo que yo considero "tiempo de compilación". Tienes razón, por supuesto, dado un compilador JIT lo suficientemente inteligente y agresivo, efectivamente no hay límites para lo que podría hacer. Pero C ++ requiere este comportamiento. Además, las plantillas de C ++ permiten al programador especificar especializaciones explícitas, permitiendo optimizaciones explícitas adicionales donde corresponda. C # no tiene equivalente para eso. Por ejemplo, en C ++, podría definir un lugar vector<N>donde, para el caso específico de vector<4>, debería usarse mi implementación SIMD codificada a mano
jalf
55
@Leo: la expansión de código a través de plantillas era un problema hace 15 años. Con una fuerte plantilla e inlínea, además de los compiladores de habilidades recogidos desde entonces (como doblar instancias idénticas), hoy en día mucho código se hace más pequeño a través de plantillas.
sbi
46

Lo 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 virtualcuando 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 inlinehacerlo 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.

sbi
fuente
44
No pagas por lo que no usas. => y luego agregaron RTTI :(
Matthieu M.
11
@ Matthieu: Si bien entiendo su sentimiento, no puedo evitar notar que incluso eso se ha agregado con cuidado con respecto al rendimiento. RTTI se especifica para que pueda implementarse usando tablas virtuales y, por lo tanto, agrega muy poca sobrecarga si no lo usa. Si no usa polimorfismo, no hay costo alguno. ¿Me estoy perdiendo de algo?
sbi
99
@ Mathieu: Por supuesto, hay una razón. ¿Pero es esta razón racional? Por lo que puedo ver, el "costo de RTTI", si no se usa, es un puntero adicional en cada tabla virtual de clase polimórfica, apuntando a algún objeto RTTI asignado estáticamente en algún lugar. A menos que desee programar el chip en mi tostadora, ¿cómo podría ser relevante?
sbi
44
@Aaronaught: No sé qué responder a eso. ¿Realmente rechazaste mi respuesta porque señala la filosofía subyacente que hizo que Stroustrup et al agreguen funciones de una manera que permita el rendimiento, en lugar de enumerar estas formas y características individualmente?
sbi
99
@Aaronaught: Tienes mi simpatía.
sbi
29

¿Conoces el trabajo de investigación de Google sobre ese tema?

De la conclusión:

Encontramos que en lo que respecta al rendimiento, C ++ gana por un amplio margen. Sin embargo, también requirió los esfuerzos de ajuste más extensos, muchos de los cuales se realizaron a un nivel de sofisticación que no estaría disponible para el programador promedio.

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".

Doc Brown
fuente
44
Además de las diferencias de uso de memoria y caché, una de las más importantes es la cantidad de optimización realizada. Compare cuántas optimizaciones hace GCC / LLVM (y probablemente Visual C ++ / ICC) en relación con el compilador Java HotSpot: mucho más, especialmente con respecto a los bucles, la eliminación de ramas redundantes y la asignación de registros. Los compiladores JIT generalmente no tienen el tiempo para estas optimizaciones agresivas, incluso si pensaran que podrían implementarlas mejor utilizando la información de tiempo de ejecución disponible.
Gratian Lup
2
@GratianLup: Me pregunto si eso es (todavía) cierto con LTO.
Deduplicador
2
@GratianLup: No olvidemos la optimización guiada por perfil para C ++ ...
Deduplicator
23

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:

Fundamentalmente, la semántica de Java dicta que es un lenguaje más lento que C ++.

Entonces, dependiendo de con qué otro idioma compare C ++, puede obtener o no la misma respuesta.

En C ++ tienes:

  • Capacidad para hacer líneas inteligentes,
  • generación de código genérico que tiene una fuerte localidad (plantillas)
  • datos tan pequeños y compactos como sea posible
  • oportunidades para evitar indirecciones
  • comportamiento predecible de la memoria
  • Las optimizaciones del compilador solo son posibles debido al uso de abstracciones de alto nivel (plantillas)

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:

  • use indirección masivamente (lenguajes "todo es una referencia / puntero administrado"): indirección significa que la CPU tiene que saltar en la memoria para obtener los datos necesarios, lo que aumenta las fallas de la memoria caché de la CPU, lo que significa ralentizar el procesamiento - C también usa indirectas a mucho incluso si puede tener datos pequeños como C ++;
  • genera objetos de gran tamaño a los que se accede indirectamente a los miembros: esto es una consecuencia de tener referencias por defecto, los miembros son punteros, por lo que cuando obtiene un miembro, es posible que no obtenga datos cerca del núcleo del objeto principal, lo que nuevamente desencadena errores de caché.
  • use un colector de garbarge: solo hace imposible la previsibilidad del rendimiento (por diseño).

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.

Klaim
fuente
7

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 ++.

gbjbaanb
fuente
2
Muchas cosas que escribes no son ciertas para los recolectores de basura generacionales modernos.
Michael Borgwardt
3
@MichaelBorgwardt como? Digo "el GC funciona regularmente" y "compacta el montón". El resto de mi respuesta se refiere a cómo las estructuras de datos de la aplicación usan la memoria.
gbjbaanb
6

"¿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 ArrayIndexOutOfBoundsExceptionsy NullPointerExceptions). El recolector de basura es otro gasto general significativo.

Hay una comparación bastante detallada aquí .

vaughandroid
fuente
2

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.

Mark H
fuente
0

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.

Jeff Gates
fuente
0

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.

Maximus Minimus
fuente
3
"Las optimizaciones del compilador que son apropiadas para una máquina pueden estar completamente equivocadas para otra" Bueno, eso no es realmente culpable del lenguaje. El código verdaderamente crítico para el rendimiento se puede compilar por separado para cada máquina en la que se ejecutará, lo que es obvio si compila localmente desde la 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.
izquierda alrededor del
"El código verdaderamente crítico para el rendimiento puede compilarse por separado para cada máquina en la que se ejecutará, lo cual es obvio si compila localmente desde la fuente"; esto falla debido a la suposición subyacente de que el usuario final también es un programador.
Maximus Minimus
No necesariamente el usuario final, solo la persona responsable de instalar el programa. En el escritorio y los dispositivos móviles, ese es típicamente el usuario final, pero estas no son las únicas aplicaciones que existen, ciertamente no son las más críticas para el rendimiento. Y realmente no necesita ser un programador para compilar un programa desde la fuente, si tiene escrituras de compilación escritas correctamente, como lo hacen todos los buenos proyectos de software libre / abierto.
Leftaroundabout
1
Si bien en teoría sí, un JIT puede hacer más trucos que un compilador estático, en la práctica (para .NET al menos, no conozco Java también), en realidad no hace nada de esto. He hecho un montón de desmontaje de código .NET JIT recientemente, y hay todo tipo de optimizaciones como levantar código de bucles, eliminación de código muerto, etc., que .NET JIT simplemente no hace. Desearía que lo hiciera, pero bueno, el equipo de Windows dentro de Microsoft ha estado tratando de matar a .NET durante años, por lo que no estoy conteniendo la respiración
Orion Edwards
-1

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 .

superM
fuente
8
Si JIT realmente tuvo un impacto negativo en el rendimiento, ¿seguramente no se usaría?
Zavior
2
@Zavior: no puedo pensar en una buena respuesta a su pregunta, pero no veo cómo JIT no puede agregar una sobrecarga de rendimiento adicional: el JIT es un proceso adicional que se debe completar en tiempo de ejecución que requiere recursos que no son t gastado en la ejecución del programa en sí, mientras que un lenguaje totalmente compilado está "listo para funcionar".
Anónimo el
3
JIT tiene un efecto positivo en el rendimiento, no negativo, si lo pone en contexto: está compilando el código de bytes en el código de la máquina antes de ejecutarlo. Los resultados también se pueden almacenar en caché, lo que le permite ejecutarse más rápido que el código de bytes equivalente que se interpreta.
Casey Kuball
3
JIT (o más bien, el enfoque de código de bytes) no se usa para el rendimiento, sino por conveniencia. En lugar de pre-construir binarios para cada plataforma (o un subconjunto común, que es subóptimo para cada uno de ellos), compila solo hasta la mitad y deja que el compilador JIT haga el resto. 'Escribir una vez, desplegar en cualquier lugar' es por eso que se hace de esta manera. La comodidad se puede tener con sólo un intérprete de código de bytes, pero JIT hace que sea más rápido que el intérprete cruda (aunque no necesariamente lo suficientemente rápido como para vencer a una solución pre-compilados; compilación JIT hace tomar tiempo, y el resultado no siempre se conforman para ello).
tdammers
44
@Tdammmers, en realidad también hay un componente de rendimiento. Consulte java.sun.com/products/hotspot/whitepaper.html . Las optimizaciones pueden incluir cosas como ajustes dinámicos para mejorar la predicción de ramificaciones y los éxitos de caché, la alineación dinámica, la des-virtualización, la desactivación de la comprobación de límites y el desenrollado de bucles. La afirmación es que en muchos casos estos pueden más que pagar el costo de JIT.
Charles E. Grant