![](/img/trans.png)
[英]Speed of accessing local vs. global variables in gcc/g++ at different optimization levels
[英]g++ vs. optimization by hand for complex number multiplication
在我们的代码库中,我们有很多操作,如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()};
}
}
一开始,我对这种“聪明”的转变不屑一顾,因为
然而,正如一些表现回归所显示的那样,第三个论点并不成立。 在比较这两个函数时(请参阅下面的列表),智能版本优于-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
使用标志-ffast-math
编译-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
编译器标志。 根据文档 ,这个标志用于
允许优化浮点运算,(a)假设参数和结果有效,(b)可能违反IEEE或ANSI标准。 什么时候
编辑2 :更具体地说,它是-fno-signed-zeros
选项,其中:
允许优化浮点运算,忽略零的符号。 IEEE算法指定了不同的
+0.0
和-0.0
值的行为,然后禁止简化表达式,如x+0.0
或0.0*x
(即使使用-ffinite-math-only
)。 此选项意味着零结果的符号不重要。
我对使用godbolt编译器资源管理器的Aziz回答中提到的编译器选项进行了更多调查。 示例代码实现了内部循环的三个版本:
mult_jomega
示例。 operator*=
的调用已被计算所取代 mult_jomega_smart
示例 // 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
内联 x1
和x4
得到计算,尽管它们总是0.0,因为jomega.real()==0.0
我不知道为什么operator*=
doe snot内联。 源代码是直接的,仅包含三行。
当您认为对于double
类型的值, 0.0 * x == 0.0
并不总是为真时,可以解释x1
和x4
的计算。 除了在另一个答案中提到的带符号的零定义之外,还有无限值nan
和inf
,其中x * 0.0 = 0.0
不成立。
如果使用-fno-signed-zeros
和-ffinite-math-only
编译,则应用优化并删除x1
和x4
计算。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.