Z b*_*son 31 c++ x86 assembly gcc visual-c++
我发现在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]
vmulps ymm9, ymm0, ymm9
vaddps ymm7, ymm7, ymm9
vmovups ymm9, YMMWORD PTR [rax-192]
vmulps ymm9, ymm0, ymm9
vaddps ymm6, ymm6, ymm9
vmovups ymm9, YMMWORD PTR [rax-160]
vmulps ymm9, ymm0, ymm9
vaddps ymm5, ymm5, ymm9
vmovups ymm9, YMMWORD PTR [rax-128]
vmulps ymm9, ymm0, ymm9
vaddps ymm4, ymm4, ymm9
vmovups ymm9, YMMWORD PTR [rax-96]
vmulps ymm9, ymm0, ymm9
vaddps ymm3, ymm3, ymm9
vmovups ymm9, YMMWORD PTR [rax-64]
vmulps ymm9, ymm0, ymm9
vaddps ymm2, ymm2, ymm9
vmovups ymm9, YMMWORD PTR [rax-32]
cmp esi, edx
vmulps ymm0, ymm0, ymm9
vaddps ymm1, ymm1, ymm0
jg .L4
Run Code Online (Sandbox Code Playgroud)
MSVC/FAc/O2/openmp/arch:AVX ......
vbroadcastss ymm2, DWORD PTR [r10]
lea rax, QWORD PTR [rax+256]
lea r10, QWORD PTR [r10+4]
vmulps ymm1, ymm2, YMMWORD PTR [rax-320]
vaddps ymm3, ymm1, ymm3
vmulps ymm1, ymm2, YMMWORD PTR [rax-288]
vaddps ymm4, ymm1, ymm4
vmulps ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps ymm5, ymm1, ymm5
vmulps ymm1, ymm2, YMMWORD PTR [rax-224]
vaddps ymm6, ymm1, ymm6
vmulps ymm1, ymm2, YMMWORD PTR [rax-192]
vaddps ymm7, ymm1, ymm7
vmulps ymm1, ymm2, YMMWORD PTR [rax-160]
vaddps ymm8, ymm1, ymm8
vmulps ymm1, ymm2, YMMWORD PTR [rax-128]
vaddps ymm9, ymm1, ymm9
vmulps ymm1, ymm2, YMMWORD PTR [rax-96]
vaddps ymm10, ymm1, ymm10
dec rdx
jne SHORT $LL3@AddDot4x4_
Run Code Online (Sandbox Code Playgroud)
编辑:
我通过计算总浮点运算来对代码进行基准测试,2.0*n^3
其中n是方阵的宽度,除以测量的时间omp_get_wtime()
.我重复循环几次.在下面的输出中,我重复了100次.
所有内核的Intel Xeon E5 1620(Ivy Bridge)turbo上的MSVC2012输出为3.7 GHz
maximum GFLOPS = 236.8 = (8-wide SIMD) * (1 AVX mult + 1 AVX add) * (4 cores) * 3.7 GHz
n 64, 0.02 ms, GFLOPs 0.001, GFLOPs/s 23.88, error 0.000e+000, efficiency/core 40.34%, efficiency 10.08%, mem 0.05 MB
n 128, 0.05 ms, GFLOPs 0.004, GFLOPs/s 84.54, error 0.000e+000, efficiency/core 142.81%, efficiency 35.70%, mem 0.19 MB
n 192, 0.17 ms, GFLOPs 0.014, GFLOPs/s 85.45, error 0.000e+000, efficiency/core 144.34%, efficiency 36.09%, mem 0.42 MB
n 256, 0.29 ms, GFLOPs 0.034, GFLOPs/s 114.48, error 0.000e+000, efficiency/core 193.37%, efficiency 48.34%, mem 0.75 MB
n 320, 0.59 ms, GFLOPs 0.066, GFLOPs/s 110.50, error 0.000e+000, efficiency/core 186.66%, efficiency 46.67%, mem 1.17 MB
n 384, 1.39 ms, GFLOPs 0.113, GFLOPs/s 81.39, error 0.000e+000, efficiency/core 137.48%, efficiency 34.37%, mem 1.69 MB
n 448, 3.27 ms, GFLOPs 0.180, GFLOPs/s 55.01, error 0.000e+000, efficiency/core 92.92%, efficiency 23.23%, mem 2.30 MB
n 512, 3.60 ms, GFLOPs 0.268, GFLOPs/s 74.63, error 0.000e+000, efficiency/core 126.07%, efficiency 31.52%, mem 3.00 MB
n 576, 3.93 ms, GFLOPs 0.382, GFLOPs/s 97.24, error 0.000e+000, efficiency/core 164.26%, efficiency 41.07%, mem 3.80 MB
n 640, 5.21 ms, GFLOPs 0.524, GFLOPs/s 100.60, error 0.000e+000, efficiency/core 169.93%, efficiency 42.48%, mem 4.69 MB
n 704, 6.73 ms, GFLOPs 0.698, GFLOPs/s 103.63, error 0.000e+000, efficiency/core 175.04%, efficiency 43.76%, mem 5.67 MB
n 768, 8.55 ms, GFLOPs 0.906, GFLOPs/s 105.95, error 0.000e+000, efficiency/core 178.98%, efficiency 44.74%, mem 6.75 MB
n 832, 10.89 ms, GFLOPs 1.152, GFLOPs/s 105.76, error 0.000e+000, efficiency/core 178.65%, efficiency 44.66%, mem 7.92 MB
n 896, 13.26 ms, GFLOPs 1.439, GFLOPs/s 108.48, error 0.000e+000, efficiency/core 183.25%, efficiency 45.81%, mem 9.19 MB
n 960, 16.36 ms, GFLOPs 1.769, GFLOPs/s 108.16, error 0.000e+000, efficiency/core 182.70%, efficiency 45.67%, mem 10.55 MB
n 1024, 17.74 ms, GFLOPs 2.147, GFLOPs/s 121.05, error 0.000e+000, efficiency/core 204.47%, efficiency 51.12%, mem 12.00 MB
Run Code Online (Sandbox Code Playgroud)
iwo*_*olf 21
由于我们已经涵盖了对齐问题,我猜是这样的:http://en.wikipedia.org/wiki/Out-of-order_execution
由于g ++发出独立的加载指令,因此您的处理器可以对指令进行重新排序,以便在添加和乘法时预取下一个需要的数据.MSVC向mul抛出指针使得load和mul绑定到同一条指令,因此更改指令的执行顺序对任何事情都无济于事.
编辑:今天所有文档的英特尔服务器都不那么生气了,所以这里有更多研究为什么乱序执行是(部分)答案.
首先,看起来你的评论是完全正确的,因为MSVC版本的乘法指令可以解码以分离可以由CPU的乱序引擎优化的μ-ops.这里有趣的部分是现代微码序列发生器是可编程的,因此实际行为依赖于硬件和固件.生成的程序集的差异似乎来自GCC和MSVC各自试图对抗不同的潜在瓶颈.GCC版本试图为无序引擎提供余地(正如我们已经介绍过的那样).然而,MSVC版本最终利用了一种称为"微操作融合"的功能.这是因为μ-op退休限制.管道的末端每个滴答只能退出3μ-ops.在特定情况下,微操作融合需要两个μ-ops,必须在两个不同的执行单元(即存储器读取和算术)上完成,并将它们连接到大多数流水线的单个μ-op.融合的μ-op仅在执行单元分配之前被分成两个实际的μ-op.执行后,ops再次融合,允许它们作为一个退役.
无序引擎只能看到融合的μ-op,因此它无法将负载op拉离乘法.这会导致管道在等待下一个操作数完成其公共汽车时挂起.
所有链接!!!:http: //download-software.intel.com/sites/default/files/managed/71/2e/319433-017.pdf
http://www.agner.org/optimize/microarchitecture.pdf
http://www.agner.org/optimize/optimizing_assembly.pdf
http://www.agner.org/optimize/instruction_tables.ods (注意:Excel抱怨此电子表格部分损坏或粗略,因此请自行承担风险.但它似乎并不是恶意的,并且根据在我的研究的其余部分,Agner Fog非常棒.在我选择了Excel恢复步骤之后,我发现它充满了大量的数据.
http://www.syncfusion.com/Content/downloads/ebook/Assembly_Language_Succinctly.pdf
很多编辑:哇,这里的讨论有一些有趣的更新.我想我误解了微操作融合实际影响了多少管道.也许从循环条件检查的差异中可以获得比预期更多的性能增益,其中未融合指令允许GCC将比较和跳转与最后的矢量加载和算术步骤交错?
vmovups ymm9, YMMWORD PTR [rax-32]
cmp esi, edx
vmulps ymm0, ymm0, ymm9
vaddps ymm1, ymm1, ymm0
jg .L4
Run Code Online (Sandbox Code Playgroud)
我可以确认在Visual Studio中使用GCC代码确实提高了性能.我是通过将Linux中的GCC目标文件转换为在Visual Studio中工作来实现的.使用所有四个核心(和单个核心的60%到70%),效率从50%提高到60%.
微软已经从64位代码中删除了内联汇编,并且还破坏了它们的64位分解器,因此代码无法在不进行修改的情况下完成(但32位版本仍可正常工作).他们显然认为内在就足够了,但这个案例表明他们错了.
也许融合指令应该是独立的内在函数?
但微软并不是唯一一个产生不太优化的内在代码的微软.如果您将以下代码放入http://gcc.godbolt.org/,您可以看到Clang,ICC和GCC的作用. ICC的表现甚至比MSVC还差. 它正在使用,vinsertf128
但我不知道为什么.我不确定Clang正在做什么,但它看起来更接近GCC只是以不同的顺序(和更多的代码).
这就解释了为什么Agner Fog在他的手册" 用汇编语言优化子程序 "中写到了" 使用内部函数的缺点":
编译器可以修改代码或以低于程序员预期的方式实现代码.可能有必要查看编译器生成的代码,以查看它是否按程序员的预期方式进行了优化.
对于使用内在函数的情况,这是令人失望的.这意味着要么仍然要编写64位汇编代码soemtimes,要么找到一个编译器,它按照程序员的意图实现内在函数.在这种情况下,只有GCC似乎这样做(也许是Clang).
#include <immintrin.h>
extern "C" void AddDot4x4_vec_block_8wide(const int n, const float *a, const float *b, float *c, const int stridea, const int strideb, const int stridec) {
const int vec_size = 8;
__m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
tmp7 = _mm256_loadu_ps(&c[7*vec_size]);
for(int i=0; i<n; i++) {
__m256 areg0 = _mm256_set1_ps(a[i]);
__m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);
__m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
__m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);
__m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);
__m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);
__m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);
__m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);
__m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);
}
_mm256_storeu_ps(&c[0*vec_size], tmp0);
_mm256_storeu_ps(&c[1*vec_size], tmp1);
_mm256_storeu_ps(&c[2*vec_size], tmp2);
_mm256_storeu_ps(&c[3*vec_size], tmp3);
_mm256_storeu_ps(&c[4*vec_size], tmp4);
_mm256_storeu_ps(&c[5*vec_size], tmp5);
_mm256_storeu_ps(&c[6*vec_size], tmp6);
_mm256_storeu_ps(&c[7*vec_size], tmp7);
}
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
20743 次 |
最近记录: |