为什么向预定义Eigen向量写入迭代生成值会产生较高计算开销?
你猜测的返回值优化(RVO)影响可以直接排除:std::complex<double>是长度仅16字节的平凡标量类型,GCC会直接通过XMM寄存器传递返回值,不存在内存拷贝开销,RVO对这类标量返回的场景没有影响。
核心原因按概率从高到低排列如下:
1. 编译器死代码消除(最匹配你的测试数据)
你不存储calcPoint返回值时,GCC可通过静态分析判断calcPoint没有任何副作用:所有入参都是const修饰,函数内部没有修改全局变量、没有IO操作,返回值又被直接丢弃,因此编译器会直接优化掉整个calcPoint的函数调用,相当于第二个版本的for循环几乎是空跑。
结合你的耗时数据来看,0.0217秒大概率是程序其余逻辑的固定开销,第一个版本多出来的0.023秒才是calcPoint计算+结果写入的实际耗时,这就解释了为什么看似只多了一步写入,耗时却接近翻倍。
验证方法:给
calcPoint增加不可被优化的副作用,比如定义一个全局的std::atomic<long long> dummy_cnt,在calcPoint的循环内每次对计数器加1,再重新测试两个版本的耗时。如果此时两者的耗时差缩小到10%以内,即可确认是死代码消除导致的差距。
2. OpenMP伪共享(False Sharing)
如果通过上述验证排除了死代码消除的影响,那额外开销基本都来自多线程伪共享:Eigen::MatrixXcd的元素是连续存储的,每个std::complex<double>占16字节,常见CPU的缓存行大小为64字节,意味着4个连续的bfVec元素会落在同一个缓存行内。OpenMP多线程并行时,相邻迭代的线程会同时写入同一个缓存行的不同位置,触发缓存一致性协议的高频同步开销,导致大量缓存行失效、跨核心数据同步,这种开销远高于单个复数写入内存本身的开销。
验证方法:
- 关闭OpenMP跑单线程测试两个版本的耗时差,正常单线程下写入单个复数的开销不会超过总耗时的5%
- 多线程下给写入元素加缓存行对齐填充,比如把
bfVec改成存储struct { std::complex<double> val; char pad[48]; }的数组,再测试耗时,如果差距大幅缩小即可确认是伪共享问题。
优化建议
如果最终确认是伪共享问题,对于这种单迭代计算量不大的并行循环,可以调整OpenMP的调度策略为块调度,让每个线程处理连续的一大块迭代,减少不同线程写入相邻位置的概率;也可以直接改用Eigen内置的向量化并行操作,避免手动编写OpenMP引入的伪共享问题。
内容的提问来源于stack exchange,提问作者drakon101

