Traté de comparar el rendimiento del lenguaje ensamblador en línea y el código C ++, así que escribí una función que agrega dos matrices de tamaño 2000 por 100000 veces. Aquí está el código:
#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
for(int i = 0; i < TIMES; i++)
{
for(int j = 0; j < length; j++)
x[j] += y[j];
}
}
void calcuAsm(int *x,int *y,int lengthOfArray)
{
__asm
{
mov edi,TIMES
start:
mov esi,0
mov ecx,lengthOfArray
label:
mov edx,x
push edx
mov eax,DWORD PTR [edx + esi*4]
mov edx,y
mov ebx,DWORD PTR [edx + esi*4]
add eax,ebx
pop edx
mov [edx + esi*4],eax
inc esi
loop label
dec edi
cmp edi,0
jnz start
};
}
Aquí está main()
:
int main() {
bool errorOccured = false;
setbuf(stdout,NULL);
int *xC,*xAsm,*yC,*yAsm;
xC = new int[2000];
xAsm = new int[2000];
yC = new int[2000];
yAsm = new int[2000];
for(int i = 0; i < 2000; i++)
{
xC[i] = 0;
xAsm[i] = 0;
yC[i] = i;
yAsm[i] = i;
}
time_t start = clock();
calcuC(xC,yC,2000);
// calcuAsm(xAsm,yAsm,2000);
// for(int i = 0; i < 2000; i++)
// {
// if(xC[i] != xAsm[i])
// {
// cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
// errorOccured = true;
// break;
// }
// }
// if(errorOccured)
// cout<<"Error occurs!"<<endl;
// else
// cout<<"Works fine!"<<endl;
time_t end = clock();
// cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";
cout<<"time = "<<end - start<<endl;
return 0;
}
Luego ejecuto el programa cinco veces para obtener los ciclos del procesador, que podrían verse como el tiempo. Cada vez que llamo a una de las funciones mencionadas anteriormente solamente.
Y aquí viene el resultado.
Función de la versión de montaje:
Debug Release
---------------
732 668
733 680
659 672
667 675
684 694
Average: 677
Función de la versión C ++:
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
El código C ++ en modo de lanzamiento es casi 3,7 veces más rápido que el código ensamblador. ¿Por qué?
Supongo que el código de ensamblaje que escribí no es tan efectivo como los generados por GCC. Es difícil para un programador común como yo escribir código más rápido que su oponente generado por un compilador. ¿Eso significa que no debería confiar en el rendimiento del lenguaje ensamblador escrito por mis manos, centrarme en C ++ y olvidarme del lenguaje ensamblador?
fuente
Respuestas:
Si, la mayoría de las veces.
En primer lugar, comienza con la suposición errónea de que un lenguaje de bajo nivel (ensamblado en este caso) siempre producirá un código más rápido que un lenguaje de alto nivel (C ++ y C en este caso). No es verdad. ¿El código C es siempre más rápido que el código Java? No porque hay otra variable: programador. La forma en que escribe el código y el conocimiento de los detalles de la arquitectura influyen enormemente en el rendimiento (como vio en este caso).
Puede siempre producirá un ejemplo en el código de montaje hecho a mano es mejor que el código compilado, pero por lo general es un ejemplo ficticio o una única rutina no es un verdadero programa de 500.000+ líneas de código C ++). Creo que los compiladores producirán un mejor código de ensamblaje 95% de veces y , a veces, solo algunas veces, es posible que deba escribir código de ensamblaje para pocas, cortas, altamente utilizadas , rutinas críticas de rendimiento o cuando tenga que acceder a las funciones de su lenguaje de alto nivel favorito No expone. ¿Quieres un toque de esta complejidad? Lea esta increíble respuesta aquí en SO.
¿Por qué esto?
En primer lugar, porque los compiladores pueden hacer optimizaciones que ni siquiera podemos imaginar (vea esta breve lista ) y las harán en segundos (cuando necesitemos días ).
Cuando codifica en ensamblado, debe realizar funciones bien definidas con una interfaz de llamada bien definida. Sin embargo, pueden tener en cuenta la optimización de todo el programa y la optimización entre procedimientos , como la asignación de registros , la propagación constante , la eliminación de subexpresiones comunes , la programación de instrucciones y otras optimizaciones complejas y no obvias ( modelo de Polytope , por ejemplo). En la arquitectura RISC , los muchachos dejaron de preocuparse por esto hace muchos años (la programación de instrucciones, por ejemplo, es muy difícil de ajustar a mano ) y las CPU CISC modernas tienen tuberías muy largas también.
Para algunos microcontroladores complejos, incluso las bibliotecas del sistema se escriben en C en lugar de ensamblar porque sus compiladores producen un código final mejor (y fácil de mantener).
Los compiladores a veces pueden usar automáticamente algunas instrucciones MMX / SIMDx por sí mismas, y si no las usa, simplemente no puede comparar (otras respuestas ya revisaron muy bien su código de ensamblaje). Solo para bucles, esta es una breve lista de optimizaciones de bucle de lo que comúnmente comprueba un compilador (¿cree que podría hacerlo usted mismo cuando se haya decidido su programación para un programa C #?) Si escribe algo en conjunto, I cree que debe considerar al menos algunas optimizaciones simples . El ejemplo de libro escolar para matrices es desenrollar el ciclo (su tamaño se conoce en tiempo de compilación). Hazlo y ejecuta tu prueba nuevamente.
En estos días también es muy poco frecuente que necesite usar lenguaje ensamblador por otra razón: la gran cantidad de CPU diferentes . ¿Quieres apoyarlos a todos? Cada uno tiene una microarquitectura específica y algunos conjuntos de instrucciones específicas . Tienen un número diferente de unidades funcionales y las instrucciones de montaje deben organizarse para mantenerlos a todos ocupados . Si escribe en C, puede usar PGO, pero en el ensamblaje necesitará un gran conocimiento de esa arquitectura específica (y repensar y rehacer todo para otra arquitectura ). Para tareas pequeñas, el compilador generalmente lo hace mejor, y para tareas complejas, generalmente el trabajo no se paga (ycompilador puede hacerlo mejor de todos modos).
Si te sientas y lees tu código, probablemente verás que ganarás más para rediseñar tu algoritmo que para traducirlo al ensamblaje (lee esta gran publicación aquí en SO ), hay optimizaciones de alto nivel (y sugerencias para el compilador) que puede aplicar de manera efectiva antes de que necesite recurrir al lenguaje ensamblador. Probablemente valga la pena mencionar que, a menudo, utilizando intrínsecos, obtendrá el aumento de rendimiento que está buscando y el compilador aún podrá realizar la mayoría de sus optimizaciones.
Dicho todo esto, incluso cuando puede producir un código de ensamblaje 5 a 10 veces más rápido, debe preguntar a sus clientes si prefieren pagar una semana de su tiempo o comprar una CPU 50 $ más rápida . La mayoría de nosotros simplemente no necesita una optimización extrema (y especialmente en aplicaciones LOB).
fuente
Su código de ensamblaje es subóptimo y puede mejorarse:
loop
instrucción, que se sabe que es muy lenta en la mayoría de las CPU modernas (posiblemente como resultado de usar un antiguo libro de ensamblaje *)Entonces, a menos que mejore enormemente su conjunto de habilidades con respecto al ensamblador, no tiene sentido que escriba código de ensamblador para el rendimiento.
* Por supuesto, no sé si realmente recibió las
loop
instrucciones de un antiguo libro de ensamblaje. Pero casi nunca lo ves en el código del mundo real, ya que todos los compiladores son lo suficientemente inteligentes como para no emitirloop
, solo lo ves en los libros malos y obsoletos de mi humilde opinión.fuente
loop
(y muchas instrucciones "obsoletas") si optimiza el tamañoIncluso antes de profundizar en el ensamblaje, hay transformaciones de código que existen en un nivel superior.
se puede transformar a través de Loop Rotation :
que es mucho mejor en lo que respecta a la localidad de memoria.
Esto podría optimizarse aún más, hacer
a += b
X veces es equivalente a hacerlo,a += X * b
por lo que obtenemos:Sin embargo, parece que mi optimizador favorito (LLVM) no realiza esta transformación.
[editar] Descubrí que la transformación se realiza si teníamos el
restrict
calificador parax
yy
. De hecho, sin esta restricción,x[j]
yy[j]
podría alias a la misma ubicación que hace que esta transformación sea errónea. [final de edición]De todos modos, esta es, creo, la versión C optimizada. Ya es mucho más simple. En base a esto, aquí está mi crack en ASM (dejo que Clang lo genere, soy inútil):
Me temo que no entiendo de dónde provienen todas esas instrucciones, sin embargo, siempre puede divertirse e intentar ver cómo se compara ... pero todavía usaría la versión C optimizada en lugar de la de ensamblaje, en código, Mucho más portátil.
fuente
x
yy
. Es decir, el compilador no puede estar seguro de que para todosi,j
en la[0, length)
que tienex + i != y + j
. Si hay superposición, entonces la optimización es imposible. El lenguaje C introdujo larestrict
palabra clave para decirle al compilador que dos punteros no pueden tener alias, sin embargo, no funciona para las matrices porque aún pueden superponerse incluso si no tienen exactamente un alias.__restrict
). SSE2 es la línea de base para x86-64, y con la combinación aleatoria, SSE2 puede hacer 2x multiplicaciones de 32 bits a la vez (produciendo productos de 64 bits, de ahí la combinación para volver a unir los resultados). godbolt.org/z/r7F_uo . (Se necesita SSE4.1 parapmulld
: 32x32 empaquetado => multiplicación de 32 bits). GCC tiene un buen truco para convertir multiplicadores enteros constantes en shift / add (y / o restar), lo cual es bueno para multiplicadores con pocos bits establecidos. El código aleatorio de Clang va a obstaculizar el rendimiento aleatorio en las CPU de Intel.Respuesta corta: sí.
Respuesta larga: sí, a menos que realmente sepa lo que está haciendo y tenga una razón para hacerlo.
fuente
He arreglado mi código asm:
Resultados para la versión de lanzamiento:
El código de ensamblaje en modo de lanzamiento es casi 2 veces más rápido que el C ++.
fuente
xmm0
lugar demm0
), obtendrá otra aceleración por un factor de dos ;-)paddd xmm
(después de verificar la superposición entrex
yy
, porque no lo usóint *__restrict x
). Por ejemplo, gcc hace eso: godbolt.org/z/c2JG0- . O después de ingresarmain
, no debería ser necesario verificar la superposición porque puede ver la asignación y demostrar que no se superponen. (Y supondría una alineación de 16 bytes en algunas implementaciones x86-64, lo que no es el caso para la definición independiente). Y si compilagcc -O3 -march=native
, puede obtener 256 bits o 512 bits vectorizaciónSí, eso es exactamente lo que significa, y es cierto para todos los idiomas. Si no sabe cómo escribir código eficiente en el lenguaje X, entonces no debe confiar en su capacidad para escribir código eficiente en X. Por lo tanto, si desea un código eficiente, debe usar otro idioma.
El ensamblaje es particularmente sensible a esto, porque, bueno, lo que ves es lo que obtienes. Escribe las instrucciones específicas que desea que ejecute la CPU. Con lenguajes de alto nivel, hay un compilador entre ellos, que puede transformar su código y eliminar muchas ineficiencias. Con el montaje, estás solo.
fuente
La única razón para usar el lenguaje ensamblador hoy en día es usar algunas funciones a las que el lenguaje no tiene acceso.
Esto aplica a:
Pero los compiladores actuales son bastante inteligentes, incluso pueden reemplazar dos declaraciones separadas, como
d = a / b; r = a % b;
con una sola instrucción que calcula la división y el resto de una vez si está disponible, incluso si C no tiene dicho operador.fuente
Es cierto que un compilador moderno hace un trabajo increíble en la optimización del código, pero aún así lo alentaría a que siga aprendiendo ensamblaje.
En primer lugar, claramente no está intimidado por eso , eso es una gran ventaja, a continuación: está en el camino correcto al realizar un perfil para validar o descartar sus suposiciones de velocidad , está solicitando la opinión de personas experimentadas y usted tener la mejor herramienta de optimización conocida por la humanidad: un cerebro .
A medida que aumente su experiencia, aprenderá cuándo y dónde usarlo (por lo general, los bucles más íntimos y ajustados de su código, después de haber optimizado profundamente a nivel algorítmico).
Para inspirarte, te recomendaría que busques los artículos de Michael Abrash (si no has tenido noticias suyas, él es un gurú de la optimización; ¡incluso colaboró con John Carmack en la optimización del procesador de software Quake!)
fuente
He cambiado el código asm:
Resultados para la versión de lanzamiento:
El código de ensamblaje en modo de lanzamiento es casi 4 veces más rápido que el C ++. IMHo, la velocidad del código de ensamblaje depende del programador
fuente
shr ecx,2
es superfluo, porque la longitud de la matriz ya está dadaint
y no en byte. Entonces básicamente logras la misma velocidad. Puede probar lapaddd
respuesta de Harolds, esto realmente será más rápido.¡Es un tema muy interesante!
He cambiado el MMX por SSE en el código de Sasha.
Aquí están mis resultados:
El código de ensamblaje con SSE es 5 veces más rápido que el C ++
fuente
La mayoría de los compiladores de idiomas de alto nivel están muy optimizados y saben lo que están haciendo. Puede intentar volcar el código de desmontaje y compararlo con su ensamblaje nativo. Creo que verá algunos buenos trucos que está utilizando su compilador.
Solo por ejemplo, incluso si ya no estoy seguro de que sea correcto :):
Haciendo:
cuestan más ciclos que
que hace lo mismo
El compilador conoce todos estos trucos y los usa.
fuente
El compilador te ganó. Lo intentaré, pero no haré ninguna garantía. Voy a suponer que la "multiplicación" de veces que se pretende que sea una prueba de rendimiento más relevante, que
y
yx
están alineados-16, y quelength
es un múltiplo no cero de 4. Eso es probablemente todo es verdad de todos modos.Como dije, no hago garantías. Pero me sorprendería si se puede hacer mucho más rápido: el cuello de botella aquí es el rendimiento de la memoria, incluso si todo es un golpe L1.
fuente
mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax
y luego usa [esi + ecx] en todas partes, evitará un bloqueo de ciclo por instrucción que acelere los lotes de bucle. (Si tiene la última versión de Skylake, esto no se aplica). Add reg, reg simplemente hace que el bucle sea más estricto, lo que puede o no ayudar.Simplemente implementar ciegamente el mismo algoritmo, instrucción por instrucción, en el ensamblaje se garantiza que será más lento de lo que el compilador puede hacer.
Esto se debe a que incluso la optimización más pequeña que realiza el compilador es mejor que su código rígido sin ninguna optimización.
Por supuesto, es posible superar el compilador, especialmente si es una parte pequeña y localizada del código, incluso tuve que hacerlo yo mismo para obtener un aprox. Se acelera 4 veces, pero en este caso tenemos que confiar en gran medida en el buen conocimiento del hardware y en numerosos trucos aparentemente contraintuitivos.
fuente
Como compilador, reemplazaría un bucle con un tamaño fijo para muchas tareas de ejecución.
Producirá
y eventualmente sabrá que "a = a + 0;" es inútil, por lo que eliminará esta línea. Esperemos que haya algo en su cabeza dispuesto a adjuntar algunas opciones de optimización como comentario. Todas esas optimizaciones muy efectivas harán que el lenguaje compilado sea más rápido.
fuente
a
sea volátil, hay una buena posibilidad de que el compilador lo hagaint a = 13;
desde el principio.Es exactamente lo que significa. Deje las microoptimizaciones al compilador.
fuente
Me encanta este ejemplo porque demuestra una importante lección sobre el código de bajo nivel. Sí, puede escribir un ensamblaje que sea tan rápido como su código C. Esto es tautológicamente cierto, pero no necesariamente significa nada. Claramente, alguien puede, de lo contrario el ensamblador no conocería las optimizaciones apropiadas.
Del mismo modo, se aplica el mismo principio a medida que asciende en la jerarquía de la abstracción del lenguaje. Sí, puede escribir un analizador en C que sea tan rápido como un script perl rápido y sucio, y mucha gente lo hace. Pero eso no significa que debido a que usaste C, tu código será rápido. En muchos casos, los lenguajes de nivel superior realizan optimizaciones que quizás nunca haya considerado.
fuente
En muchos casos, la forma óptima de realizar alguna tarea puede depender del contexto en el que se realiza la tarea. Si una rutina está escrita en lenguaje ensamblador, generalmente no será posible variar la secuencia de instrucciones según el contexto. Como un ejemplo simple, considere el siguiente método simple:
Un compilador para código ARM de 32 bits, dado lo anterior, probablemente lo representaría de la siguiente manera:
o quizás
Eso podría optimizarse ligeramente en código ensamblado a mano, ya sea:
o
Ambos enfoques ensamblados a mano requerirían 12 bytes de espacio de código en lugar de 16; este último reemplazaría una "carga" con un "complemento", que en un ARM7-TDMI se ejecutaría dos ciclos más rápido. Si el código se ejecutara en un contexto en el que r0 era no sabe / no importa, las versiones en lenguaje ensamblador serían algo mejores que la versión compilada. Por otro lado, suponga que el compilador sabía que algún registro [por ejemplo, r5] iba a tener un valor que estaba dentro de 2047 bytes de la dirección deseada 0x40001204 [por ejemplo, 0x40001000], y además sabía que algún otro registro [por ejemplo, r7] iba para mantener un valor cuyos bits bajos eran 0xFF. En ese caso, un compilador podría optimizar la versión C del código para simplemente:
Mucho más corto y más rápido que incluso el código de ensamblaje optimizado a mano. Además, supongamos que set_port_high ocurrió en el contexto:
Nada inverosímil cuando se codifica para un sistema embebido. Si
set_port_high
está escrito en el código de ensamblaje, el compilador tendría que mover r0 (que contiene el valor de retornofunction1
) a otro lugar antes de invocar el código de ensamblaje, y luego mover ese valor nuevamente a r0 después (yafunction2
que esperará su primer parámetro en r0), entonces el código de ensamblaje "optimizado" necesitaría cinco instrucciones. Incluso si el compilador no supiera de ningún registro que contenga la dirección o el valor para almacenar, su versión de cuatro instrucciones (que podría adaptar para usar cualquier registro disponible, no necesariamente r0 y r1) superaría al ensamblado "optimizado" versión en lenguaje. Si el compilador tuviera la dirección y los datos necesarios en r5 y r7 como se describió anteriormente,function1
no alteraría esos registros y, por lo tanto, podría reemplazarset_port_high
con una solastrb
instrucción: cuatro instrucciones más pequeñas y más rápidas que el código de ensamblaje "optimizado a mano".Tenga en cuenta que el código de ensamblaje optimizado a mano a menudo puede superar a un compilador en los casos en que el programador conoce el flujo preciso del programa, pero los compiladores brillan en los casos en que se escribe un fragmento de código antes de que se conozca su contexto, o donde se puede encontrar un fragmento de código fuente invocado desde múltiples contextos [si
set_port_high
se usa en cincuenta lugares diferentes en el código, el compilador podría decidir independientemente para cada uno de ellos cuál es la mejor manera de expandirlo].En general, sugeriría que el lenguaje ensamblador es apto para producir las mayores mejoras de rendimiento en aquellos casos en los que cada fragmento de código puede abordarse desde un número muy limitado de contextos, y es perjudicial para el rendimiento en lugares donde un fragmento de código El código puede ser abordado desde muchos contextos diferentes. Curiosamente (y convenientemente) los casos en que el ensamblaje es más beneficioso para el rendimiento son a menudo aquellos en los que el código es más sencillo y fácil de leer. Los lugares donde el código del lenguaje ensamblador se convertiría en un desastre pegajoso son a menudo aquellos en los que escribir en ensamblaje ofrecería el menor beneficio de rendimiento.
[Nota menor: hay algunos lugares donde el código de ensamblaje se puede usar para producir un desastre pegajoso hiper optimizado; por ejemplo, un fragmento de código que hice para ARM necesitaba recuperar una palabra de RAM y ejecutar una de las doce rutinas basadas en los seis bits superiores del valor (muchos valores asignados a la misma rutina). Creo que optimicé ese código para algo como:
El registro r8 siempre contenía la dirección de la tabla de despacho principal (dentro del bucle donde el código pasó el 98% de su tiempo, nada lo usó para ningún otro propósito); Las 64 entradas se refieren a direcciones en los 256 bytes que le preceden. Dado que el ciclo primario tenía en la mayoría de los casos un límite de tiempo de ejecución difícil de aproximadamente 60 ciclos, la recuperación y el despacho de nueve ciclos fue muy instrumental para alcanzar ese objetivo. El uso de una tabla de 256 direcciones de 32 bits habría sido un ciclo más rápido, pero habría engullido 1 KB de RAM muy valiosa [la memoria flash habría agregado más de un estado de espera]. El uso de 64 direcciones de 32 bits habría requerido agregar una instrucción para enmascarar algunos bits de la palabra obtenida, y aún habría engullido 192 bytes más que la tabla que realmente usé. El uso de la tabla de compensaciones de 8 bits produjo un código muy compacto y rápido, pero no es algo que esperaría que un compilador pudiera encontrar; Tampoco esperaría que un compilador dedique un registro "a tiempo completo" para mantener la dirección de la tabla.
El código anterior fue diseñado para ejecutarse como un sistema autónomo; podría llamar periódicamente al código C, pero solo en ciertos momentos cuando el hardware con el que se comunicaba podría ponerse en estado "inactivo" de forma segura durante dos intervalos de aproximadamente un milisegundo cada 16 ms.
fuente
En los últimos tiempos, todas las optimizaciones de velocidad que he realizado reemplazan el código lento dañado por el cerebro con un código razonable. Pero debido a que la velocidad era realmente crítica y puse un esfuerzo serio para hacer algo rápido, el resultado siempre fue un proceso iterativo, donde cada iteración daba más información sobre el problema, encontrando formas de resolver el problema con menos operaciones. La velocidad final siempre dependía de la cantidad de información que tuviera sobre el problema. Si en cualquier etapa utilicé el código de ensamblaje, o el código C que estaba demasiado optimizado, el proceso de encontrar una mejor solución habría sufrido y el resultado final sería más lento.
fuente
Cuando codifico en ASM, reorganizo las instrucciones manualmente para que la CPU pueda ejecutar más de ellas en paralelo cuando sea lógicamente posible. Apenas uso RAM cuando codifico en ASM, por ejemplo: podría haber más de 20000 líneas de código en ASM y nunca utilicé push / pop.
Potencialmente, podría saltar en el medio del código de operación para auto modificar el código y el comportamiento sin la posible penalización del código de auto modificación. Acceder a los registros toma 1 tick (a veces toma .25 ticks) de la CPU. Acceder a la RAM puede tomar cientos.
Para mi última aventura de ASM, nunca utilicé la RAM para almacenar una variable (para miles de líneas de ASM). ASM podría ser potencialmente inimaginablemente más rápido que C ++. Pero depende de muchos factores variables como:
¡Ahora estoy aprendiendo C # y C ++ porque me di cuenta de que la productividad es importante! Podría intentar hacer los programas más rápidos imaginables utilizando ASM puro solo en el tiempo libre. Pero para producir algo, use un lenguaje de alto nivel.
Por ejemplo, el último programa que codifiqué estaba usando JS y GLSL y nunca noté ningún problema de rendimiento, incluso hablando de JS, que es lento. Esto se debe a que el simple concepto de programar la GPU para 3D hace que la velocidad del lenguaje que envía los comandos a la GPU sea casi irrelevante.
La velocidad del ensamblador solo en el metal desnudo es irrefutable. ¿Podría ser aún más lento dentro de C ++? - Podría ser porque está escribiendo código de ensamblaje con un compilador que no está utilizando un ensamblador para comenzar.
Mi consejo personal es que nunca escriba el código de ensamblaje si puede evitarlo, aunque me encanta el ensamblaje.
fuente
Todas las respuestas aquí parecen excluir un aspecto: a veces no escribimos código para lograr un objetivo específico, sino por pura diversión . Puede que no sea económico invertir el tiempo para hacerlo, pero podría decirse que no hay mayor satisfacción que vencer al fragmento de código optimizado del compilador más rápido en velocidad con una alternativa asm rodada manualmente.
fuente
Un compilador de c ++ produciría, después de la optimización a nivel organizacional, un código que utilizaría las funciones integradas de la CPU objetivo. HLL nunca superará o superará al ensamblador por varias razones; 1.) HLL se compilará y generará con el código de acceso, la verificación de límites y, posiblemente, la recolección de basura integrada (que anteriormente abordaba el alcance en el estilo de OOP), todo lo cual requiere ciclos (flips y flops). HLL hace un excelente trabajo en estos días (incluidos C ++ más nuevos y otros como GO), pero si superan al ensamblador (es decir, su código), debe consultar la documentación de la CPU: las comparaciones con código descuidado ciertamente no son concluyentes y los idiomas compilados como ensamblador se resuelven. hasta el código de operación, HLL extrae los detalles y no los elimina; de lo contrario, su aplicación no se ejecutará si el sistema operativo host la reconoce.
La mayoría del código ensamblador (principalmente objetos) se muestra como "sin cabeza" para su inclusión en otros formatos ejecutables con mucho menos procesamiento requerido, por lo tanto, será mucho más rápido, pero mucho más inseguro; si el ensamblador genera un ejecutable (NAsm, YAsm; etc.), seguirá ejecutándose más rápido hasta que coincida completamente con el código HLL en la funcionalidad, entonces los resultados se pueden pesar con precisión.
Llamar a un objeto de código basado en ensamblador desde HLL en cualquier formato agregará inherentemente una sobrecarga de procesamiento, además de las llamadas de espacio de memoria utilizando memoria asignada globalmente para tipos de datos variables / constantes (esto se aplica tanto a LLL como a HLL). Recuerde que el resultado final es usar la CPU en última instancia como su api y abi en relación con el hardware (código de operación) y ambos, los ensambladores y los "compiladores HLL" son esencialmente / fundamentalmente idénticos, con la única excepción verdadera que es la legibilidad (gramatical).
La aplicación de consola Hello World en ensamblador que usa FAsm es de 1.5 KB (y esto es en Windows aún más pequeño en FreeBSD y Linux) y supera todo lo que GCC puede tirar en su mejor día; Las razones son relleno implícito con nops, validación de acceso y verificación de límites, por nombrar algunos. El objetivo real es libs HLL limpias y un compilador optimizable que apunta a una CPU de una manera "hardcore" y la mayoría lo hace en estos días (finalmente). GCC no es mejor que YAsm: son las prácticas de codificación y la comprensión del desarrollador las que están en cuestión y la "optimización" se produce después de la exploración novata y la capacitación y experiencia interinas.
Los compiladores tienen que vincular y ensamblar para la salida en el mismo código de operación que un ensamblador porque esos códigos son todo lo que una CPU excepto (CISC o RISC [PIC también]). YAsm optimizó y limpió mucho en los primeros NAsm, lo que aceleró en última instancia toda la salida de ese ensamblador, pero aun así, YAsm aún, como NAsm, produce ejecutables con dependencias externas dirigidas a las bibliotecas del sistema operativo en nombre del desarrollador, por lo que el kilometraje puede variar. Al cerrar, C ++ se encuentra en un punto increíble y mucho más seguro que el ensamblador para más del 80 por ciento, especialmente en el sector comercial ...
fuente
ld
, pero no hace ninguna diferencia a menos que esté tratando de optimizar realmente el tamaño del archivo (no solo el tamaño del archivo segmento de texto). Vea un tutorial de Whirlwind sobre la creación de ejecutables ELF realmente para Teensy para Linux .std::vector
compilado en modo de depuración. Las matrices de C ++ no son así. Los compiladores pueden verificar cosas en el momento de la compilación, pero a menos que habilite opciones de endurecimiento adicionales, no hay verificación en tiempo de ejecución. Vea, por ejemplo, una función que incrementa los primeros 1024 elementos de unint array[]
argumento. La salida asm no tiene comprobaciones de tiempo de ejecución: godbolt.org/g/w1HF5t . Todo lo que obtiene es un punterordi
, sin información de tamaño. Depende del programador evitar un comportamiento indefinido al nunca llamarlo con una matriz más pequeña que 1024.new
, eliminar manualmente condelete
, sin verificación de límites). Usted puede utilizar C ++ para producir mierda hinchada asm / máquina de código (como la mayoría del software), pero eso es culpa del programador, no C ++ 's. Incluso puede usaralloca
para asignar espacio de pila como una matriz.g++ -O3
generar código de verificación de límites para una matriz simple, o hacer cualquier otra cosa de la que esté hablando. C ++ hace que sea mucho más fácil generar binarios hinchados (y, de hecho, debes tener cuidado de no hacerlo si buscas rendimiento), pero no es literalmente inevitable. Si comprende cómo C ++ se compila en asm, puede obtener un código que es solo algo peor de lo que podría escribir a mano, pero con una alineación y propagación constante en una escala mayor de la que podría manejar a mano.El ensamblaje podría ser más rápido si su compilador genera mucho código de soporte OO .
Editar:
Para los votantes: el OP escribió "¿debería ... centrarme en C ++ y olvidarme del lenguaje ensamblador?" y mantengo mi respuesta. Siempre debe vigilar el código que genera OO, especialmente cuando se utilizan métodos. Sin olvidar el lenguaje ensamblador significa que revisará periódicamente el ensamblaje que genera su código OO, lo que creo que es imprescindible para escribir un software que funcione bien.
En realidad, esto pertenece a todos los códigos compilables, no solo a OO.
fuente