【问题标题】:Why is std::sin() and std::cos() slower than sin() and cos()?为什么 std::sin() 和 std::cos() 比 sin() 和 cos() 慢?
【发布时间】:2011-08-07 23:17:48
【问题描述】:

测试代码:

#include <cmath>
#include <cstdio>

const int N = 4096;
const float PI = 3.1415926535897932384626;

float cosine[N][N];
float sine[N][N];

int main() {
    printf("a\n");
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            cosine[i][j] = cos(i*j*2*PI/N);
            sine[i][j] = sin(-i*j*2*PI/N);
        }
    }
    printf("b\n");
}

时间到了:

$ g++ main.cc -o main
$ time ./main
a
b

real    0m1.406s
user    0m1.370s
sys     0m0.030s

添加using namespace std;后,时间为:

$ g++ main.cc -o main
$ time ./main
a
b

real    0m8.743s
user    0m8.680s
sys     0m0.030s

编译器:

$ g++ --version
g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2

组装:

Dump of assembler code for function sin@plt:                                    
0x0000000000400500 <+0>:     jmpq   *0x200b12(%rip)        # 0x601018 <_GLOBAL_OFFSET_TABLE_+48>
0x0000000000400506 <+6>:     pushq  $0x3                                     
0x000000000040050b <+11>:    jmpq   0x4004c0                                 
End of assembler dump.

Dump of assembler code for function std::sin(float):                            
0x0000000000400702 <+0>:     push   %rbp                                     
0x0000000000400703 <+1>:     mov    %rsp,%rbp                                
0x0000000000400706 <+4>:     sub    $0x10,%rsp                               
0x000000000040070a <+8>:     movss  %xmm0,-0x4(%rbp)                         
0x000000000040070f <+13>:    movss  -0x4(%rbp),%xmm0                         
0x0000000000400714 <+18>:    callq  0x400500 <sinf@plt>                      
0x0000000000400719 <+23>:    leaveq                                          
0x000000000040071a <+24>:    retq                                            
End of assembler dump.

Dump of assembler code for function sinf@plt:                                   
0x0000000000400500 <+0>:     jmpq   *0x200b12(%rip)        # 0x601018 <_GLOBAL_OFFSET_TABLE_+48>
0x0000000000400506 <+6>:     pushq  $0x3                                     
0x000000000040050b <+11>:    jmpq   0x4004c0                                 
End of assembler dump.

【问题讨论】:

  • @Nawaz:可能。 &lt;cmath&gt; 是否在全局命名空间中提供 double sin(double)double cos(double) 是一个实现细节。 &lt;cstdio&gt;printf 同上。
  • @Nawaz 它确实可以编译。这是我真正的编码。
  • 回答此类问题的最简单方法是比较编译器的汇编输出。
  • @David:那最好不要是std::cos 的全部定义。见 26.8/8 和 26.8/9。 (我也相信 26.8/4 可以解释为不能在全局命名空间中提供这些重载。)或者 D.5 是否要求它们在全局范围内可用。这有点令人困惑。
  • 有趣的是,我在这里测试过,不优化编译比使用 O3 快得多...只是从他的 PI 更改为 M_PI 的事实产生了很大的不同,不知道为什么。

标签: c++ performance cmath trigonometry


【解决方案1】:

您正在使用不同的重载:

试试

        double angle = i*j*2*PI/N;
        cosine[i][j] = cos(angle);
        sine[i][j] = sin(angle);

无论有没有using namespace std;,它都应该执行相同的操作

【讨论】:

  • 您的代码可以工作,但无论是否更改命名空间,它都运行得很快。为什么我提供的代码运行速度慢很多?
  • @Ryan:因为我的代码总是调用double sin(double)。您的原始代码从全局范围调用double sin(double),或从namespace std 调用float sin(float)。现代 FPU 针对双打操作进行了优化。
  • 添加了一些程序集,你的结论还成立吗? (我不是组装忍者)
  • @Ryan:更有趣的是代码的汇编列表(尤其是循环内的部分)
  • @BenVoigt 您是否有支持“现代 FPU 已针对双打操作进行了优化”的测量结果。争论?在我的笔记本电脑上则完全相反,std::sin 在 float 上的两倍时始终慢 2.5 倍。
【解决方案2】:

不同之处在于 std::sin() 对 float 和 double 有重载,而 sin() 只需要 double。在浮点数的 std::sin() 内部,可能会转换为双精度数,然后调用双精度数的 std::sin(),然后将结果转换回浮点数,使其变慢。

【讨论】:

  • floatdouble 之间的转换不考虑它。我今天用 g++ 进行了一些测试,发现使用 -O2 时,float 代码要慢得多。但是,当我使用手动转换进行测试时,如下所示:(float)sin((double)input) 我发现优化的float 代码比优化的double 代码运行更快,即使我强制使用float使用doublesin函数的代码。
  • @KyleA:那是 2011 年。现在是 2017 年。运行时代码可能已更改。
  • @RudyVelthuis 看到我的回答
  • @OliverSohn:这是对几乎一年前的评论的回复。这是试图成为 SO 上最慢的对话吗?
【解决方案3】:

我使用带有-O3 优化的clang 进行了一些测量,在Intel Core i7 上运行。我发现:

  • std::sinfloat 的成本与 sinf 相同
  • std::sin on doublesin 的成本相同
  • double 上的 sin 函数比 float 上慢 2.5 倍(同样,在 Intel Core i7 上运行)。

这里是重现它的完整代码:

#include <chrono>
#include <cmath>
#include <iostream>

template<typename Clock>
struct Timer
{
    using rep = typename Clock::rep;
    using time_point = typename Clock::time_point;
    using resolution = typename Clock::duration;

    Timer(rep& duration) :
    duration(&duration) {
        startTime = Clock::now();
    }
    ~Timer() {
        using namespace std::chrono;
        *duration = duration_cast<resolution>(Clock::now() - startTime).count();
    }
private:

    time_point startTime;
    rep* duration;
};

template<typename T, typename F>
void testSin(F sin_func) {
  using namespace std;
  using namespace std::chrono;
  high_resolution_clock::rep duration = 0;
  T sum {};
  {
    Timer<high_resolution_clock> t(duration);
    for(int i=0; i<100000000; ++i) {
      sum += sin_func(static_cast<T>(i));
    }
  }
  cout << duration << endl;
  cout << "  " << sum << endl;
}

int main() {
  testSin<float> ([] (float  v) { return std::sin(v); });
  testSin<float> ([] (float  v) { return sinf(v); });
  testSin<double>([] (double v) { return std::sin(v); });
  testSin<double>([] (double v) { return sin(v); });
  return 0;
}

如果人们可以在 cmets 中报告其架构的结果,我会很感兴趣,尤其是关于 floatdouble 时间的结果。

【讨论】:

  • 在 Linux "ARMv8 Processor rev 0 (v8l)" 上,我用 -O0 得到这些时间刻度(以十亿为单位):63G、61G、7.2G、7.1G。使用 -O3 为 59.6G、59.1G 和 2x6.6G。所以 float 比 double 慢 ~8.7x/~9x,而 std::xxx vs xxx 可能无关紧要。在“Intel Xeon W-2123 CPU @ 3.60GHz”上,使用 -O0 我得到 2.2G、1.98G 和 2x 3.5G,使用 -O3 得到 2x1.6G 和 2x3.0G,即这里 float 是 ~比两倍快 1.7 (O0) 和 ~1.8 倍。我没有受过教育的猜测是,这是由于浮点结果是 -1.68,而双变量返回 0.78。 Xeon 上的标志 -march=native 使浮动速度减至两倍。
  • @FelEnd 我对“ARMv8 Processor rev 0 (v8l)”的结果感到非常惊讶:你知道为什么使用双精度数会快得多吗?
  • 不,我不知道。结果适用于我尝试使用您的代码的多种变体。我试图深入 glibc 的 libm 源代码的兔子洞,但无法真正确定执行了什么。在您的代码汇编中,区别在于“bl sinf”与“bl sin”(bl = 带有链接的分支)。我的盲目猜测是,arm 的 fpu 是为 double 设计的,并提供了一些内在指令,但为此需要检查 libm 的程序集。
【解决方案4】:

在编译器命令行中使用 -S 标志并检查汇编器输出之间的差异。也许using namespace std; 在可执行文件中提供了很多未使用的东西。

【讨论】:

  • 这就是我有打印语句的原因,这样如果你运行代码你会发现大部分时间都花在循环中,而不是初始化。