在我们的代码库中,我们有很多操作,如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
答案 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.0
或0.0*x
等表达式(即使使用-ffinite-math-only
)。 此选项意味着零结果的符号不重要。
答案 1 :(得分:1)
我对使用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*=
会吸引内幕消息。源代码是直接的,仅包含三行。
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
.除了在另一个答案中提到的已签名零定义之外,还有无限值nan
和inf
x * 0.0 = 0.0
不存在。
如果使用-fno-signed-zeros
和-ffinite-math-only
进行编译,则应用优化并删除x1
和x4
的计算。
答案 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 _Imaginary
,double _Imaginary
和long 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的运算符。