有没有办法利用所有 XMM 寄存器?

Posted

技术标签:

【中文标题】有没有办法利用所有 XMM 寄存器?【英文标题】:Is there a way to utilize all XMM registers? 【发布时间】:2013-11-23 06:05:35 【问题描述】:

这是一个代码 sn-p,用于计算浮点数组中值的平方根,取自 http://felix.abecassis.me/2011/09/cpp-getting-started-with-sse/

void sse(float* a, int N)

    // We assume N % 4 == 0.
    int nb_iters = N / 4;
   __m128* ptr = (__m128*)a;


    for (int i = 0; i < nb_iters; ++i, ++ptr, a += 4)
        _mm_store_ps(a, _mm_sqrt_ps(*ptr));
    


当我分解这段代码时,我看到只有一个 xmm (xmm0) 被使用。我假设展开循环会给编译器一个可以使用更多 xmms 的提示。我将代码修改为

void sse3(float* a, int N)

__m128* ptr = (__m128*)a;

 for (int i = 0; i < N; i+=32)

    _mm_store_ps(a + i, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 4, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 8, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 12, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 16, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 20, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 24, _mm_sqrt_ps(*ptr));
    ptr++;
    _mm_store_ps(a + i + 28, _mm_sqrt_ps(*ptr));
    ptr++;
 

在这种情况下,N 应该大于 32。 但是我仍然没有看到超过一个 xmm。为什么编译器不能分配多个 xmm?

我的理解是 xmm0、xmm1、xmm2 ... xmm7 上的计算是独立的,可以在现代超标量架构上并行进行。在 4 路超标量机器上,第二个代码 sn-p 应该给我 4 的理论加速(例如,如果分配了 4 个不同的 xmm)。

PS:第二个代码 sn-p 似乎快一点(一致)。

sse3: 18809 microseconds
sse: 20543 microseconds

更新

按照建议使用 -O3 标志

这是 Ben Voigt 答案的反汇编 - 请注意,我将函数的名称更改为 sse4。

147:ssetest.cpp   **** void sse4(float* a, int N)
148:ssetest.cpp   **** 
2076                    .loc 8 148 0
2077                    .cfi_startproc
2078                .LVL173:
2079                .LBB5900:
2080                .LBB5901:
149:ssetest.cpp   ****    __m128 b, c, d, e;
150:ssetest.cpp   ****
151:ssetest.cpp   ****    for (int i = 0; i < N; i += 16) 
2081                    .loc 8 151 0
2082 0320 85F6          testl   %esi, %esi  # N
2083 0322 7E4C          jle .L106   #,
147:ssetest.cpp   **** void sse4(float* a, int N)
2084                    .loc 8 147 0
2085 0324 8D56FF        leal    -1(%rsi), %edx  #, tmp104
2086                .LBE5901:
2087                .LBE5900:
2088 0327 31C0          xorl    %eax, %eax  # ivtmp.1046
2089                .LBB5925:
2090                .LBB5924:
2091 0329 C1EA04        shrl    $4, %edx    #,
2092 032c 4883C201      addq    $1, %rdx    #, D.189746
2093 0330 48C1E206      salq    $6, %rdx    #, D.189746
2094                .LVL174:
2095                    .p2align 4,,10
2096 0334 0F1F4000      .p2align 3
2097                .L108:
2098                .LBB5902:
2099                .LBB5903:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) *(__v4sf *)__P;
2100                    .loc 9 899 0 discriminator 2
2101 0338 0F285407      movaps  16(%rdi,%rax), %xmm2    # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 16B], c
2101      10
2102                .LVL175:
2103                .LBE5903:
2104                .LBE5902:
2105                .LBB5904:
2106                .LBB5905:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2107                    .loc 9 182 0 discriminator 2
2108 033d 0F511C07      sqrtps  (%rdi,%rax), %xmm3  # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 0B], tmp107
2109                .LBE5905:
2110                .LBE5904:
2111                .LBB5906:
2112                .LBB5907:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) *(__v4sf *)__P;
2113                    .loc 9 899 0 discriminator 2
2114 0341 0F284C07      movaps  32(%rdi,%rax), %xmm1    # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 32B], d
2114      20
2115                .LVL176:
2116                .LBE5907:
2117                .LBE5906:
2118                .LBB5908:
2119                .LBB5909:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2120                    .loc 9 182 0 discriminator 2
2121 0346 0F51D2        sqrtps  %xmm2, %xmm2    # c, tmp109
2122                .LBE5909:
2123                .LBE5908:
2124                .LBB5910:
2125                .LBB5911:
899:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) *(__v4sf *)__P;
2126                    .loc 9 899 0 discriminator 2
2127 0349 0F284407      movaps  48(%rdi,%rax), %xmm0    # MEM[base: a_7(D), index: ivtmp.1046_85, offset: 48B], e
2127      30
2128                .LVL177:
2129                .LBE5911:
2130                .LBE5910:
2131                .LBB5912:
2132                .LBB5913:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2133                    .loc 9 182 0 discriminator 2
2134 034e 0F51C9        sqrtps  %xmm1, %xmm1    # d, tmp111
2135                .LBE5913:
2136                .LBE5912:
2137                .LBB5914:
2138                .LBB5915:
2139                    .loc 9 948 0 discriminator 2
2140 0351 0F291C07      movaps  %xmm3, (%rdi,%rax)  # tmp107, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 0B]
2141                .LVL178:
2142                .LBE5915:
2143                .LBE5914:
2144                .LBB5916:
2145                .LBB5917:
182:/usr/lib/gcc/x86_64-linux-gnu/4.6/include/xmmintrin.h ****   return (__m128) __builtin_ia32_sqrtps ((__v4sf)__A);
2146                    .loc 9 182 0 discriminator 2
2147 0355 0F51C0        sqrtps  %xmm0, %xmm0    # e, tmp113
2148                .LBE5917:
2149                .LBE5916:
2150                .LBB5918:
2151                .LBB5919:
2152                    .loc 9 948 0 discriminator 2
2153 0358 0F295407      movaps  %xmm2, 16(%rdi,%rax)    # tmp109, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 16B]
2153      10
2154                .LVL179:
2155                .LBE5919:
2156                .LBE5918:
2157                .LBB5920:
2158                .LBB5921:
2159 035d 0F294C07      movaps  %xmm1, 32(%rdi,%rax)    # tmp111, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 32B]
2159      20
2160                .LVL180:
2161                .LBE5921:
2162                .LBE5920:
2163                .LBB5922:
2164                .LBB5923:
2165 0362 0F294407      movaps  %xmm0, 48(%rdi,%rax)    # tmp113, MEM[base: a_7(D), index: ivtmp.1046_85, offset: 48B]
2165      30
2166 0367 4883C040      addq    $64, %rax   #, ivtmp.1046
2167                .LVL181:
2168                .LBE5923:
2169                .LBE5922:
2170                    .loc 8 151 0 discriminator 2
2171 036b 4839D0        cmpq    %rdx, %rax  # D.189746, ivtmp.1046
2172 036e 75C8          jne .L108   #,
2173                .LVL182:
2174                .L106:
2175 0370 F3            rep
2176 0371 C3            ret
2177                .LBE5924:
2178                .LBE5925:
2179                    .cfi_endproc
2180                .LFE7998:
2182 0372 66666666      .p2align 4,,15
2182      662E0F1F
2182      84000000
2182      0000
2183                    .globl  _Z6normalPfi
2185                _Z6normalPfi:
2186                .LFB7999:
152:ssetest.cpp   ****       b = _mm_load_ps(a + i);
153:ssetest.cpp   ****       c = _mm_load_ps(a + i +  4);
154:ssetest.cpp   ****       d = _mm_load_ps(a + i +  8);
155:ssetest.cpp   ****       e = _mm_load_ps(a + i + 12);
156:ssetest.cpp   ****       _mm_store_ps(a + i,      _mm_sqrt_ps(b));
157:ssetest.cpp   ****       _mm_store_ps(a + i +  4, _mm_sqrt_ps(c));
158:ssetest.cpp   ****       _mm_store_ps(a + i +  8, _mm_sqrt_ps(d));
159:ssetest.cpp   ****       _mm_store_ps(a + i + 12, _mm_sqrt_ps(e));
160:ssetest.cpp   ****    
161:ssetest.cpp   **** 

奇怪的是,sse 和 sse4 的性能几乎相同,而 sse3 的性能最差(尽管部分循环已展开)。

【问题讨论】:

看看Register Renaming。因此,编译器根本不需要使用超过 1 个寄存器... 根据 Anger Fog 的知名手册 (agner.org/optimize),对于 Wolfdale CPU,指令 SQRTPS 有 6-13 个周期的延迟,5-12 个周期的倒数吞吐量,并且只能使用一个执行端口, p0。为您翻译,只有一条 SQRTPS 指令可以在运行中,并且每 5-12 个周期只能启动一条。因此,并发执行 SQRTPS 指令的机会为零,与加法或乘法相比,它们可能需要很长时间才能完成。编译器无法通过在此处破坏更多寄存器来取胜。 @Mysticial 汇编代码中存在 Read-After-Write 依赖关系,xmm0 在上一次存储到内存之后总是必须等待从内存加载。 @IwillnotexistIdonotexist 很棒的信息。是时候阅读那个文档了:)谢谢 编译器输出绝对是可怕的。检查您的编译器选项。您正在使用优化,对吧? 【参考方案1】:

怎么样:

void sse3(float* a, int N)

   __m128 b, c, d, e;

   for (int i = 0; i < N; i += 16) 
      b = _mm_load_ps(a + i);
      c = _mm_load_ps(a + i +  4);
      d = _mm_load_ps(a + i +  8);
      e = _mm_load_ps(a + i + 12);
      _mm_store_ps(a + i,      _mm_sqrt_ps(b));
      _mm_store_ps(a + i +  4, _mm_sqrt_ps(c));
      _mm_store_ps(a + i +  8, _mm_sqrt_ps(d));
      _mm_store_ps(a + i + 12, _mm_sqrt_ps(e));
   

注意:使用同一个 XMM 寄存器进行多个计算是可以杀死流水线的一件事,但它不是唯一的事情。只有在其他资源足够多的情况下,对不同寄存器的操作才能独立进行。正如您的评论所暗示的那样,没有一个完整的 SIMD 计算单元专用于每个寄存器。

【讨论】:

感谢您的回复。我在反汇编中仍然只看到 xmm0 。速度没有差别。 @Mathai:你能发布反汇编吗?速度没有变化我并不感到非常惊讶,但是考虑到它们的生命周期重叠,编译器对所有四个命名变量只使用一个 XMM 寄存器应该是完全不同的。它做了很多指令重新排序吗? 已发布。我不确定指令重新排序是什么。 @Ben Voigt,我关于为什么这更快的理论是,无论 _mm_sqrt_ps 的并行执行和管道如何,加载和存储都可以并行发生。这不是并行执行处理器资源的瓶颈,而是内存的瓶颈。你知道这个理论是否有效吗?【参考方案2】:

我认为您在这里看到的是编译器防止别名。您正在通过指针进行读写,因此编译器必须假设每次写入任何float * 都可能会更改通过float * 的下一次读取。尝试在指针上使用__restrict__ 来告诉编译器你没有这样做。您还可以重写循环以从一个数组(全局是理想的,因为这样不会涉及别名)计算到另一个数组。

【讨论】:

他实际上并没有在任何地方阅读float*。不过,摆脱 aptr 之间的别名肯定会很好。 @Ben Jackson,我不熟悉 retric 。我是否将其更改为 void sse3(float* restrict a, int N) ?我试过这个,我在反汇编中只看到 xmm0。 GCC 似乎并不关心 @BenVoigt 指出的 float__m128 之间的别名。我使用-funroll-all-loops 获得多个寄存器(它甚至可以处理nb_iters 不是展开的倍数的所有情况。)

以上是关于有没有办法利用所有 XMM 寄存器?的主要内容,如果未能解决你的问题,请参考以下文章

将XMM寄存器推入堆栈

如何将浮点常量值移动到xmm寄存器中?

XMM 寄存器的按位取反

SSE XMM 点积说明

有没有办法用 xor 翻转 32 位浮点数的符号位?

XMM 寄存器中的取消引用指针(收集)