为什么 mulss 在 Haswell 上只需要 3 个周期,与 Agner 的指令表不同? (展开具有多个累加器的 FP 循环)

Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators)

我是指令优化的新手。

我对一个简单的函数 dotp 进行了简单的分析,该函数用于获取两个 float 数组的点积。

C代码如下:

float dotp(               
    const float  x[],   
    const float  y[],     
    const short  n      
)
{
    short i;
    float suma;
    suma = 0.0f;

    for(i=0; i<n; i++) 
    {    
        suma += x[i] * y[i];
    } 
    return suma;
}

我使用网上Agner Fog提供的测试框架testp

本例中使用的数组是对齐的:

int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);

float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;

然后调用函数dotp, n=2048, repeat=100000:

 for (i = 0; i < repeat; i++)
 {
     sum = dotp(x,y,n);
 }

我用 gcc 4.8.3 编译它,编译选项为 -O3。

我在不支持FMA指令的电脑上编译这个应用程序,所以你可以看到只有SSE指令。

汇编代码:

.L13:
        movss   xmm1, DWORD PTR [rdi+rax*4]  
        mulss   xmm1, DWORD PTR [rsi+rax*4]   
        add     rax, 1                       
        cmp     cx, ax
        addss   xmm0, xmm1
        jg      .L13

我做一些分析:

          μops-fused  la    0    1    2    3    4    5    6    7    
movss       1          3             0.5  0.5
mulss       1          5   0.5  0.5  0.5  0.5
add         1          1   0.25 0.25               0.25   0.25 
cmp         1          1   0.25 0.25               0.25   0.25
addss       1          3         1              
jg          1          1                                   1                                                   -----------------------------------------------------------------------------
total       6          5    1    2     1     1      0.5   1.5

经过运行ning,得到结果:

   Clock  |  Core cyc |  Instruct |   BrTaken | uop p0   | uop p1      
--------------------------------------------------------------------
542177906 |609942404  |1230100389 |205000027  |261069369 |205511063 
--------------------------------------------------------------------  
   2.64   |  2.97     | 6.00      |     1     | 1.27     |  1.00   

   uop p2   |    uop p3   |  uop p4 |    uop p5  |  uop p6    |  uop p7       
-----------------------------------------------------------------------   
 205185258  |  205188997  | 100833  |  245370353 |  313581694 |  844  
-----------------------------------------------------------------------          
    1.00    |   1.00      | 0.00    |   1.19     |  1.52      |  0.00           

第二行是从Intel寄存器读取的值;第三行除以分支号,"BrTaken".

所以我们可以看到,循环中有6条指令,7微秒,符合分析。

port0 port1 port 5 port6中的uops个数运行和分析说的差不多。我想也许 uops 调度程序会这样做,它可能会尝试平衡端口上的负载,对吗?

我完全不明白为什么每个循环只有大约3个周期。根据 Agner 的 instruction table,指令 mulss 的延迟是 5,并且循环之间存在依赖关系,所以据我所知每个循环至少需要 5 个周期。

任何人都可以分享一些见解吗?

============================================= =====================

我尝试在 nasm 中编写此函数的优化版本,将循环展开 8 倍并使用 vfmadd231ps 指令:

.L2:
    vmovaps         ymm1, [rdi+rax]             
    vfmadd231ps     ymm0, ymm1, [rsi+rax]       

    vmovaps         ymm2, [rdi+rax+32]          
    vfmadd231ps     ymm3, ymm2, [rsi+rax+32]    

    vmovaps         ymm4, [rdi+rax+64]          
    vfmadd231ps     ymm5, ymm4, [rsi+rax+64]    

    vmovaps         ymm6, [rdi+rax+96]          
    vfmadd231ps     ymm7, ymm6, [rsi+rax+96]   

    vmovaps         ymm8, [rdi+rax+128]         
    vfmadd231ps     ymm9, ymm8, [rsi+rax+128]  

    vmovaps         ymm10, [rdi+rax+160]               
    vfmadd231ps     ymm11, ymm10, [rsi+rax+160] 

    vmovaps         ymm12, [rdi+rax+192]                
    vfmadd231ps     ymm13, ymm12, [rsi+rax+192] 

    vmovaps         ymm14, [rdi+rax+224]                
    vfmadd231ps     ymm15, ymm14, [rsi+rax+224] 
    add             rax, 256                    
    jne             .L2

结果:

  Clock   | Core cyc |  Instruct  |  BrTaken  |  uop p0   |   uop p1  
------------------------------------------------------------------------
 24371315 |  27477805|   59400061 |   3200001 |  14679543 |  11011601  
------------------------------------------------------------------------
    7.62  |     8.59 |  18.56     |     1     | 4.59      |     3.44


   uop p2  | uop p3  |  uop p4  |   uop p5  |   uop p6   |  uop p7  
-------------------------------------------------------------------------
 25960380  |26000252 |  47      |  537      |   3301043  |  10          
------------------------------------------------------------------------------
    8.11   |8.13     |  0.00    |   0.00    |   1.03     |  0.00        

所以我们可以看到L1数据缓存达到2*256bit/8.59,已经非常接近峰值2*256/8,使用率约为93%,FMA单元只使用了8/8.59,峰值为2*8/8,使用率为47%。

所以我认为我已经达到了 Peter Cordes 预期的 L1D 瓶颈。

============================================= =====================

特别感谢Boann,修正了我问题中的很多语法错误。

============================================= ====================

从Peter的回复中得知只有"read and written"寄存器是依赖,"writer-only"寄存器不是依赖

所以我尝试减少循环中使用的寄存器,并尝试展开5,如果一切正常,我应该会遇到同样的瓶颈,L1D。

.L2:
    vmovaps         ymm0, [rdi+rax]    
    vfmadd231ps     ymm1, ymm0, [rsi+rax]    

    vmovaps         ymm0, [rdi+rax+32]    
    vfmadd231ps     ymm2, ymm0, [rsi+rax+32]   

    vmovaps         ymm0, [rdi+rax+64]    
    vfmadd231ps     ymm3, ymm0, [rsi+rax+64]   

    vmovaps         ymm0, [rdi+rax+96]    
    vfmadd231ps     ymm4, ymm0, [rsi+rax+96]   

    vmovaps         ymm0, [rdi+rax+128]    
    vfmadd231ps     ymm5, ymm0, [rsi+rax+128]   

    add             rax, 160                    ;n = n+32
    jne             .L2 

结果:

    Clock  | Core cyc  | Instruct  |  BrTaken |    uop p0  |   uop p1  
------------------------------------------------------------------------  
  25332590 |  28547345 |  63700051 |  5100001 |   14951738 |  10549694   
------------------------------------------------------------------------
    4.97   |  5.60     | 12.49     |    1     |     2.93   |    2.07    

    uop p2  |uop p3   | uop p4 | uop p5 |uop p6   |  uop p7 
------------------------------------------------------------------------------  
  25900132  |25900132 |   50   |  683   | 5400909 |     9  
-------------------------------------------------------------------------------     
    5.08    |5.08     |  0.00  |  0.00  |1.06     |     0.00    

我们可以看到5/5.60 = 89.45%,比urolling小了8,有什么问题吗?

============================================= ====================

我尝试按 6、7 和 15 展开循环,以查看结果。 我也再次展开 5 和 8,以双重确认结果。

结果如下,可以看到这次的效果比之前好很多

虽然结果不稳定,但展开因子越大,结果越好。

            | L1D bandwidth     |  CodeMiss | L1D Miss | L2 Miss 
----------------------------------------------------------------------------
  unroll5   | 91.86% ~ 91.94%   |   3~33    | 272~888  | 17~223
--------------------------------------------------------------------------
  unroll6   | 92.93% ~ 93.00%   |   4~30    | 481~1432 | 26~213
--------------------------------------------------------------------------
  unroll7   | 92.29% ~ 92.65%   |   5~28    | 336~1736 | 14~257
--------------------------------------------------------------------------
  unroll8   | 95.10% ~ 97.68%   |   4~23    | 363~780  | 42~132
--------------------------------------------------------------------------
  unroll15  | 97.95% ~ 98.16%   |   5~28    | 651~1295 | 29~68

============================================= ========================

我尝试在 web "https://gcc.godbolt.org"

中用 gcc 7.1 编译函数

编译选项为“-O3 -march=haswell -mtune=intel”,类似于gcc 4.8.3。

.L3:
        vmovss  xmm1, DWORD PTR [rdi+rax]
        vfmadd231ss     xmm0, xmm1, DWORD PTR [rsi+rax]
        add     rax, 4
        cmp     rdx, rax
        jne     .L3
        ret

相关:

  • 有一个很好的 manually-vectorized dot-product 循环,使用具有 FMA 内在函数的多个累加器。答案的其余部分用 cpu-architecture / asm 详细信息解释了为什么这是一件好事。
  • 表明使用正确的编译器选项,一些编译器会 auto-vectorize 这样。
  • Loop unrolling to achieve maximum throughput with Ivy Bridge and Haswell 此问答的另一个版本更侧重于展开以隐藏延迟(和吞吐量瓶颈),更少的背景知识甚至意味着什么。以及使用 C 内在函数的示例。
  • - 关于依赖链的教科书练习,有两个互锁链,一个是从前面读到另一个。

再次查看您的循环:movss xmm1, src 不依赖于 xmm1 的旧值,因为它的目的地是 write-only。每次迭代的 mulss 都是独立的。 Out-of-order 执行可以并且确实利用了 instruction-level 并行性,因此您绝对不会成为 mulss 延迟的瓶颈。

可选阅读:在计算机体系结构术语中:寄存器重命名避免了 WAR anti-dependency data hazard 重复使用相同的体系结构寄存器。 (有些流水线+寄存器重命名前的dependency-tracking方案并没有解决所有的问题,所以计算机体系结构领域对各种数据危害大做文章

注册重命名 Tomasulo's algorithm makes everything go away except the actual true dependencies (read after write), so any instruction where the destination is not also a source register has no interaction with the dependency chain involving the old value of that register. (Except for false dependencies, like popcnt on Intel CPUs, and writing only part of a register without clearing the rest (like mov al, 5 or sqrtss xmm2, xmm1). Related: Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?).


返回您的代码:

.L13:
    movss   xmm1, DWORD PTR [rdi+rax*4]  
    mulss   xmm1, DWORD PTR [rsi+rax*4]   
    add     rax, 1                       
    cmp     cx, ax
    addss   xmm0, xmm1
    jg      .L13

loop-carried 个依赖项(从一次迭代到下一次迭代)分别是:

  • xmm0,由addss xmm0, xmm1读写,在Haswell上有3个周期延迟。
  • rax,由add rax, 1读写。 1c 延迟,所以它不是 critical-path.

看起来你测量的执行时间 / cycle-count 是正确的,因为 3c 上的循环瓶颈 addss 延迟 .

这是意料之中的:点积中的序列相关性是对单个和的加法(也称为减法),而不是向量元素之间的乘法。 (使用多个 sum 累加器变量/寄存器展开可以隐藏延迟。)

这是迄今为止该循环的主要瓶颈,尽管存在各种小的低效问题:


short i 产生了愚蠢的 cmp cx, ax,它需要一个额外的 operand-size 前缀。幸运的是,gcc 设法避免实际执行 add ax, 1,因为 signed-overflow 是 C 中的未定义行为。So the optimizer can assume it doesn't happen. (update: ,所以 UB 没有进入其中,但 gcc 仍然可以合法地优化。非常古怪的东西。)

如果您使用 -mtune=intel 或更好的 -march=haswell 进行编译,gcc 会将 cmpjg 放在彼此相邻的位置 macro-fuse.

我不确定为什么您在 cmpadd 指令的 table 中有一个 *。 (更新:我纯粹是在猜测您使用的是 IACA 之类的符号,但显然您没有)。他们都没有融合。唯一发生的融合是 mulss xmm1, [rsi+rax*4] 的 micro-fusion。

并且因为它是一个 2-ope运行d ALU 指令和一个 read-modify-write 目标寄存器,所以即使在 Haswell 上的 ROB 中它也会保持 macro-fused。 (Sandybridge 会 un-laminate 它在发布时。) Note that vmulss xmm1, xmm1, [rsi+rax*4] would un-laminate on Haswell, too.

None 这真的很重要,因为你完全是 FP-add 延迟的瓶颈,比任何 uop-throughput 限制都慢得多。没有 -ffast-math,编译器将无能为力。使用 -ffast-math,clang 通常会展开多个累加器,它会 auto-vectorize,因此它们将是矢量累加器。因此,如果您命中 L1D 缓存,您可能会饱和 Haswell 的吞吐量限制,即每个时钟添加 1 个向量或标量 FP。

Haswell 上的 FMA 延迟为 5c,吞吐量为 0.5c,因此您需要 10 个累加器来保持 10 个 FMA 处于运行状态,并通过保持 p0/p1 被 FMA 饱和来最大化 FMA 吞吐量。 (Skylake 将 FMA 延迟减少到 4 个周期,并且 运行 在 FMA 单元上进行乘法、加法和 FMA。因此它实际上比 Haswell 具有更高的添加延迟。)

(您遇到了负载瓶颈,因为每个 FMA 都需要两个负载。在其他情况下,您实际上可以通过将一些 vaddps 指令替换为乘数为 1.0 的 FMA 来增加吞吐量. 这意味着要隐藏更多的延迟,所以最好是在一个更复杂的算法中,你有一个首先不在关键路径上的添加。)


回复:每个端口的 uops:

there are 1.19 uops per loop in the port 5, it is much more than expect 0.5, is it the matter about the uops dispatcher trying to make uops on every port same

是的,类似的东西。

微指令没有被分配 运行domly,或者以某种方式均匀分布在它们 可以 运行 的每个端口上。您假设 addcmp 微指令会均匀分布在 p0156 上,但事实并非如此。

问题阶段根据已经在等待该端口的微指令数量将微指令分配给端口。由于 addss 只能在 p1 上 运行 (而且是循环瓶颈),所以通常有很多 p1 uops 发出但没有执行。很少有其他 uops 会被安排到端口 1。 (这包括 mulss:大多数 mulss 微指令将最终安排到端口 0。)

[=125=Taken-branches can only 运行 only port 6. Port 5 在这个循环中没有任何可以 only 运行 的 uops,所以它结束了吸引了很多 many-port uops。

调度程序(从保留站中挑选 unfused-domain 微指令)不够智能,无法 运行 critical-path-first,所以这是分配算法减少 resource-conflict 延迟(其他 uops 在 addss 可能有 运行 的周期上窃取端口 1)。在给定端口的吞吐量出现瓶颈的情况下,它也很有用。

already-assigned 微指令的调度通常是 oldest-ready 首先,据我了解。这个简单的算法并不令人惊讶,因为它必须选择一个 uop,其输入准备好用于来自 a 60-entry RS every clock cycle, without melting your CPU. The out-of-order machinery that finds and exploits the ILP 的每个端口,这是现代 CPU 中的重要电力成本之一,与执行单元相比实际工作。

相关/更多详情:


更多性能分析资料:

除了缓存未命中/b运行ch 预测错误之外,CPU-bound 循环的三个主要可能瓶颈是:

  • 依赖链(如本例)
  • front-end 吞吐量(在 Haswell 上每个时钟最多发出 4 fused-domain 微指令)
  • 执行端口瓶颈,例如如果大量 uops 需要 p0/p1 或 p2/p3,就像在展开的循环中一样。计算特定端口的 unfused-domain 微指令。通常你可以假设 best-case 分布,在其他端口上可以 运行 的 uops 不会经常窃取繁忙的端口,但它确实发生了一些。

循环体或短代码块可以大致用 3 件事来表征:fused-domain uop 计数,unfused-domain 它可以 运行 执行单元的计数,以及总数critical-path 延迟假设 best-case 对其关键路径进行调度。 (或者从每个输入 A/B/C 到输出的延迟...)

例如,做所有三个来比较一些短序列,请参阅我在

上的回答

对于短循环,现代 CPUs 有足够的 out-of-order 执行资源(物理寄存器文件大小所以重命名不会 运行 超出寄存器,ROB 大小)有足够的循环迭代 in-flight 以找到所有并行性。但是随着循环内的依赖链越来越长,最终它们 运行 出来了。有关当 CPU 运行 超出要重命名的寄存器时发生的情况的详细信息,请参阅 Measuring Reorder Buffer Capacity

另请参阅 标签 wiki 中的大量性能和参考链接。


调整您的 FMA 循环:

是的,dot-product Haswell 上的 L1D 吞吐量将成为瓶颈,其吞吐量仅为 FMA 单元的一半,因为每次乘法+加法需要两次加载。

如果您正在做 B[i] = x * A[i] + y;sum(A[i]^2),您可能会使 FMA 吞吐量饱和。

看起来即使在 write-only 情况下,例如 vmovaps 加载的目的地,您仍在尝试避免寄存器重用,所以您 运行展开后的寄存器数 8。这很好,但对于其他情况可能很重要。

此外,使用 ymm8-15 可以稍微增加 code-size 如果这意味着需要 3 字节的 VEX 前缀而不是 2 字节。有趣的事实:vpxor ymm7,ymm7,ymm8 需要一个 3 字节的 VEX,而 vpxor ymm8,ymm8,ymm7 只需要一个 2 字节的 VEX 前缀。对于可交换操作,将源代码从高到低排序。

我们的负载瓶颈意味着 best-case FMA 吞吐量是最大值的一半,因此我们至少需要 5 个向量累加器来隐藏它们的延迟。 8 很好,因此依赖链中有很多松弛部分,可以让它们在因意外延迟或竞争 p0/p1 造成的任何延迟后赶上来。 7 甚至 6 也可以:您的展开因子不必是 2 的幂。

展开恰好 5 意味着您也正处于依赖链的瓶颈。任何时候 FMA 没有在确切的周期中 运行 其输入就绪意味着该依赖链中的周期丢失。如果加载速度很慢(例如,它在 L1 缓存中丢失并且必须等待 L2),或者如果加载无序完成并且来自另一个依赖链的 FMA 窃取了该 FMA 计划用于的端口,则可能会发生这种情况。 (请记住,调度发生在发布时间,因此位于调度程序中的 uops 是 port0 FMA 或 port1 FMA,而不是可以占用空闲端口的 FMA)。

如果您在依赖链中留一些余量,out-of-order 执行可以“赶上”FMA,因为它们不会在吞吐量或延迟方面遇到瓶颈,只需等待加载结果。 @Forward 发现(在问题的更新中)展开 5 将此循环的性能从 L1D 吞吐量的 93% 降低到 89.5%。

我的猜测是展开 6(比隐藏延迟的最小值多一倍)在这里是可以的,并且获得与展开 8 大致相同的性能。如果我们更接近最大化 FMA 吞吐量(而不是不仅仅是负载吞吐量的瓶颈),比最小值多一个可能还不够。

更新:@Forward的实验证明我的猜测是错误的。 unroll5 和 unroll6 之间没有太大区别。此外,unroll15 是 unroll8 的两倍,接近每时钟 2x 256b 负载的理论最大吞吐量。仅使用循环中的独立负载进行测量,或者使用独立负载和 register-only FMA 进行测量,将告诉我们其中有多少是由于与 FMA 依赖链的交互造成的。即使是最好的情况也不会获得完美的 100% 吞吐量,如果只是因为测量错误和定时器中断造成的中断的话。 (Linux perf 仅测量 user-space 周期,除非你 运行 它作为 root,但时间仍然包括在中断处理程序中花费的时间。这就是为什么你的 CPU 频率当 运行 作为 non-root 时,可能报告为 3.87GHz,但当 运行 作为根并测量 cycles 而不是 cycles:u 时,可能报告为 3.900GHz。)


我们在 front-end 吞吐量上没有瓶颈,但我们可以通过避免非 mov 指令的索引寻址模式来减少 fused-domain uop 计数。越少越好,当与除此之外的其他东西共享核心时,它会变得更多 hyperthreading-friendly

简单的方法就是在循环中做两个pointer-increments。复杂的方法是相对于另一个数组索引一个数组的巧妙技巧:

;; input pointers for x[] and y[] in rdi and rsi
;; size_t n  in rdx

    ;;; zero ymm1..8, or load+vmulps into them

    add             rdx, rsi             ; end_y
    ; lea rdx, [rdx+rsi-252]  to break out of the unrolled loop before going off the end, with odd n

    sub             rdi, rsi             ; index x[] relative to y[], saving one pointer increment

.unroll8:
    vmovaps         ymm0, [rdi+rsi]            ; *px, actually py[xy_offset]
    vfmadd231ps     ymm1, ymm0, [rsi]          ; *py

    vmovaps         ymm0,       [rdi+rsi+32]   ; write-only reuse of ymm0
    vfmadd231ps     ymm2, ymm0, [rsi+32]

    vmovaps         ymm0,       [rdi+rsi+64]
    vfmadd231ps     ymm3, ymm0, [rsi+64]

    vmovaps         ymm0,       [rdi+rsi+96]
    vfmadd231ps     ymm4, ymm0, [rsi+96]

    add             rsi, 256       ; pointer-increment here
                                   ; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
                                   ; smaller code-size helps in the big picture, but not for a micro-benchmark

    vmovaps         ymm0,       [rdi+rsi+128-256]  ; be pedantic in the source about compensating for the pointer-increment
    vfmadd231ps     ymm5, ymm0, [rsi+128-256]
    vmovaps         ymm0,       [rdi+rsi+160-256]
    vfmadd231ps     ymm6, ymm0, [rsi+160-256]
    vmovaps         ymm0,       [rdi+rsi-64]       ; or not
    vfmadd231ps     ymm7, ymm0, [rsi-64]
    vmovaps         ymm0,       [rdi+rsi-32]
    vfmadd231ps     ymm8, ymm0, [rsi-32]

    cmp             rsi, rdx
    jb              .unroll8                 ; } while(py < endy);

使用 non-indexed 寻址模式作为 vfmaddps 的内存操作运行d 让它在 out-of-order 内核中保持 micro-fused,而不是被un-laminated 有问题。 Micro fusion and addressing modes

所以我的循环是 18 fused-domain 微指令,用于 8 个向量。你的每个 vmovaps + vfmaddps 对需要 3 fused-domain 微指令,而不是 2,因为 un-lamination 索引寻址模式。当然,他们俩每对仍然有 2 unfused-domain 个负载微指令 (port2/3),所以这仍然是瓶颈。

更少的 fused-domain 微指令让 out-of-order 执行提前看到更多的迭代,可能帮助它更好地吸收缓存未命中。但是,当我们在执行单元(在本例中为加载 uops)上遇到瓶颈时,即使没有缓存未命中,这也是一件小事。但是使用超线程,除非另一个线程停滞,否则您只能每隔一个周期获得 front-end 问题带宽。如果它没有为负载和 p0/1 竞争太多,更少的 fused-domain 微指令将使这个循环 运行 在共享核心时更快。 (例如,也许另一个 hyper-thread 是 运行 宁很多 port5 / port6 和存储 uops?)

由于 un-lamination 发生在 uop-cache 之后,您的版本不会在 uop 缓存中占用额外的 space。每个 uop 的 disp32 是可以的,并且不需要额外的 space。但是更大的 code-size 意味着 uop-cache 不太可能有效地打包,因为在 uop 缓存行充满之前你会更频繁地达到 32B 边界。 (实际上,更小的代码也不会 gua运行tee 更好。更小的指令可能会导致填充 uop 缓存行并在跨越 32B 边界之前需要另一行中的一个条目。)这个小循环可以 运行 来自环回缓冲区 (LSD),所以幸运的是 uop-cache 不是一个因素。


然后在循环之后:高效清理是小数组高效矢量化的难点,这些小数组可能不是展开因子的倍数,尤其是矢量宽度

    ...
    jb

    ;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
    ;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.

    ; reduce down to a single vector, with a tree of dependencies
    vaddps          ymm1, ymm2, ymm1
    vaddps          ymm3, ymm4, ymm3
    vaddps          ymm5, ymm6, ymm5
    vaddps          ymm7, ymm8, ymm7

    vaddps          ymm0, ymm3, ymm1
    vaddps          ymm1, ymm7, ymm5

    vaddps          ymm0, ymm1, ymm0

    ; horizontal within that vector, low_half += high_half until we're down to 1
    vextractf128    xmm1, ymm0, 1
    vaddps          xmm0, xmm0, xmm1
    vmovhlps        xmm1, xmm0, xmm0        
    vaddps          xmm0, xmm0, xmm1
    vmovshdup       xmm1, xmm0
    vaddss          xmm0, xmm1
    ; this is faster than 2x vhaddps

    vzeroupper    ; important if returning to non-AVX-aware code after using ymm regs.
    ret           ; with the scalar result in xmm0

有关末尾水平总和的更多信息,请参阅Fastest way to do horizontal SSE vector sum (or other reduction)。我使用的两个 128b shuffle 甚至不需要立即控制字节,因此与更明显的 shufps 相比,它节省了 2 个字节的代码大小。 (还有 4 个字节的 code-size 与 vpermilps,因为该操作码总是需要一个 3 字节的 VEX 前缀和一个立即数)。与 SSE 相比,AVX 3-ope运行d 的东西 非常 很好,尤其是在用 C 编写带有内在函数的时候,所以你不能轻易地选择一个冷寄存器到 movhlps 进入.