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

59

我是一名指令优化的新手。

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

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

运行后,我们得到结果:
   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           

第二行是从英特尔寄存器中读取的值;第三行被分成了"BrTaken"分支数。

因此,我们可以看到,在循环中有6条指令,7个uops,与分析结果一致。

在端口0、端口1、端口5和端口6上运行的uops数量与分析报告中说的相似。我认为这可能是由于uops调度程序的作用,它试图平衡端口上的负载,我想我是对的吗?

我绝对不明白为什么每个循环只需要大约3个周期。根据Agner的指令表,指令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 * 256位/8.59,非常接近峰值2 * 256/8,使用率约为93%,FMA单元仅使用了8/8.59,峰值为2 * 8/8,使用率为47%。

因此,我认为我已经达到了Peter Cordes的预期的L1D瓶颈。

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

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

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

根据Peter的回复,我明白只有“可读和可写”寄存器才是依赖项,“仅写入”寄存器不是依赖项。所以我尝试减少循环中使用的寄存器,并尝试每次展开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%,比起向上取整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

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

我尝试在 "https://gcc.godbolt.org" 上使用 gcc 7.1 编译该函数。
编译选项为 "-O3 -march=haswell -mtune=intel",与 gcc 4.8.3 类似。请注意保留 html 标签。
.L3:
        vmovss  xmm1, DWORD PTR [rdi+rax]
        vfmadd231ss     xmm0, xmm1, DWORD PTR [rsi+rax]
        add     rax, 4
        cmp     rdx, rax
        jne     .L3
        ret

17
研究付出值得点赞。 - fuz
4
在Haswell处理器上有两个执行单元可以执行浮点数乘法,因此可以并行运行两个MULSS指令。每个循环迭代中的MULSS指令之间没有依赖关系。 - Ross Ridge
做得好,展开FMA循环的工作非常出色。我在我的答案中添加了一个关于这个的部分。你可以缩小代码大小和融合域uops的数量,但是你可能无法更接近饱和p2/p3 uop吞吐量,这限制了你每个周期提供平均一个FMA的两个L1D负载。我更新了我的答案,以使重用寄存器与只写指令一起使用更清晰。你的FMA循环将许多架构寄存器用作负载目标,但没有任何好处。(但只有代码大小的缺点)。 - Peter Cordes
顺便说一句,我建议你使用更新的编译器。gcc4.8相当老了。gcc6.4或gcc7.1是当前版本,通常会做得更好,特别是在AVX/AVX2自动向量化方面。但是使用C内部函数,你可以从新版本的gcc中获得更高效的代码。当编译器做出奇怪或酷炫的事情时,将其与clang或其他gcc版本进行比较可能会很有趣。 - Peter Cordes
1
通常情况下,您需要使用比硬件更新的编译器,这样他们就有时间更新-march=native的调整选项,并修复一些可能只在AVX2存在一段时间后才会注意到的使代码变慢的问题。虽然我认为很多人使用旧的编译器也能得到不错的结果。也许我太过于重视它了,但是当我查看编译器汇编输出时,新版本的gcc通常表现更好。尽管在整体上并不会有太大影响。 - Peter Cordes
显示剩余4条评论
1个回答

45

相关:


请看一下你的循环:movss xmm1, srcxmm1 的旧值没有依赖性,因为它的目标是只写入。每次迭代的 mulss 是独立的。乱序执行可以并且确实利用了指令级并行性,因此你绝对不会受到 mulss 延迟的限制。 可选阅读:在计算机体系结构术语中,寄存器重命名避免了重复使用相同结构寄存器的 WAR反依赖数据冒险。 (在寄存器重命名之前的一些流水线和依赖跟踪方案并未解决所有问题,因此计算机体系结构领域非常关注不同类型的数据冒险。)
使用Tomasulo算法的寄存器重命名可以消除除实际真正依赖(写后读)以外的所有内容,因此任何目标寄存器不是源寄存器的指令都不会与涉及该寄存器旧值的依赖链发生交互。(除了虚假依赖,例如Intel CPU上的popcnt,以及只写入寄存器的一部分而不清除其余部分(例如mov al,5sqrtss xmm2,xmm1)。相关:为什么x86-64指令对32位寄存器清零全64位寄存器的上半部分?)。

返回您的代码:

.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

循环内的依赖关系(从一个迭代到下一个)分别为:
  • xmm0,由addss xmm0,xmm1读取和写入,Haswell上具有3个周期延迟。
  • rax,由add rax,1读取和写入。1个周期延迟,因此不是关键路径。

看起来您正确测量了执行时间/循环计数,因为循环瓶颈在3个周期的addss延迟上

这是预期的:点积中的串行依赖关系是加法到单个总和(又称缩减),而不是向量元素之间的乘法。(使用多个sum累加器变量/寄存器展开可以隐藏该延迟。)

尽管存在各种次要的低效率,但这远远是该循环的主要瓶颈:


short i产生了愚蠢的cmp cx,ax,它需要一个额外的操作数大小前缀。幸运的是,gcc设法避免实际执行add ax,1,因为在C语言中有符号溢出是未定义行为。因此,优化器可以假定它不会发生。(更新:{{link2:整数提升规则对于short类型不同} },因此UB并不涉及,但gcc仍然可以合法地进行优化。非常奇妙。)
如果使用-mtune=intel或更好的-march=haswell编译,gcc将把cmpjg放在一起,以便宏融合。

我不确定为什么在cmpadd指令的表格中会出现*。(更新:我猜测你可能使用的是类似IACA的符号表示法,但显然你没有这样做)。它们都没有融合。唯一发生的融合是mulss xmm1,[rsi+rax*4]的微融合。

由于这是一个带有读-修改-写目标寄存器的两操作数ALU指令,在Haswell上即使在ROB中也会保持宏融合。 (Sandybridge会在问题时间取消层压)。请注意,vmulss xmm1,xmm1,[rsi+rax*4] 在Haswell上也会取消层压

由于您只是完全受制于FP-add延迟,比任何uop-throughput限制都慢,所以这一切都不重要。如果没有-ffast-math,编译器无能为力。使用-ffast-math,clang通常会展开多个累加器,并自动向量化,因此它们将成为向量累加器。因此,如果命中L1D缓存,则可以饱和Haswell每个时钟周期的吞吐量限制,即1个矢量或标量FP加法。

由于FMA在Haswell上的延迟为5个时钟周期,吞吐量为0.5个时钟周期,因此您需要10个累加器来保持10个FMA并最大化FMA吞吐量,通过保持p0/p1饱和的FMAs。 (Skylake将FMA延迟降低到4个周期,并在FMA单元上运行乘法、加法和FMA。因此,它实际上比Haswell具有更高的加法延迟。)

您的瓶颈在于负载,因为每个FMA需要两个负载。在其他情况下,通过用乘数1.0的FMA替换一些vaddps指令,实际上可以增加加法吞吐量。这意味着需要隐藏更多的延迟,因此最好在更复杂的算法中使用,其中首先不在关键路径上的加法。


关于每个端口的uops数量:

在端口5中,每个循环有1.19个uops, 远远超过了0.5的预期,这是不是由于uops分配器试图让每个端口上的uops相同所导致的问题?

是的,类似于这样的问题。

uops不是随机分配的,也不会均匀分布在它们可以运行的每个端口上。您假设addcmp uops将均匀分布在p0156上,但实际情况并非如此。

问题阶段根据每个端口上等待的uops数量将uops分配给端口。由于addss只能在p1上运行(并且它是循环的瓶颈),通常会发出很多p1 uops但未执行。因此,很少有其他uops被调度到port1。(包括mulss: 大多数mulss uops最终会被调度到端口0。)

已分配的分支只能在端口6上运行。在这个循环中,端口5没有任何只能在那里运行的uops,因此它吸引了许多多端口uops。
调度程序(从预约站中选择未融合域uops)不够聪明,不能优先运行关键路径,所以这个分配算法可以减少资源冲突延迟(其他uops在一个addss可以运行的周期中窃取port1)。在瓶颈出现在给定端口的吞吐量时,这也很有用。
已分配uops的调度通常是最老的就绪优先,据我所知。这个简单的算法并不令人惊讶,因为它必须从60项RS每个时钟周期中选择一个输入就绪的uop,并且不能使您的CPU崩溃。找到和利用ILP的乱序机制是现代CPU中显著的功耗之一,与执行实际工作的执行单元相当。

相关/更多细节: x86 uops如何被准确调度?


更多性能分析内容:

除了缓存未命中/分支预测失误外,CPU绑定循环的三个主要潜在瓶颈是:

  • 依赖链(就像在这种情况下一样)
  • 前端吞吐量(Haswell每个时钟周期最多发出4个融合域uop)
  • 执行端口瓶颈,例如如果许多uop需要p0/p1,或p2/p3,就像你的展开循环一样。对于特定端口计算未融合域uop的数量。通常可以假设最佳分布,即可以在其他端口上运行的uop不会经常窃取繁忙端口,但确实会发生一些情况。

循环体或短代码块可以通过3个方面进行大致刻画:融合域uop数量、可以运行的执行单元的未融合域数量以及假设其关键路径具有最佳调度的总关键路径延迟。(或从输入A/B/C到输出的延迟...)

例如,为了比较几个短序列,可以执行这三种操作,详见我的回答What is the efficient way to count set bits at a position or lower?

对于短循环,现代CPU具有足够的乱序执行资源(物理寄存器文件大小,因此重命名不会用完寄存器,ROB大小),可以有足够的循环迭代来发现所有并行性。但是随着循环内部的依赖链变得更长,最终它们会用完。请参见Measuring Reorder Buffer Capacity,了解CPU在无法重命名到寄存器时会发生什么情况。

还可以在标签wiki中找到大量性能和参考链接。


调整您的FMA循环:

是的,在Haswell上进行点积将在L1D吞吐量上达到瓶颈,仅达到FMA单元吞吐量的一半,因为它需要每个乘法+加法两次加载。

如果您正在执行B[i] = x * A[i] + y;sum(A[i]^2),则可以饱和FMA吞吐量。

看起来您仍然在尝试避免寄存器重用,即使在只写情况下,例如vmovaps加载的目标,因此在展开8次后已用完寄存器。这没关系,但对其他情况可能有影响。

此外,使用ymm8-15可能会略微增加代码大小,如果这意味着需要3字节的VEX前缀而不是2字节,则会产生影响。有趣的事实是:vpxor ymm7,ymm7,ymm8需要3字节的VEX,而vpxor ymm8,ymm8,ymm7仅需要2字节的VEX前缀。对于可交换的操作,从高到低排序源寄存器。

我们的负载瓶颈意味着最佳情况下FMA吞吐量是最大值的一半,因此我们需要至少5个向量累加器来隐藏它们的延迟。8个是不错的选择,这样就有足够的余地在依赖链中让它们在任何意外延迟或p0/p1竞争造成的延迟后追赶上来。7或甚至6也可以:您的展开系数不必是2的幂。

恰好展开5次意味着您也处于依赖链瓶颈的位置。每当FMA无法在准确的周期内运行其输入时,就会在该依赖链中丢失一个周期。如果加载速度慢(例如,在L1缓存中未命中并且必须等待L2),或者如果加载完成的顺序不正确,并且另一个依赖链的FMA窃取了此FMA计划使用的端口。(请记住,调度发生在发布时,因此坐在调度程序中的uop要么是port0 FMA,要么是port1 FMA,而不是可以使用空闲端口的FMA)。

如果在依赖链中留下一些余地,乱序执行可以“追赶”FMA,因为它们不会受到吞吐量或延迟的瓶颈影响,只是等待负载结果。@Forward发现(在问题的更新中),将循环展开5次会将性能从L1D吞吐量的93%降至89.5%。
我猜在这里展开6次(比最小值多一个来隐藏延迟)应该没问题,并且可以获得与展开8次相同的性能。如果我们离最大化FMA吞吐量更近(而不仅仅是被负载吞吐量限制),那么比最小值多一个可能不足够。

更新:@Forward的实验测试表明我的猜测是错误的。unroll5和unroll6之间没有很大的区别。此外,unroll15比unroll8更接近理论最大吞吐量的2倍256b每个时钟周期的负载。仅使用循环中的独立负载进行测量,或者使用独立负载和仅寄存器FMA会告诉我们其中多少是由于与FMA依赖链的交互所致。即使在最佳情况下,也不会获得完美的100%吞吐量,因为可能存在测量误差和由于计时器中断而引起的干扰。(Linux perf只测量用户空间周期,除非以root身份运行,但时间仍包括在中断处理程序中花费的时间。这就是为什么以非root身份运行时,您的CPU频率可能报告为3.87GHz,而以root身份运行并测量cycles而不是cycles:u时,则为3.900GHz。)


我们的前端吞吐量没有瓶颈,但是我们可以通过避免对非mov指令使用索引寻址模式来减少聚合域uop计数。数量越少越好,当与其他内容共享核心时,这样做更加“超线程友好”。
简单的方法就是在循环内部进行两次指针递增。复杂的方法是一种有趣的技巧,将一个数组相对于另一个数组进行索引。
;; 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);

使用非索引寻址模式作为 vfmaddps 的内存操作数,可以使其在乱序核心中保持微融合,而不是在发出时解离。微融合和寻址模式 因此,我的循环对于8个向量需要18个融合域 uops。你的循环对于每个 vmovaps + vfmaddps 对需要3个融合域 uops,而不是2个,因为索引寻址模式会解离。它们两个当然仍然每对有2个未融合域的加载 uops(端口2/3),所以这仍然是瓶颈。
较少的融合域uops可以让乱序执行看到更多迭代,潜在地帮助其更好地吸收缓存未命中。即使没有缓存未命中,在我们受到执行单元(在此情况下为load uops)的瓶颈限制时,这只是一个小问题。但是,在超线程中,除非另一个线程停顿,否则您只能获得每个前端问题带宽的另一周期。如果它在负载和p0/1方面不竞争太多,较少的融合域uops将使此循环在共享核心时运行更快。 (例如,可能其他超线程正在运行大量port5 / port6和store uops?)
由于解层发生在uop高速缓存之后,因此您的版本不会占用额外的uop高速缓存空间。每个uop都有一个disp32是可以的,不需要额外的空间。但是,较大的代码大小意味着uop-cache更不可能有效地打包,因为您会更经常地在32B边界上击中,而不是在uop高速缓存线充满之前。 (实际上,较小的代码也不能保证更好。较小的指令可能导致填充uop高速缓存行并在跨越32B边界之前需要另一个条目。)这个小循环可以从loopback缓冲区(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

关于最后的水平求和,更多信息请参见如何最快地进行水平SSE向量求和(或其他约简)。我使用的两个128位洗牌甚至不需要一个立即控制字节,因此与更明显的shufps相比,可以节省2个字节的代码大小。(与vpermilps相比,代码大小节省4个字节,因为该操作码总是需要一个3字节的VEX前缀以及一个立即数)。AVX 3操作数的东西与SSE相比非常好,特别是在使用内部函数时用C编写,这样你不能像movhlps那样轻松地选择一个冷寄存器。


你认为呢,实际上在端口5中每个循环有1.19个uops,比预期的0.5要多得多,这是不是与uops调度器试图使每个端口上的uops相同有关呢? - Forward
啊,我没有读完你的整个问题,因为它很长,而且我可以从代码中看到答案,特别是在Ross的提示之后。IACA使用类似于“2 ^”的符号来表示微融合,但这根本不是标准的东西。这只是我在试图弄清楚你的目标时的第一个猜测。 - Peter Cordes
3
i 声明为 short 类型且 i 的值为 2^15-1 时,执行 i++ 操作不会产生未定义行为。具体而言,i++ 会被扩展为 i = (short) ((int) i + 1);,在这个赋值语句中,int 类型转换成 short 类型时发生的溢出行为是由具体实现定义的。尽管如此,GCC 的代码转换仍然是正确的。 - Pascal Cuoq
3
@Forward: 是的,我没有将这个答案局限于初学者级别的内容:P 这似乎是一个写下如何计算延迟、前端uop和执行端口uop的权威版本的好地方。如果我要在其他答案中链接到这里,我可能会为任何想阅读有趣细节的人提供充足的信息。:) 如果你在阅读 Agner Fog 的指南(特别是微体系结构一章)并在 SO 上搜索后仍然卡住了,请在将来提出更多好问题。这里有一些很好的 x86 性能答案(其中一些是我自己的 :) - Peter Cordes
1
@PeterCordes,是的,在我的测试中,15比8快一些,但只是一点点,你可以看到8的最佳情况类似于15的最差情况。 - Forward
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接