不幸的是,标准 C++ 库没有对 sincos 进行一次调用,这为这个问题提供了空间。
第一个问题:
如果我想计算 sin 和 cos,是先计算 sin 和 cos 还是先计算 sin 再用 sqrt(1-sin^2) 来得到 cos 比较便宜?
第二个问题:
英特尔数学内核库为标准数学函数计算提供了非常好的函数,因此函数 vdSinCos() 的存在可以以非常优化的方式解决问题,但英特尔编译器不是免费的。 Linux 发行版中是否有可用的开源库(C、C++、Fortran)具有这些功能,我可以简单地链接到它们并获得最佳实现?
注意:我不想讨论指令调用,因为并非所有 CPU 都支持它们。我想链接到一个通用库,它可以在任何 CPU 上为我完成这项工作。
谢谢。
GNU C 库具有 sincos() 函数,它将利用大多数现代指令集所具有的“FSINCOS”指令。我想说这是你最好的选择;它应该和英特尔库方法一样快。
如果你不这样做,我会选择“sqrt(1-sin(x)^2)”路线。到目前为止,在我看过的每个处理器架构文档中,FSQRT 指令都比 FSIN 函数快得多。
几乎每个性能问题的答案都是“为什么不在代码中对其进行测量”,因为有大量不同的因素会影响几乎任何此类计算的性能。例如,“谁生成数学函数”。平方根计算起来相对简单,但我不相信
sqrt(1-sin*sin)
和再次计算 cos
之间存在巨大差异。什么处理器也可能是一个因素,以及“围绕”正弦/余弦计算进行哪些其他计算。
如果某个地方有一个具有此类功能的图书馆,我不会感到惊讶,但我还没有寻找过。
如果精度不重要,获得 sin 或 cos 的最快方法是使用表格。 保存一些全局常量数组,其中包含所有角度的 sin 和 cos 值以及您需要的步骤。因此,您的 sin/cos 函数只需将角度转换为索引即可获得结果。
我遇到了同样的问题,所以我对其进行了基准测试。结果是,根据您所做的事情,您的编译器可能比您更擅长优化,即使它不使用内在的
sincos
函数。
我编写了一个小测试程序来测试使用
sincos
内在函数、std::sin
和 std::cos
以及 std::sin
和从 sqrt(1-sin*sin)
计算出的 cos
测试涉及从 0-
2*M_PI
生成 1e8 个随机数。每个测试都计算每个随机数的 sin 和 cos,对值进行求和,然后将总和输出到标准输出 - 这确保了整个程序不会被优化。我用 O3 和 fp:fast 编译
使用 sqrt(1-sin*sin)
是迄今为止最慢的。这是因为我需要一个 if 语句来检查结果的符号。这意味着循环无法矢量化。其他选项的速度相似。最初,我创建了一个
fastSinCos
函数,它接受 4 个双精度数并返回 4 个双精度数。然后我将 4 个双打加到总和中。这比仅使用
sum += std::sin(input[i])+std::cos(input[i])
慢。事实证明,编译器在幼稚的实现中对总和进行了向量化,所以这样打败了我。当我修改代码来创建一个“fastSinCosSum”函数,其中总和被矢量化时,我成功地击败了简单的版本,但仅提高了 10%。
如果我将输入范围限制为
M_PI/2.0
-
3.0*M_PI/2.0
,这样我就知道 cos 的结果始终为负,那么速度与初始版本相同。由于 1e8 双倍比我的缓存大,我怀疑缓存未命中可能是实际的瓶颈。然而,即便如此,测试只花了大约一秒钟的时间来运行,所以担心它似乎很愚蠢。
所以最后,除非最理想化设置中 10% 的增益对您很重要,否则我怀疑您最好确保编译器可以向量化而不是尝试使用内部函数。三个选项生成的程序集如下所示。
fastSinCosSum(&inputs[i], r_sum3); //pass in the address of the first of the 4 elements to use and a register to store 4 sums
00007FF7FC321460 vmovupd ymm0,ymmword ptr [r14+rdi*8]
00007FF7FC321466 call __vdecl_sincos4 (07FF7FC322AD0h)
00007FF7FC32146B vaddpd ymm0,ymm0,ymmword ptr [r_sum3]
00007FF7FC321470 vaddpd ymm0,ymm0,ymm1
00007FF7FC321474 vmovupd ymmword ptr [r_sum3],ymm0
sum2 += std::sin(inputs[i]) + std::cos(inputs[i]); // just calculate naively
00007FF7FC321305 vmovupd ymm0,ymmword ptr [r14+rbx*8]
00007FF7FC32130B call __vdecl_cos4 (07FF7FC322A80h)
00007FF7FC321310 vmovupd ymmword ptr [rbp+60h],ymm0
00007FF7FC321315 vmovupd ymm0,ymmword ptr [r14+rbx*8]
00007FF7FC32131B call __vdecl_sin4 (07FF7FC322AA0h)
00007FF7FC321320 vaddpd ymm1,ymm0,ymmword ptr [rbp+60h]
00007FF7FC321325 vaddpd ymm0,ymm1,ymmword ptr [rbp+20h]
00007FF7FC32132A vmovupd ymmword ptr [rbp+20h],ymm0
double sin = std::sin(inputs[i]);
00007FF607CD1363 vmovupd ymm0,ymmword ptr [r14+rbx*8]
00007FF607CD1369 call __vdecl_sin4 (07FF607CD2A70h)
sum2a += sin - std::sqrt(1 - sin * sin); // calculate cos using sqrt. The angles are limited so we know the sign of the result is negative
00007FF607CD136E vmovupd ymm1,ymmword ptr [__ymm@3ff00000000000003ff00000000000003ff00000000000003ff0000000000000 (07FF607CD7480h)]
00007FF607CD1376 vfnmadd231pd ymm1,ymm0,ymm0
00007FF607CD137B vsqrtpd ymm1,ymm1
00007FF607CD137F vsubpd ymm0,ymm0,ymm1
00007FF607CD1383 vaddpd ymm1,ymm0,ymmword ptr [rbp+60h]
00007FF607CD1388 vmovupd ymmword ptr [rbp+60h],ymm1