记录一个最近在写 C++/SIMD 向量化运算时遇到的小坑。
问题背景是最近在对一个线上项目的代码进行性能优化。优化的思路是使用 simd 对连续内存变量的数学运算进行加速。 这里只保留了代码的主干部分。
待处理的数据类型是vector
大体代码如下:
// ptr is address of std::vertor<u_int_16>
for (int index = 0; index + 8 < length; index = index + 8) {
__m128i mm_frame = _mm_loadu_si128((__m128i*)(ptr));
__m128i mm_frame2 = _mm_add_epi16(mm_frame, mm_shift); // add and create
u_int16_t* res_address = (u_int16_t*)(&mm_frame_2); // get address of __m128i
for (int k = 0; k < 8; k++) {
u_int16_t res = res_address[k];
... // some other operation with res
auto x=*(ptr + k); // Unused code !!!
}
... // some other code
}
问题出在 __m128i 取地址那一行。测试中发现 mm_frame_2 的值和 res_address 的值并不相等。(报错时 res_address 为 0。) 但是,如果在循环体内部加上一条废代码 auto x=*(ptr + k) ,结果就变得正常了。 更神奇的是,这条代码也可以换成其他代码,例如打印出k的值(cout « k « endl),结果依然是正常的。 但如果什么都不加,结果是错的。
一条废代码到底影响了什么呢?逐层打印每个变量,结果都是正常。使用 gdb 调试进到代码内部看,结果依然正常。 难道这就是传说中的观测影响状态(测不准)??看上去似乎和编译选项有关,测了一下 -Debug 下正常,-O3 有时报错有时正常。
第一个想法是取地址操作对变量有影响,但打印了对应变量,以及将该语句替换成其他不相关语句结果不变,否定了这个猜测。 第二个想法是编译器在编译过程中,mm_frame2 创建和下一行取地址两行的操作顺序颠倒了。 毕竟 simd 和 普通的 cpu 指令运行机理有些差别。但在使用 objdump -d 查看了可执行文件的反汇编结果后,否定了这个猜测。 最终在查找了相当多资料后,终于定位到了原因。简单来讲,寄存器内的值(右值)不能取地址。详细来讲,编译器的 -Ofast/-O3 优化, 一定概率下会把 m128i 的结果直接放到寄存器内以加速。这时直接对结果取地址是非法的,(但不知道为什么没报错)。 但是,如果编译选项不同,或者增加一些看似不相关的语句,编译器可能会把变量移到栈上,从而得到正确结果。 总体而言,是编译器过度优化的结果(不过好像在非向量化代码中没遇到类似的问题)。
查到了问题,解决也就变得相对简单了。采用了等价但可读性更好的写法来规避这个问题:
union src_u {
__m128i v1;
uint16_t v2[8];
};
src_u mm_frame;
for (int index = 0; index + 8 < length; index = index + 8) {
mm_frame.v1 = _mm_loadu_si128((__m128i*)(ptr));
mm_frame.v1 = _mm_add_epi16(mm_frame.v1, mm_shift);
for (int k = 0; k < 8; k++) {
u_int16_t vote_frame = mm_frame.v2[k];
... // some other operation with res
}
... // some other code
}
使用联合体,对同一块内存赋予不同的数据类型,直接取值而非地址。
c++真的是稍不注意就要踩坑。😢😢😢