用于高度优化的matrix复制代码的MSVC和GCC之间的性能差异

在MSVC(在Windows)和GCC(在Linux上)编译的代码中,Ivy Bridge系统的性能差异很大。 该代码做密集matrix乘法。 我得到GCC峰值的70%,MSVC只有50%。 我想我可能已经把它们的差异分离出来了,它们是如何将以下三个内在因素进行转换的。

__m256 breg0 = _mm256_loadu_ps(&b[8*i]) _mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0) 

GCC做到这一点

 vmovups ymm9, YMMWORD PTR [rax-256] vmulps ymm9, ymm0, ymm9 vaddps ymm8, ymm8, ymm9 

MSVC做到这一点

 vmulps ymm1, ymm2, YMMWORD PTR [rax-256] vaddps ymm3, ymm1, ymm3 

请问有人可以解释一下,为什么这两个解决scheme可以在性能上有如此大的差异呢?

尽pipeMSVC使用一个较less的指令,它将负载连接到mult,也许这使得它更依赖(也许负载不能乱序)? 我的意思是常春藤桥可以在一个时钟周期内完成一个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 

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_ 

编辑:

我通过将总浮点运算分解为2.0*n^3对代码进行基准testing,其中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 

由于我们已经涵盖了alignment问题,我猜想是这样的: http : //en.wikipedia.org/wiki/Out-of-order_execution

因为g ++发出一个独立的加载指令,所以你的处理器可以重新sorting指令,以预取下一个需要的数据,同时也可以进行加和乘。 MSVC在mul上抛出一个指针使得负载和mul绑定到相同的指令,所以改变指令的执行顺序对任何事情都没有帮助。

编辑:英特尔的服务器(S)与所有的文档是不是今天生气,所以这里有更多的研究,为什么乱序执行(的一部分)的答案。

首先,看起来您的评论是完全正确的,因为乘法指令的MSVC版本可能会解码以分离μ-ops,这些μ-ops可以通过CPU的无序引擎进行优化。 这里有趣的部分是现代的微码定序器是可编程的,所以实际的行为是硬件和固件的依赖。 生成的程序集中的差异似乎来自GCC和MSVC,每个尝试对抗不同的潜在瓶颈。 海湾合作委员会的版本试图给顺序引擎的余地(我们已经覆盖)。 但是,MSVC版本最终利用了一个称为“微操作融合”的特性。 这是因为μ-op退休限制。 stream水线的结束只能延迟3微秒。 在特定情况下,微操作融合需要两个μ-ops, 必须在两个不同的执行单元(即存储器读取和算术)上完成,并将它们连接到大多数stream水线的单个μ-op。 在执行单元分配之前,融合的μ-op仅被分成两个实际的μ-op。 执行后,操作再次融合,让他们作为一个退休。

无序引擎只能看到融合的μ-op,所以它不能将负载运算从乘法中拉开。 这会导致pipe道挂起,等待下一个操作数完成其总线。

ALL THE LINKS !!!: http : //download-software.intel.com/sites/default/files/managed/71/2e/319433-017.pdf

UTF-8''248966_software_optimization_manual.pdf

optimize/microarchitecture.pdf

optimize/optimizing_assembly.pdf

http://www.agner.org/optimize/instruction_tables.ods (注意:Excel抱怨说这个电子表格部分损坏或者是粗略的,所以要自己承担风险,这似乎并不是恶意的,在我的研究中,Agner Fog非常棒,在selectExcel恢复步骤之后,我发现它充满了大量的数据)

http://cs.nyu.edu/courses/fall13/CSCI-GA.3033-008/Microprocessor-Report-Sandy-Bridge-Spans-Generations-243901.pdf

http://www.syncfusion.com/Content/downloads/ebook/Assembly_Language_Succinctly.pdf


很多更新:哇,这里的讨论有一些有趣的更新。 我想我误解了多lesspipe道实际上受微操作融合的影响。 也许有更多的性能比我预期的循环条件检查的差异,其中未融合指令允许海湾合作委员会交错比较和跳转与最后一个vector负载和算术步骤?

 vmovups ymm9, YMMWORD PTR [rax-32] cmp esi, edx vmulps ymm0, ymm0, ymm9 vaddps ymm1, ymm1, ymm0 jg .L4 

我可以确认在Visual Studio中使用GCC代码确实可以提高性能。 我通过在Linux中转换GCC目标文件以在Visual Studio中工作 。 使用全部四个核心的效率从50%提高到60%(单个核心的效率从60%提高到70%)。

微软已经从64位代码中删除了内联程序集,并且还破坏了他们的64位拆装程序,这样代码就无法修改 ( 但是32位版本仍然有效 )。 他们显然认为内在性是足够的,但是正如这个案例表明他们错了。

也许融合的说明应该是单独的内在?

但是微软并不是唯一一个产生较less内在代码的人。 如果你把下面的代码放到http://gcc.godbolt.org/中,你可以看到Clang,ICC和GCC做了什么。; ICC的performance甚至比MSVC更差。 它使用vinsertf128但我不知道为什么。 我不确定Clang在做什么,但它看起来更接近于GCC,只是按照不同的顺序(和更多的代码)。

这就解释了为什么Agner Fog在他的手册“ 在汇编语言中优化子程序 ”中写道:“使用内部函数的缺点”:

编译器可以修改代码或者以比程序员想要的效率更低的方式实现代码。 查看编译器生成的代码可能是必要的,以查看它是否按照程序员的意图进行了优化。

使用内在函数的情况令人失望。 这意味着要么必须编写64位的汇编代码,要么find一个编译器来实现程序员所期望的内部函数。 在这种情况下,只有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); } 

MSVC正是你所要求的。 如果您想要发出vmovups指令,请使用_mm256_loadu_ps内部函数。