compiler c++ performance gcc optimization clang

c++ - clang compiler



¿Por qué produce clang un código mucho más rápido que gcc para esta función simple que implica exponenciación? (2)

Además de la respuesta de Shafik Yaghmour, me gustaría señalar que la razón por la cual el uso de volatile en la variable num parece tener efecto es que num se lee antes de que se func . La lectura no se puede optimizar, pero la llamada a la función aún puede optimizarse. Si declaró que el parámetro de func es una referencia a volatile , es decir. long double func(volatile int& num) , esto evitaría que el compilador optimizara toda la llamada a func .

El siguiente código compilado con clang ejecuta casi 60 veces más rápido que el compilado con gcc con banderas de compilación idénticas (ya sea -O2 o -O2 ):

#include <iostream> #include <math.h> #include <chrono> #include <limits> long double func(int num) { long double i=0; long double k=0.7; for(int t=1; t<num; t++){ for(int n=1; n<16; n++){ i += pow(k,n); } } return i; } int main() { volatile auto num = 3000000; // avoid constant folding std::chrono::time_point<std::chrono::system_clock> start, end; start = std::chrono::system_clock::now(); auto i = func(num); end = std::chrono::system_clock::now(); std::chrono::duration<double> elapsed = end-start; std::cout.precision(std::numeric_limits<long double>::max_digits10); std::cout << "Result " << i << std::endl; std::cout << "Elapsed time is " << elapsed.count() << std::endl; return 0; }

He probado esto con tres versiones de gcc 4.8.4/4.9.2/5.2.1 y dos versiones de clang 3.5.1/3.6.1 y aquí están los tiempos en mi máquina (para gcc 5.2.1 y clang 3.6.1 ) :

Temporización -O3 :

gcc: 2.41888s clang: 0.0396217s

Timing -O2 :

gcc: 2.41024s clang: 0.0395114s

Temporización -O1 :

gcc: 2.41766s clang: 2.43113s

Parece que gcc no optimiza esta función, incluso a niveles de optimización más altos. La salida de ensamblaje de clang es casi alrededor de 100 líneas más largas que gcc y no creo que sea necesario publicarlo aquí, todo lo que puedo decir es que en la salida de montaje de gcc hay una llamada a pow que no aparece en el ensamblaje de clang , presumiblemente porque clang optimiza para un grupo de llamadas intrínsecas.

Dado que los resultados son idénticos (es decir, i = 6966764.74717416727754 ), la pregunta es:

  1. ¿Por qué gcc no puede optimizar esta función cuando clang puede?
  2. Cambia el valor de k a 1.0 y gcc vuelve tan rápido, ¿hay un problema de aritmética de punto flotante que gcc no pueda gcc ?

static_cast la static_cast y static_cast las advertencias para ver si había algún problema con las conversiones implícitas, pero no realmente.

Actualización: para completar los resultados de -Ofast

gcc: 0.00262204s clang: 0.0013267s

El punto es que gcc no optimiza el código en O2/O3 .


Desde esta sesión de godbolt, clang puede realizar todos los cálculos de pow en tiempo de compilación. Sabe en tiempo de compilación cuáles son los valores de k y n , y simplemente dobla el cálculo de manera constante:

.LCPI0_0: .quad 4604480259023595110 # double 0.69999999999999996 .LCPI0_1: .quad 4602498675187552091 # double 0.48999999999999994 .LCPI0_2: .quad 4599850558606658239 # double 0.34299999999999992 .LCPI0_3: .quad 4597818534454788671 # double 0.24009999999999995 .LCPI0_4: .quad 4595223380205512696 # double 0.16806999999999994 .LCPI0_5: .quad 4593141924544133109 # double 0.11764899999999996 .LCPI0_6: .quad 4590598673379842654 # double 0.082354299999999963 .LCPI0_7: .quad 4588468774839143248 # double 0.057648009999999972 .LCPI0_8: .quad 4585976388698138603 # double 0.040353606999999979 .LCPI0_9: .quad 4583799016135705775 # double 0.028247524899999984 .LCPI0_10: .quad 4581356477717521223 # double 0.019773267429999988 .LCPI0_11: .quad 4579132580613789641 # double 0.01384128720099999 .LCPI0_12: .quad 4576738892963968780 # double 0.0096889010406999918 .LCPI0_13: .quad 4574469401809764420 # double 0.0067822307284899942 .LCPI0_14: .quad 4572123587912939977 # double 0.0047475615099429958

y desenrolla el bucle interno:

.LBB0_2: # %.preheader faddl .LCPI0_0(%rip) faddl .LCPI0_1(%rip) faddl .LCPI0_2(%rip) faddl .LCPI0_3(%rip) faddl .LCPI0_4(%rip) faddl .LCPI0_5(%rip) faddl .LCPI0_6(%rip) faddl .LCPI0_7(%rip) faddl .LCPI0_8(%rip) faddl .LCPI0_9(%rip) faddl .LCPI0_10(%rip) faddl .LCPI0_11(%rip) faddl .LCPI0_12(%rip) faddl .LCPI0_13(%rip) faddl .LCPI0_14(%rip)

Tenga en cuenta que está utilizando una función incorporada ( gcc documenta la suya aquí ) para calcular pow en tiempo de compilación y si usamos -fno-builtin ya no realiza esta optimización.

Si cambia k por 1.0 entonces gcc puede realizar la misma optimización:

.L3: fadd %st, %st(1) #, addl $1, %eax #, t cmpl %eax, %edi # t, num fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, fadd %st, %st(1) #, jne .L3 #,

Aunque es un caso más simple.

Si cambia la condición del lazo interno a n < 4 entonces gcc parece estar dispuesto a optimizar cuando k = 0.7 . Como se indicó en los comentarios a la pregunta, si el compilador no cree que el desenrollamiento ayude, entonces será conservador en cuanto a la cantidad de desenrollado que se obtendrá ya que existe una transacción de tamaño de código.

Como se indica en los comentarios, estoy usando una versión modificada del código de OP en los ejemplos de godbolt pero no cambia la conclusión subyacente.

Tenga en cuenta como se indica en un comentario anterior si usamos -fno-math-errno , que detiene la configuración de errno , gcc aplica una optimización similar .