英特尔C编译器在对齐内存时使用不对齐的SIMD移动。

4

我正在使用Haswell Core i7-4790K处理器。

当我用 icc -O3 -std=c99 -march=core-avx2 -g 编译以下玩具例子时:

#include <stdio.h>
#include <stdint.h>
#include <immintrin.h>

typedef struct {
  __m256i a;
  __m256i b;
  __m256i c;
} mystruct_t;

#define SIZE     1000
#define TEST_VAL 42

int _do(mystruct_t* array) {
  int value = 0;

  for (size_t i = 0; i < SIZE; ++i) {
    array[i].a = _mm256_set1_epi8(TEST_VAL + i*3    );
    array[i].b = _mm256_set1_epi8(TEST_VAL + i*3 + 1);
    array[i].c = _mm256_set1_epi8(TEST_VAL + i*3 + 2);

    value += _mm_popcnt_u32(_mm256_movemask_epi8(array[i].a)) +
             _mm_popcnt_u32(_mm256_movemask_epi8(array[i].b)) +
             _mm_popcnt_u32(_mm256_movemask_epi8(array[i].c));
  }

  return value;
}

int main() {
  mystruct_t* array = (mystruct_t*)_mm_malloc(SIZE * sizeof(*array), 32);
  printf("%d\n", _do(array));
  _mm_free(array);
}

以下是为_do()函数生成的ASM代码:
0x0000000000400bc0 <+0>:    xor    %eax,%eax
0x0000000000400bc2 <+2>:    xor    %ecx,%ecx
0x0000000000400bc4 <+4>:    xor    %edx,%edx
0x0000000000400bc6 <+6>:    nopl   (%rax)
0x0000000000400bc9 <+9>:    nopl    0x0(%rax)
0x0000000000400bd0 <+16>:   lea     0x2b(%rdx),%r8d
0x0000000000400bd4 <+20>:   inc    %ecx
0x0000000000400bd6 <+22>:   lea     0x2a(%rdx),%esi
0x0000000000400bd9 <+25>:   lea     0x2c(%rdx),%r9d
0x0000000000400bdd <+29>:   add    $0x3,%edx
0x0000000000400be0 <+32>:   vmovd  %r8d,%xmm1
0x0000000000400be5 <+37>:   vpbroadcastb %xmm1,%ymm4
0x0000000000400bea <+42>:   vmovd  %esi,%xmm0
0x0000000000400bee <+46>:   vpmovmskb %ymm4,%r11d
0x0000000000400bf2 <+50>:   vmovd  %r9d,%xmm2
0x0000000000400bf7 <+55>:   vmovdqu %ymm4,0x20(%rdi)
0x0000000000400bfc <+60>:   vpbroadcastb %xmm0,%ymm3
0x0000000000400c01 <+65>:   vpbroadcastb %xmm2,%ymm5
0x0000000000400c06 <+70>:   vpmovmskb %ymm3,%r10d
0x0000000000400c0a <+74>:   vmovdqu %ymm3,(%rdi)
0x0000000000400c0e <+78>:   vmovdqu %ymm5,0x40(%rdi)
0x0000000000400c13 <+83>:   popcnt %r11d,%esi
0x0000000000400c18 <+88>:   add    $0x60,%rdi
0x0000000000400c1c <+92>:   vpmovmskb %ymm5,%r11d
0x0000000000400c20 <+96>:   popcnt %r10d,%r9d
0x0000000000400c25 <+101>:  popcnt %r11d,%r8d
0x0000000000400c2a <+106>:  add    %esi,%r9d
0x0000000000400c2d <+109>:  add    %r8d,%r9d
0x0000000000400c30 <+112>:  add    %r9d,%eax
0x0000000000400c33 <+115>:  cmp    $0x3e8,%ecx
0x0000000000400c39 <+121>:  jb      0x400bd0 <_do+16>
0x0000000000400c3b <+123>:  vzeroupper 
0x0000000000400c3e <+126>:  retq   
0x0000000000400c3f <+127>:  nop

如果我使用gcc-5 -O3 -std=c99 -mavx2 -march=native -g编译相同的代码,将会产生以下汇编代码用于_do()函数:

0x0000000000400650 <+0>:    lea     0x17700(%rdi),%r9
0x0000000000400657 <+7>:    mov    $0x2a,%r8d
0x000000000040065d <+13>:   xor    %eax,%eax
0x000000000040065f <+15>:   nop
0x0000000000400660 <+16>:   lea     0x1(%r8),%edx
0x0000000000400664 <+20>:   vmovd  %r8d,%xmm2
0x0000000000400669 <+25>:   xor    %esi,%esi
0x000000000040066b <+27>:   vpbroadcastb %xmm2,%ymm2
0x0000000000400670 <+32>:   vmovd  %edx,%xmm1
0x0000000000400674 <+36>:   add    $0x60,%rdi
0x0000000000400678 <+40>:   lea     0x2(%r8),%edx
0x000000000040067c <+44>:   vpbroadcastb %xmm1,%ymm1
0x0000000000400681 <+49>:   vmovdqa %ymm2,-0x60(%rdi)
0x0000000000400686 <+54>:   add    $0x3,%r8d
0x000000000040068a <+58>:   vmovd  %edx,%xmm0
0x000000000040068e <+62>:   vpmovmskb %ymm2,%edx
0x0000000000400692 <+66>:   vmovdqa %ymm1,-0x40(%rdi)
0x0000000000400697 <+71>:   vpbroadcastb %xmm0,%ymm0
0x000000000040069c <+76>:   popcnt %edx,%esi
0x00000000004006a0 <+80>:   vpmovmskb %ymm1,%edx
0x00000000004006a4 <+84>:   popcnt %edx,%edx
0x00000000004006a8 <+88>:   vpmovmskb %ymm0,%ecx
0x00000000004006ac <+92>:   add    %esi,%edx
0x00000000004006ae <+94>:   vmovdqa %ymm0,-0x20(%rdi)
0x00000000004006b3 <+99>:   popcnt %ecx,%ecx
0x00000000004006b7 <+103>:  add    %ecx,%edx
0x00000000004006b9 <+105>:  add    %edx,%eax
0x00000000004006bb <+107>:  cmp    %rdi,%r9
0x00000000004006be <+110>:  jne     0x400660 <_do+16>
0x00000000004006c0 <+112>:  vzeroupper 
0x00000000004006c3 <+115>:  retq

我的问题如下:

1) 为什么icc使用未对齐的移动(vmovdqu)而不是gcc?

2) 在对齐内存时,使用vmovdqu是否会有惩罚?

P.S: 使用SSE指令/寄存器时问题相同。

谢谢


2
ICC在2012年开始这样做,MSVC一年后也效仿了。令人烦恼的是,当数据未对齐时它不会崩溃。因此,您甚至不知道存在性能问题。幸运的是,流指令只有对齐版本。因此编译器没有“作弊”的余地。 - Mysticial
2个回答

7

当地址对齐时,使用VMOVDQU没有惩罚。在这种情况下,行为与使用VMOVDQA相同。

至于“为什么”,可能没有一个清晰的答案。ICC可能故意这样做,以便稍后使用不对齐的参数调用_do的用户不会崩溃,但也有可能它只是编译器的紧急行为。Intel编译器团队中的某个人可以回答这个问题,我们其他人只能猜测。


谢谢!我完全理解闭源编译器的行为是不可预测的。但由于使用vmovdqu在地址对齐时没有惩罚,因此了解其使用原因并不是非常重要。我更想知道是否我的代码中存在内存对齐的问题。 - benlaug
1
@benlaug:就我所知,我曾与一位可能是ICC工程师的人进行过交谈,他可能说过,每当您没有通过内在函数明确要求对齐时,他们就会使用未对齐的加载,但我对此的记忆非常模糊。 - Stephen Canon
奇怪的是,如果我将 array[i].a = _mm256_set1_epi8(TEST_VAL + i*3 ); ... 替换为 _mm256_store_si256(&(array[i].a), _mm256_set1_epi8(TEST_VAL + i*3 )); ...,icc 仍然使用 vmovdqu - benlaug
2
这个页面(https://software.intel.com/en-us/articles/data-alignment-to-assist-vectorization)说:“它还需要在感兴趣的循环之前使用形式为__assume_aligned(a, 64) [32 in your case]的子句。如果没有这一步,编译器将无法检测使用这些数组的最佳对齐方式。” - Stephen Canon

4
有三个因素在解决更大的问题:
a) 错误行为可能对调试性能有好处,但对生产代码来说不是最好的选择 - 特别是涉及到混合使用第三方库时 - 很少有人会选择在客户现场崩溃而不是略慢一些的软件产品性能。
b) 英特尔微架构从 Nehalem 开始解决了对齐数据性能问题,“未对齐”指令形式和“对齐”形式的性能相同,AMD 甚至在此之前就做到了。
c) AVX+改进了Load + OP形式的架构行为,使其变成了非错误行为,比SSE更好。
VADDPS ymm0, ymm0, ymmword ptr [rax]; // no longer faults when rax is misaligned

由于对于AVX+,我们希望编译器在从内置函数生成代码时仍然有使用独立或Load+OP指令形式的自由,因此对于像这样的代码:

_mm256_add_ps( a, *(__m256*)data_ptr  );

使用AVX+,编译器可以在所有加载操作中使用vMOVUs(VMOVUPS/VMOVUPD/VMOVDQU),并保持与Load+OP形式相同的行为。

当源代码略微更改或相同代码的代码生成更改时(例如在不同编译器/版本之间或由于内联),如果代码生成从Load+OP指令切换到独立的Load和OP指令,则加载操作的行为与Load+OP相同,即非故障状态。

因此,AVX与上述编译器实践以及“非对齐”存储指令形式的使用总体上允许SIMD代码具有统一的非故障行为,而不会在对齐数据上降低性能。

当然,仍有(相对罕见的)针对非临时存储(vMOVNTDQ/vMOVNTPS/vMOVNTPD)以及从WC类型存储器(vMOVNDQA)加载的使用目标指令,这些指令维护对于非对齐地址的故障行为。

-Max Locktyukhin, Intel


AMD K10具有廉价的非对齐加载,但没有存储。即movdqu xmm,[mem]在地址对齐的情况下每个时钟为2个,但是movdqu [mem],xmm存储为每2个时钟1个,而movdqa为每1个时钟1个。(http://agner.org/optimize/) - Peter Cordes
关于实现质量决策使用未对齐加载的问题,即使编译器认为对齐加载是安全的:gcc做出相反的决定,并且在编译时已知足够的对齐保证时使用vmovdqa(即使程序员使用_mm_store而不是storeu做出了错误的承诺)。这在调试时很有用,当您计划对数据进行对齐时,但其他情况下则不需要。您总是使用未对齐形式的原因非常合理。 - Peter Cordes

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