如何使用popcnt和SSE4.2在cpu上更快地计算数组的近似倒数平方根?
输入是存储在浮点数组中的正整数(范围从0到大约200,000)。
输出是浮点数组。
两个数组都有正确的内存对齐方式。
下面的代码只使用1 xmm寄存器,在linux上运行,可以由gcc -O3 code.cpp -lrt -msse4.2
编译
谢谢。
#include <iostream>
#include <emmintrin.h>
#include <time.h>
using namespace std;
void print_xmm(__m128 xmm){
float out[4];
_mm_storeu_ps(out,xmm);
int i;
for (i = 0; i < 4; ++i) std::cout << out[i] << " ";
std::cout << std::endl;
}
void print_arr(float* ptr, size_t size){
size_t i;
for(i = 0; i < size; ++i){
cout << ptr[i] << " ";
}
cout << endl;
}
int main(void){
size_t size = 25000 * 4;
// this has to be multiple of 4
size_t repeat = 10000;
// test 10000 cycles of the code
float* ar_in = (float*)aligned_alloc(16, size*sizeof(float));
float* ar_out = (float*)aligned_alloc(16, size*sizeof(float));
//fill test data into the input array
//the data is an array of positive numbers.
size_t i;
for (i = 0; i < size; ++i){
ar_in[i] = (i+1) * (i+1);
}
//prepare for recipical square root.
__m128 xmm0;
size_t size_fix = size*sizeof(float)/sizeof(__m128);
float* ar_in_end = ar_in + size_fix;
float* ar_out_now;
float* ar_in_now;
//timing
struct timespec tp_start, tp_end;
i = repeat;
clock_gettime(CLOCK_MONOTONIC, &tp_start);
//start timing
while(--i){
ar_out_now = ar_out;
for(ar_in_now = ar_in;
ar_in_now != ar_in_end;
ar_in_now += 4, ar_out_now+=4){
//4 = sizeof(__m128)/sizeof(float);
xmm0 = _mm_load_ps(ar_in_now);
//cout << "load xmm: ";
//print_xmm(xmm0);
xmm0 = _mm_rsqrt_ps(xmm0);
//cout << "rsqrt xmm: ";
//print_xmm(xmm0);
_mm_store_ps(ar_out_now,xmm0);
}
}
//end timing
clock_gettime(CLOCK_MONOTONIC, &tp_end);
double timing;
const double nano = 0.000000001;
timing = ((double)(tp_end.tv_sec - tp_start.tv_sec )
+ (tp_end.tv_nsec - tp_start.tv_nsec) * nano)/repeat;
cout << " timing per cycle: " << timing << endl;
/*
cout << "input array: ";
print_arr(ar_in, size);
cout << "output array: ";
print_arr(ar_out,size);
*/
//free mem
free(ar_in);
free(ar_out);
return 0;
}
答案 0 :(得分:4)
你的花车阵列有多大?如果它已经在L1(或者L2)中已经很热,那么这个代码的gcc5.3输出会影响现代Intel CPU上的uop吞吐量,因为它会产生一个循环,其中包含6个融合域uop,每次迭代执行一个向量。 (因此它将以每2个周期一个向量运行)。
要在现代Intel CPU上实现每个时钟吞吐量1个向量,需要展开循环(请参阅下文,了解未展开的asm无法正常工作的原因)。让编译器为你做这件事可能是好的(而不是在C ++源代码中手工完成)。例如使用配置文件引导优化(gcc -fprofile-use
),或者只是盲目地使用-funroll-loops
。
在Haswell或更高版本中,每个时钟加载和存储的16个字节只是L1缓存带宽的一半,因此您需要AVX版本才能达到最大每核心带宽。
如果你在内存上遇到瓶颈,you might want to do a Newton-Raphson iteration to get a nearly-full accuracy 1/sqrt(x)
,特别是如果你为一个大阵列使用多个线程。(因为如果一个线程无法维持一个加载+存储,那就没关系每个时钟。)
或者稍后在加载此数据时可能只是动态使用rsqrt
。它的非常便宜,具有高吞吐量,但仍然延迟类似于FP添加。同样,如果它是一个不适合缓存的大数组,那么通过对数据进行较少的单独传递来提高计算强度是一件大事。 (Cache Blocking aka Loop Tiling也是一个好主意,如果你可以这样做:在一个缓存大小的数据块上运行算法的多个步骤。)
如果找不到有效使用缓存的方法,请仅使用缓存绕过NT存储作为最后的手段。如果你可以转换一些你将要使用的数据,那就更好了,所以当它下次使用时它会在缓存中。
对于Intel SnB系列CPU,主循环(来自.L31
to jne .L31
on the Godbolt compiler explorer)为6 uops,因为indexed addressing modes don't micro-fuse。 (不幸的是,Agner Fog's microarch pdf尚未记录这一点。)
在Nehalem上有4个融合域uop,只有三个ALU uop,所以Nehalem应该每时钟运行一次。
.L31: # the main loop: 6 uops on SnB-family, 4 uops on Nehalem
rsqrtps xmm0, XMMWORD PTR [rbx+rax] # tmp127, MEM[base: ar_in_now_10, index: ivtmp.51_61, offset: 0B]
movaps XMMWORD PTR [rbp+0+rax], xmm0 # MEM[base: ar_out_now_12, index: ivtmp.51_61, offset: 0B], tmp127
add rax, 16 # ivtmp.51,
cmp rax, 100000 # ivtmp.51,
jne .L31 #,
由于你想要写一个单独的目的地,所以没有办法让循环到4个融合域的uops,所以它可以在每个时钟运行一个向量而不展开。 (加载和存储都需要是单寄存器寻址模式,因此使用由src - dst
索引的current_dst
而不是递增src
的技巧不起作用。)
修改你的C ++所以gcc会使用指针增量只能保存一个uop,因为你必须增加src和dst。即float *endp = start + length;
和for (p = start ; p < endp ; p+=4) {}
会形成像
.loop:
rsqrtps xmm0, [rsi]
add rsi, 16
movaps [rdi], xmm0
add rdi, 16
cmp rdi, rbx
jne .loop
希望gcc在展开时会做这样的事情,否则rsqrtps
+ movaps
如果仍然使用索引寻址模式,则会自己4个融合域uops ,并且没有任何数量的展开会使你的循环每个时钟以一个向量运行。
答案 1 :(得分:3)
因为这是一个算术强度非常低的流式计算,所以几乎可以肯定是内存限制。如果您使用非临时加载和存储,您可能会发现可测量的加速。
userType
修改强>:
我运行了一些测试,看看在不同的硬件上看起来是什么样的。不出所料,结果对使用的硬件非常敏感。我认为这可能是由于现代CPU中读/写缓冲区数量的增加。
所有代码都是使用gcc-6.1和
编译的<bean parent="transactional-cache">
<property name="name" value="userType"/>
</bean>
Intel Core i3-3120M @ 2.5GHz; 3MB缓存
{{1}}
Intel Core i7-6500U CPU @ 2.50GHz; 4MB缓存
{{1}}
将问题大小增加到2MB,NTA Prefetch + NTA存储的运行时间比OP的解决方案减少了约30%。
结果:问题规模太小,无法从NTA中获益。在较旧的架构上,这是有害的。在较新的架构上,它与OP的解决方案相同。
结论:在这种情况下可能不值得付出额外的努力。
答案 2 :(得分:1)
当然,你必须测量它,但是有已知的代码来计算(不是非常精确)反平方根,检查https://www.beyond3d.com/content/articles/8/
float InvSqrt (float x) {
float xhalf = 0.5f*x;
int i = *(int*)&x;
i = 0x5f3759df - (i>>1);
x = *(float*)&i;
x = x*(1.5f - xhalf*x*x);
return x;
}
经过测试(使用VS2015和GCC 5.4.0)转换为SSE2,link
__m128 InvSqrtSSE2(__m128 x) {
__m128 xhalf = _mm_mul_ps(x, _mm_set1_ps(0.5f));
x = _mm_castsi128_ps(_mm_sub_epi32(_mm_set1_epi32(0x5f3759df), _mm_srai_epi32(_mm_castps_si128(x), 1)));
return _mm_mul_ps(x, _mm_sub_ps(_mm_set1_ps(1.5f), _mm_mul_ps(xhalf, _mm_mul_ps(x, x))));
}
更新我
Mea culpa!感谢@EOF,他注意到转换不正确,将其替换为强制转换