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:
- ¿Por qué
gcc
no puede optimizar esta función cuandoclang
puede? - Cambia el valor de
k
a1.0
ygcc
vuelve tan rápido, ¿hay un problema de aritmética de punto flotante quegcc
no puedagcc
?
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 .