对于复数乘法,g ++与手动优化

时间:2018-03-09 08:09:17

标签: c++ gcc optimization clang

在我们的代码库中,我们有很多操作,如j *ω* X,其中j是虚数单位,ω是实数,X是复数。实际上很多循环看起来像:

#include <complex>
#include <vector>

void mult_jomega(std::vector<std::complex<double> > &vec, double omega){
    std::complex<double> jomega(0.0, omega);
    for (auto &x : vec){
        x*=jomega;
    }
}

然而,我们利用jomega的实部为零并将乘法写为:

的事实
void mult_jomega_smart(cvector &vec, double omega){
    for (auto &x : vec){
        x={-omega*x.imag(), omega*x.real()};
    }
}

一开始,我对这种“聪明”的转变不屑一顾,因为

  1. 更难理解。
  2. 错误的概率更高。
  3. “编译器无论如何都会优化它。”
  4. 然而,正如一些表现回归所显示的那样,第三个论点并不成立。在比较这两个函数时(请参阅下面的详细信息),智能版本与-O2以及-O3的效果始终如一:

    size    orig(musec)   smart(musec)  speedup
    10      0.039928      0.0117551     3.39665
    100     0.328564      0.0861379     3.81439
    500     1.62269       0.417475      3.8869
    1000    3.33012       0.760515      4.37877
    2000    6.46696       1.56048       4.14422
    10000   32.2827       9.2361        3.49528
    100000  326.828       115.158       2.8381
    500000  1660.43       850.415       1.95249
    

    我的机器上的智能版本速度提高了大约4倍(gcc-5.4),并且随着任务变得越来越大,随着阵列尺寸的增加,内存限制越来越快,速度降低到2倍。

    我的问题是,什么阻止编译器优化不太智能但更易读的版本,毕竟编译器可以看到jomega的实部是零?是否可以通过提供一些额外的编译标志来帮助编译器进行优化?

    注意:其他编译器也存在加速:

    compiler      speedup
    g++-5.4          4
    g++-7.2          4
    clang++-3.8      2  [original version 2-times faster than gcc]
    

    编目:

    mult.cpp - 防止内联:

    #include <complex>
    #include <vector>
    
    typedef std::vector<std::complex<double> > cvector;
    void mult_jomega(cvector &vec, double omega){
        std::complex<double> jomega(0.0, omega);
        for (auto &x : vec){
            x*=jomega;
        }
    }
    
    void mult_jomega_smart(cvector &vec, double omega){
        for (auto &x : vec){
            x={-omega*x.imag(), omega*x.real()};
        }
    }
    

    main.cpp中:

    #include <chrono>
    #include <complex>
    #include <vector>
    #include <iostream>
    
    typedef std::vector<std::complex<double> > cvector;
    void mult_jomega(cvector &vec, double omega);
    void mult_jomega2(cvector &vec, double omega);
    void mult_jomega_smart(cvector &vec, double omega);
    
    
    const size_t N=100000;   //10**5
    const double OMEGA=1.0;//use 1, so nothing changes -> no problems with inf & Co
    
    void compare_results(const cvector &vec){
       cvector m=vec;
       cvector m_smart=vec;
       mult_jomega(m, 5.0);
       mult_jomega_smart(m_smart,5.0);
       std::cout<<m[0]<<" vs "<<m_smart[0]<<"\n";
       std::cout<< (m==m_smart ? "equal!" : "not equal!")<<"\n";
    
    }
    
    void test(size_t vector_size){
    
         cvector vec(vector_size, std::complex<double>{1.0, 1.0});
    
         //compare results, triger if in doubt
         //compare_results(vec);
    
    
         //warm_up, just in case:
         for(size_t i=0;i<N;i++)
            mult_jomega(vec, OMEGA);
    
         //test mult_jomega:
         auto begin = std::chrono::high_resolution_clock::now();
         for(size_t i=0;i<N;i++)
            mult_jomega(vec, OMEGA);
         auto end = std::chrono::high_resolution_clock::now();
         auto time_jomega=std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count()/1e3;
    
    
         //test mult_jomega_smart:
         begin = std::chrono::high_resolution_clock::now();
         for(size_t i=0;i<N;i++)
            mult_jomega_smart(vec, OMEGA);
         end = std::chrono::high_resolution_clock::now();
         auto time_jomega_smart=std::chrono::duration_cast<std::chrono::nanoseconds>(end-begin).count()/1e3;
    
         double speedup=time_jomega/time_jomega_smart;
         std::cout<<vector_size<<"\t"<<time_jomega/N<<"\t"<<time_jomega_smart/N<<"\t"<<speedup<<"\n";
    }
    
    
    int main(){
       std::cout<<"N\tmult_jomega(musec)\tmult_jomega_smart(musec)\tspeedup\n";    
       for(const auto &size : std::vector<size_t>{10,100,500,1000,2000,10000,100000,500000})
            test(size);          
    }
    

    建筑与建筑运行:

    g++ main.cpp mult.cpp -O3 -std=c++11 -o mult_test
    ./mult_test
    

3 个答案:

答案 0 :(得分:8)

使用标记-ffast-math进行编译可以提高性能。

N       mult_jomega(musec)      mult_jomega_smart(musec)        speedup
10      0.00860809              0.00818644                      1.05151
100     0.0706683               0.0693907                       1.01841
500     0.29569                 0.297323                        0.994509
1000    0.582059                0.57622                         1.01013
2000    1.30809                 1.24758                         1.0485
10000   7.37559                 7.4854                          0.98533

编辑:更具体地说,它是-funsafe-math-optimizations编译器标志。 According to the documentation,此标志用于

  

允许对(a)假设的浮点运算进行优化   参数和结果有效,(b)可能违反IEEE或ANSI   标准。当

编辑2 :更具体地说,它是-fno-signed-zeros选项,其中:

  

允许优化浮点运算,忽略   签署零。 IEEE算法指定了distinct的行为   +0.0-0.0值,然后禁止简化x+0.00.0*x等表达式(即使使用-ffinite-math-only)。   此选项意味着零结果的符号不重要。

答案 1 :(得分:1)

我对使用godbolt编译器资源管理器的Aziz答案中提到的编译器选项进行了更多调查。示例代码实现了内部循环的三个版本:

  1. mult_jomega示例。
  2. 同一循环的手写版本,其中operator*=的调用已被计算替换
  3. mult_jomega_smart示例
  4. The code from godbolt

    // 1. mult_jomega
    std::complex<double> const jomega(0.0, omega);
    for (auto &x : v){
        x*=jomega;
    }
    
    // 2. hand-written mult_jomega
    for (auto &x : v3){
        double x1 = x.real() * jomega.real();
        double x2 = x.imag() * jomega.imag();
        double x3 = x.real() * jomega.imag();
        double x4 = x.imag() * jomega.real();
        x = { x1 - x2 , x3 + x4};
    }
    
    // 3. mult_jomega_smart
    for (auto &x : v2){
        x={-omega*x.imag(), omega*x.real()};
    }
    

    检查三个循环的汇编程序代码:

    mult_jomega

     cmp %r13,%r12
     je 4008ac <main+0x10c>
     mov %r12,%rbx
     nopl 0x0(%rax)
     pxor %xmm0,%xmm0
     add $0x10,%rbx
     movsd -0x8(%rbx),%xmm3
     movsd -0x10(%rbx),%xmm2
     movsd 0x8(%rsp),%xmm1
     callq 400740 <__muldc3@plt>
     movsd %xmm0,-0x10(%rbx)
     movsd %xmm1,-0x8(%rbx)
     cmp %rbx,%r13
     jne 400880 <main+0xe0>
    

    手写乘法

     cmp %rdx,%rdi
     je 40090c <main+0x16c>
     pxor %xmm3,%xmm3
     mov %rdi,%rax
     movsd 0x8(%rsp),%xmm5
     nopl 0x0(%rax,%rax,1)
     movsd (%rax),%xmm0
     movapd %xmm5,%xmm4
     movsd 0x8(%rax),%xmm1
     add $0x10,%rax
     movapd %xmm0,%xmm2
     mulsd %xmm5,%xmm0
     mulsd %xmm1,%xmm4
     mulsd %xmm3,%xmm2
     mulsd %xmm3,%xmm1
     subsd %xmm4,%xmm2
     addsd %xmm1,%xmm0
     movsd %xmm2,-0x10(%rax)
     movsd %xmm0,-0x8(%rax)
     cmp %rax,%rdx
     jne 4008d0 <main+0x130>
    

    mult_jomega_smart

    cmp %rcx,%rdx
     je 400957 <main+0x1b7>
     movsd 0x8(%rsp),%xmm2
     mov %rcx,%rax
     xorpd 0x514(%rip),%xmm2 # 400e40 <_IO_stdin_used+0x10>
     nopl 0x0(%rax)
     add $0x10,%rax
     movsd 0x8(%rsp),%xmm0
     movsd -0x8(%rax),%xmm1
     mulsd -0x10(%rax),%xmm0
     mulsd %xmm2,%xmm1
     movsd %xmm1,-0x10(%rax)
     movsd %xmm0,-0x8(%rax)
     cmp %rax,%rdx
     jne 400930 <main+0x190>
    

    我对汇编程序代码的理解非常有限,但我看到了

    • operator*=未在mult_jomega
    • 中加入内联
    • x1x4计算,但它们总是0.0,因为jomega.real()==0.0

    我不知道为什么operator*=会吸引内幕消息。源代码是直接的,仅包含三行。

    The computation of x1 and x4 can be explained when you consider that 0.0 * x == 0.0 is not always true for values of type double.除了在另一个答案中提到的已签名零定义之外,还有无限值naninf x * 0.0 = 0.0不存在。

    如果使用-fno-signed-zeros-ffinite-math-only进行编译,则应用优化并删除x1x4的计算。

答案 2 :(得分:0)

正如其他答案所指出的那样,我的错误是假设纯虚数j*ω与复数0.0+j*ω具有相同的行为-与纯实数1.0相同与复数1.0+0.0j的行为不同,例如(live with gcc

1.0*(inf+0.0j) = inf+0.0j
(1.0 +0.0j)*(inf+0.0j) = inf - nanj

这是由于这样的事实,即复数乘法比学校公式建议的more complex还要多。

基本上,c ++编译器如何处理复数是不对称的:存在纯实数(即double),而没有纯虚数。

C ++标准未定义复数乘法必须如何发生。大多数编译器都依赖于C99的实现。 C99是第一个C格式,定义了如何在附件G中执行复数运算。但是,如G.1.1所述,它支持可选:

  

G.1.1 ...尽管这些规格是经过精心设计的,   现有很少实践可以验证设计决策。   因此,这些规范不是规范性的,但应   视更多为推荐做法...

C99还定义了纯虚数数据类型float _Imaginarydouble _Imaginarylong double _Imaginary(G.2),这正是我们j*ω所需要的。 G.5.1将纯虚数的乘法语义定义为

xj*(v+wj) = -xw+(xv)j
(v+wj)*xj = -wx+(vx)j

即学校公式足够好(不同于两个复数的乘法)。

问题是,到目前为止,尚无已知的编译器(gcc-9.2,clang-9.0)支持_Imaginary类型(因为它是可选的)。

因此,我的解决方案是实现pure_imaginary类型并重载遵循G.5.1的运算符。