我试图在L1缓存中获得全部带宽,以便在Intel处理器上实现以下功能
float triad(float *x, float *y, float *z, const int n) {
float k = 3.14159f;
for(int i=0; i<n; i++) {
z[i] = x[i] + k*y[i];
}
}
Run Code Online (Sandbox Code Playgroud)
这是STREAM的三合一功能.
使用具有此功能的SandyBridge/IvyBridge处理器可获得约95%的峰值(使用NASM组装).但是,除非我展开循环,否则使用Haswell I仅达到峰值的62%.如果我展开16次,我得到92%.我不明白这一点.
我决定使用NASM在汇编中编写我的函数.装配中的主循环看起来像这样.
.L2:
vmovaps ymm1, [rdi+rax]
vfmadd231ps ymm1, ymm2, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
Run Code Online (Sandbox Code Playgroud)
在示例12.7-12.11 中的Agner Fog的优化组装手册中,他y[i] = y[i] +k*x[i]对Pentium M,Core 2,Sandy Bridge,FMA4和FMA3 做了几乎相同的事情(但是).我设法或多或少地自己重现了他的代码(实际上他在广播时在FMA3示例中有一个小错误).除FMA4和FMA3外,他为每个处理器的表格提供指令大小计数,融合操作,执行端口.我曾试图为FMA3制作这张桌子.
ports
size ?ops-fused 0 1 2 3 4 5 6 7
vmovaps 5 1 ½ ½ …Run Code Online (Sandbox Code Playgroud) 我发现在MSVC(在Windows上)和GCC(在Linux上)为Ivy Bridge系统编译的代码之间的性能差异很大.代码执行密集矩阵乘法.我使用GCC获得了70%的峰值失误,而MSVC只获得了50%.我想我可能已经把他们两个内在函数如何转换的差异分开了.
__m256 breg0 = _mm256_loadu_ps(&b[8*i])
_mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0)
Run Code Online (Sandbox Code Playgroud)
GCC这样做
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
Run Code Online (Sandbox Code Playgroud)
MSVC这样做
vmulps ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps ymm3, ymm1, ymm3
Run Code Online (Sandbox Code Playgroud)
有人可以向我解释这两种解决方案是否以及为何能够在性能上产生如此大的差异?
尽管MSVC使用少一条指令,但它会将负载与多线程联系起来,这可能会使它更加依赖(也许负载无法按顺序完成)?我的意思是Ivy Bridge可以在一个时钟周期内完成一个AVX加载,一个AVX mult和一个AVX加载,但这要求每个操作都是独立的.
也许问题出在其他地方?您可以在下面看到最里面循环的GCC和MSVC的完整汇编代码.你可以在这里看到循环的C++代码循环展开以实现Ivy Bridge和Haswell的最大吞吐量
g ++ -S -masm = intel matrix.cpp -O3 -mavx -fopenmp
.L4:
vbroadcastss ymm0, DWORD PTR [rcx+rdx*4]
add rdx, 1
add rax, 256
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
vmovups ymm9, YMMWORD PTR [rax-224] …Run Code Online (Sandbox Code Playgroud)