有没有一种方法可以利用所有XMM寄存器?

6
以下是从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)被使用。我认为展开循环会给编译器一个提示,可以使用更多的xmm。我将代码修改为:

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 路超标量机器上,如果分配了 4 个不同的 xmms,则第二个代码片段应该给我理论加速 4 倍(例如)。
PS:第二个代码片段似乎稍微快一点(一致的)。
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的表现最差(虽然循环的一部分被展开了)。


2
看一下寄存器重命名。由于这个,编译器只需使用一个寄存器就足够了... - Mysticial
4
根据Anger Fog的著名手册(http://www.agner.org/optimize/),对于Wolfdale CPU来说,指令SQRTPS的延迟为6-13个周期,倒数吞吐量为5-12个周期,并且只能使用一个执行端口p0。简单地说,只能有一个SQRTPS指令在运行,每5-12个周期才能开始一个新的指令。因此,SQRTPS指令没有并发执行的机会,并且与加法或乘法相比,可能需要更长时间才能完成。编译器无法通过增加寄存器来解决这个问题。 - Iwillnotexist Idonotexist
1
那个编译器的输出太糟糕了。检查一下你的编译器选项。你正在使用优化吗? - Ben Voigt
@BenVoigt 很抱歉我是 SSE 和优化方面的新手。我并没有进行任何优化。我应该如何编译?我使用的是 GCC 版本 4.6.3。 - Mathai
@BenVoigt 发布了。我觉得奇怪的是,sse 的表现与 sse4(您的函数)相同。现在 sse3 的表现最差。我现在看到您的函数中使用 xmm0 - xmm3。sse 只使用 xmm0,但提供相同的性能。我不明白为什么。 - Mathai
显示剩余6条评论
2个回答

4
怎么样:
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
@Mathai:你能发布反汇编代码吗?我并不感到惊讶速度没有改变,但是对于编译器仅使用一个XMM寄存器来处理所有四个命名变量,考虑到它们的生命周期重叠,应该会有很大的不同。它是否进行了大量指令重排? - Ben Voigt
发布了。我不确定指令重排是什么。 - Mathai
@Ben Voigt,我认为之所以更快的原因是无论并行执行和导管化的 _mm_sqrt_ps,装载和存储都可以并行进行。这不是并行执行处理器资源上的瓶颈,而是内存上的瓶颈。您是否知道这个理论是否有效? - Apriori

4

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


他实际上没有在任何地方阅读 float*。消除 aptr 之间的别名肯定是好的。 - Ben Voigt
@Ben Jackson,我不熟悉__restric__。我要把它改成void sse3(float* restrict a, int N)吗?我尝试了一下,结果在反汇编中只看到xmm0。 - Mathai
GCC似乎不关心float__m128之间的别名,正如@BenVoigt所指出的那样。我使用-funroll-all-loops得到多个寄存器(它甚至处理所有nb_iters不是展开倍数的情况)。 - Ben Jackson

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