sirve sintaxis resueltos que programa principiantes para funciones ejercicios ejemplos comandos codigos c++ performance assembly compiler-construction

sintaxis - programa c++



¿Por qué este programa en C++ es increíblemente rápido? (5)

¿Hay alguna manera de almacenar en caché el código compilado JIT después de que lo optimice, o tiene que volver a optimizar el código cada vez que se ejecuta el programa?

Si estuviera escribiendo en Python, intentaría reducir el tamaño del código para obtener una vista "general" de lo que estaba haciendo el código. Como intentar escribir esto (mucho más fácil de leer IMO):

for i in range(outer): innerS = sum(1 for _ in xrange(inner)) s += innerS s -= innerS

o incluso s = sum(inner - inner for _ in xrange(outer))

Escribí un pequeño punto de referencia para comparar el rendimiento de diferentes intérpretes / compiladores para Python, Ruby, JavaScript y C ++. Como era de esperar, resulta que C ++ (optimizado) supera a los lenguajes de scripting, pero el factor por el cual lo hace es increíblemente alto.

Los resultados son:

sven@jet:~/tmp/js$ time node bla.js # * JavaScript with node * 0 real 0m1.222s user 0m1.190s sys 0m0.015s sven@jet:~/tmp/js$ time ruby foo.rb # * Ruby * 0 real 0m52.428s user 0m52.395s sys 0m0.028s sven@jet:~/tmp/js$ time python blub.py # * Python with CPython * 0 real 1m16.480s user 1m16.371s sys 0m0.080s sven@jet:~/tmp/js$ time pypy blub.py # * Python with PyPy * 0 real 0m4.707s user 0m4.579s sys 0m0.028s sven@jet:~/tmp/js$ time ./cpp_non_optimized 1000 1000000 # * C++ with -O0 (gcc) * 0 real 0m1.702s user 0m1.699s sys 0m0.002s sven@jet:~/tmp/js$ time ./cpp_optimized 1000 1000000 # * C++ with -O3 (gcc) * 0 real 0m0.003s # (!!!) <---------------------------------- WHY? user 0m0.002s sys 0m0.002s

Me pregunto si alguien puede dar una explicación de por qué el código optimizado de C ++ es más de tres órdenes de magnitud más rápido que todo lo demás.

El benchmark C ++ usa parámetros de línea de comando para evitar el pre-cálculo del resultado en tiempo de compilación.

A continuación, coloqué los códigos fuente para los diferentes puntos de referencia del idioma, que deben ser semánticamente equivalentes. Además, proporcioné el código ensamblador para la salida optimizada del compilador C ++ (usando gcc). Al observar el ensamblaje optimizado, parece que el compilador fusionó los dos bucles en el punto de referencia en uno solo, pero aún así, ¡todavía hay un ciclo!

JavaScript:

var s = 0; var outer = 1000; var inner = 1000000; for (var i = 0; i < outer; ++i) { for (var j = 0; j < inner; ++j) { ++s; } s -= inner; } console.log(s);

Pitón:

s = 0 outer = 1000 inner = 1000000 for _ in xrange(outer): for _ in xrange(inner): s += 1 s -= inner print s

Rubí:

s = 0 outer = 1000 inner = 1000000 outer_end = outer - 1 inner_end = inner - 1 for i in 0..outer_end for j in 0..inner_end s = s + 1 end s = s - inner end puts s

C ++:

#include <iostream> #include <cstdlib> #include <cstdint> int main(int argc, char* argv[]) { uint32_t s = 0; uint32_t outer = atoi(argv[1]); uint32_t inner = atoi(argv[2]); for (uint32_t i = 0; i < outer; ++i) { for (uint32_t j = 0; j < inner; ++j) ++s; s -= inner; } std::cout << s << std::endl; return 0; }

Ensamblado (al compilar el código C ++ anterior con gcc -S -O3 -std = c ++ 0x):

.file "bar.cpp" .section .text.startup,"ax",@progbits .p2align 4,,15 .globl main .type main, @function main: .LFB1266: .cfi_startproc pushq %r12 .cfi_def_cfa_offset 16 .cfi_offset 12, -16 movl $10, %edx movq %rsi, %r12 pushq %rbp .cfi_def_cfa_offset 24 .cfi_offset 6, -24 pushq %rbx .cfi_def_cfa_offset 32 .cfi_offset 3, -32 movq 8(%rsi), %rdi xorl %esi, %esi call strtol movq 16(%r12), %rdi movq %rax, %rbp xorl %esi, %esi movl $10, %edx call strtol testl %ebp, %ebp je .L6 movl %ebp, %ebx xorl %eax, %eax xorl %edx, %edx .p2align 4,,10 .p2align 3 .L3: # <--- Here is the loop addl $1, %eax # <--- cmpl %eax, %ebx # <--- ja .L3 # <--- .L2: movl %edx, %esi movl $_ZSt4cout, %edi call _ZNSo9_M_insertImEERSoT_ movq %rax, %rdi call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ popq %rbx .cfi_remember_state .cfi_def_cfa_offset 24 popq %rbp .cfi_def_cfa_offset 16 xorl %eax, %eax popq %r12 .cfi_def_cfa_offset 8 ret .L6: .cfi_restore_state xorl %edx, %edx jmp .L2 .cfi_endproc .LFE1266: .size main, .-main .p2align 4,,15 .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB1420: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi addq $8, %rsp .cfi_def_cfa_offset 8 jmp __cxa_atexit .cfi_endproc .LFE1420: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .hidden __dso_handle .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2" .section .note.GNU-stack,"",@progbits


A pesar de que los bucles tienen muchas iteraciones, los programas probablemente todavía no tengan una ejecución prolongada suficiente como para escapar de la sobrecarga de los tiempos de inicio del intérprete / JVM / shell / etc. En algunos entornos, estos pueden variar enormemente; en algunos casos, * tos * Java * tos * tarda varios segundos antes de que llegue a estar cerca de su código real.

Idealmente, medirías la ejecución dentro de cada fragmento de código. Puede ser complicado hacer esto con precisión en todos los idiomas, pero incluso imprimir el tiempo del reloj en tics antes y después sería mejor que usar el time , y haría el trabajo, ya que probablemente no esté preocupado con el tiempo súper preciso aquí.

(Supongo que esto no se relaciona con por qué el ejemplo de C ++ es mucho más rápido, pero podría explicar parte de la variabilidad en los otros resultados :)).


El optimizador ha resuelto que el ciclo interno junto con la línea subsiguiente no funciona, y lo eliminó. Desafortunadamente, no ha logrado eliminar el bucle externo también.

Tenga en cuenta que el ejemplo de node.js es más rápido que el ejemplo de C ++ no optimizado, lo que indica que el V8 (compilador JIT del nodo) ha logrado eliminar al menos uno de los loops. Sin embargo, su optimización tiene algunos gastos generales, ya que (como cualquier compilador JIT) debe equilibrar las oportunidades de optimización y la re-optimización guiada por perfiles, en comparación con el costo de hacerlo.


No hice un análisis completo del ensamblaje, pero parece que se desenrolló el lazo interno y descubrí que, junto con la resta del interior, es un nudo.

El ensamblaje solo parece hacer el lazo externo que solo incrementa un contador hasta que se alcanza el exterior. Incluso podría haber optimizado eso, pero parece que no lo hizo.


for (uint32_t i = 0; i < outer; ++i) { for (uint32_t j = 0; j < inner; ++j) ++s; s -= inner; }

El bucle interno es equivalente a "s + = inner; j = inner;" que un buen compilador de optimización puede hacer. Como la variable j se ha ido después del ciclo, todo el código es equivalente a

for (uint32_t i = 0; i < outer; ++i) { s += inner; s -= inner; }

Nuevamente, un buen compilador de optimización puede eliminar los dos cambios en s, luego eliminar la variable i, y no queda nada en absoluto. Parece que eso es lo que sucedió.

Ahora depende de usted decidir con qué frecuencia ocurre una optimización como esta, y si se trata de un beneficio real.