查表法正弦估算函数使用float类型时性能反劣于double类型的原因排查求助
我最近写了一个用查表法实现的简易正弦估算函数,出于好奇同时测试了float和double两种版本——本来以为float版本因为数据更紧凑、缓存命中率更高会跑得更快,但实际结果完全相反:float版耗时7.715秒,double版只需要6.411秒,这个差异很明显而且每次测试都稳定重现。
先贴出我实现的double版本查表函数:
double fastSinD(double x) { int sinIndex = x * (0.5f * SINE_TABLE_SIZE / M_PI); double delta = x - sinIndex * (2.f * M_PI / SINE_TABLE_SIZE); int cosIndex = sinIndex + SINE_TABLE_SIZE/4; sinIndex &= SINE_TABLE_SIZE-1; cosIndex &= SINE_TABLE_SIZE-1; double sinVal = sineTable_4q[sinIndex]; double cosVal = sineTable_4q[cosIndex]; return sinVal + (cosVal - 0.5f*sinVal*delta) * delta; }
float版本的实现完全一致,只是把所有double类型换成float,查表用float数组,PI也用float版本。
我一开始怀疑是不是代码里有隐式的float转double转换拖慢了速度,但看生成的汇编代码发现并没有这种情况。下面是double版本的汇编输出:
fastSinD: .seh_endprologue vmulsd .LC4(%rip), %xmm0, %xmm1 leaq sineTable_4q(%rip), %rcx vcvttsd2sil %xmm1, %eax vxorps %xmm1, %xmm1, %xmm1 leal 128(%rax), %edx vcvtsi2sdl %eax, %xmm1, %xmm1 andl $511, %eax vfnmadd132sd .LC5(%rip), %xmm0, %xmm1 vmovsd (%rcx,%rax,8), %xmm2 movl %edx, %eax vmulsd .LC6(%rip), %xmm2, %xmm0 andl $511, %eax vfnmadd213sd (%rcx,%rax,8), %xmm1, %xmm0 vfmadd132sd %xmm1, %xmm2, %xmm0 ret
float版本的汇编几乎完全一样,只是把sd指令换成ss,对应的表地址也换成float版的:
fastSinF: .seh_endprologue vmulss .LC7(%rip), %xmm0, %xmm1 leaq f_sineTable_4q(%rip), %rcx vcvttss2sil %xmm1, %eax vxorps %xmm1, %xmm1, %xmm1 leal 128(%rax), %edx vcvtsi2ssl %eax, %xmm1, %xmm1 andl $511, %eax vfnmadd132ss .LC8(%rip), %xmm0, %xmm1 vmovss (%rcx,%rax,4), %xmm2 movl %edx, %eax vmulss .LC9(%rip), %xmm2, %xmm0 andl $511, %eax vfnmadd213ss (%rcx,%rax,4), %xmm1, %xmm0 vfmadd132ss %xmm1, %xmm2, %xmm0 ret
这两段汇编结构完全一致,说明函数本身没有问题,那为什么float版会更慢呢?
后来我按评论区的建议,把调用函数也加入了基准测试,终于发现了问题所在。先贴出我扩展后的基准测试代码:
void benchmarkCallingFunc() { struct timespec start, finish; int data[10000]; float samples[10000]; for (int i = 0; i < 10000; i++) { samples[i] = ((i*91) % 100000) / 1000.f; } clock_gettime(CLOCK_REALTIME, &start); double result = 0; for (int n = 0; n < 10; n++) { for (int k = 0; k < 10000; k++) { double sum = 0; for (int i = 0; i < 4095; i++) { double dt = samples[(k+n)%10000]*10 - i; float sinc = dt == 0 ? 1 : fastSinD(M_PI * dt) / (M_PI * dt); sum += (double)data[(i*k+n) % 10000] * sinc; } result += sum; } } printf("%f\n", result); clock_gettime(CLOCK_REALTIME, &finish); printf("Double fast sin took %d milliseconds.\n", (finish.tv_sec - start.tv_sec)*1000 + (finish.tv_nsec - start.tv_nsec) / 1000000L); clock_gettime(CLOCK_REALTIME, &start); result = 0; for (int n = 0; n < 10; n++) { for (int k = 0; k < 10000; k++) { double sum = 0; for (int i = 0; i < 4095; i++) { float dt = samples[(k+n)%10000]*10 - i; float sinc = dt == 0 ? 1 : fastSinF(M_PI * dt) / (M_PI * dt); sum += (float)data[(i*k+n) % 10000] * sinc; } result += sum; } } printf("%f\n", result); clock_gettime(CLOCK_REALTIME, &finish); printf("Float fast sin took %d milliseconds.\n", (finish.tv_sec - start.tv_sec)*1000 + (finish.tv_nsec - start.tv_nsec) / 1000000L); }
用这段代码测试时,double版耗时约1450ms,float版则需要1870ms;但我把float版本调用代码里的M_PI换成float字面量3.14159265f之后,float版的耗时直接降到了1242ms,性能提升了接近50%!
原来问题出在math.h里定义的M_PI是double类型的字面量,在float版本的调用逻辑中,用M_PI和float类型的dt相乘时,会触发大量隐式的float转double转换,这些转换的开销累积起来就导致了性能的大幅下降——我之前只检查了查表函数本身的汇编,完全没注意到调用侧的这个细节。
最后补充一下我的编译命令:
gcc -fdiagnostics-color=always -O3 -march=native benchmark.c -o benchmark.exe
内容来源于stack exchange

