英特尔 C 编译器使用未对齐的 SIMD 移动和对齐的内存
Intel C Compiler uses unaligned SIMD moves with aligned memory
我使用的是 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);
}
下面的 ASM 代码是为 _do()
函数生成的:
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 [=11=]x3,%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 [=11=]x60,%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 [=11=]x3e8,%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()
函数生成以下 ASM 代码:
0x0000000000400650 <+0>: lea 0x17700(%rdi),%r9
0x0000000000400657 <+7>: mov [=12=]x2a,%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 [=12=]x60,%rdi
0x0000000000400678 <+40>: lea 0x2(%r8),%edx
0x000000000040067c <+44>: vpbroadcastb %xmm1,%ymm1
0x0000000000400681 <+49>: vmovdqa %ymm2,-0x60(%rdi)
0x0000000000400686 <+54>: add [=12=]x3,%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 使用与 gcc 不同的未对齐移动 (vmovdqu)?
2) 在对齐内存上使用 vmovdqu 而不是 vmovdqa 时是否有惩罚?
P.S: 使用 SSE 时问题相同 instructions/registers。
谢谢
地址对齐时使用 VMOVDQU 没有任何惩罚。在这种情况下,行为与使用 VMOVDQA 相同。
至于"why"可能没有一个明确的答案。 可能 ICC 故意这样做,以便以后使用未对齐的参数调用 _do
的用户不会崩溃,但这也可能只是编译器的紧急行为。英特尔编译器团队有人可以回答这个问题,我们其他人只能推测。
解决更大问题的三个因素在起作用:
a) 错误行为可能有利于调试性能,但对生产代码却没有那么好——尤其是当涉及到第 3 方库的混合时——很少有人会因为他们的软件产品性能稍微慢一点而崩溃客户站点
b) 英特尔微架构解决了从 Nehalem 开始的关于对齐数据性能问题的 "unaligned" 指令形式,它们与 "aligned" 形式的性能相同,我认为 AMD 甚至在此之前就做到了
c) AVX+ 改进了 SSE 上 Load+OP 表单的架构行为,使其无故障,因此
VADDPS ymm0, ymm0, ymmword ptr [rax]; // no longer faults when rax is misaligned
由于对于 AVX+,我们希望编译器在从内在函数生成代码时仍然可以自由使用独立或 Load+OP 指令形式,例如这样的代码:
_mm256_add_ps( a, *(__m256*)data_ptr );
借助 AVX+,编译器可以将 vMOVU (VMOVUPS/VMOVUPD/VMOVDQU) 用于所有加载,并通过 Load+OP 形式保持统一的行为
当源代码略有变化或相同代码的代码生成发生变化(例如在不同 compilers/versions 之间或由于内联)并且代码生成从 Load+OP 指令切换到独立时需要它加载和 OP 指令,加载的行为与加载+OP 相同,即无故障。
因此,具有上述编译器实践的 AVX 和“未对齐”存储指令形式的使用总体上允许 SIMD 代码的统一无错误行为,而不会损失对齐数据的性能。
当然,仍然有(相对较少的)针对非临时存储 (vMOVNTDQ/vMOVNTPS/vMOVNTPD) 的使用目标指令和来自 WC 类型内存 (vMOVNDQA) 的加载,它们保持未对齐地址的错误行为。
-Max Locktyukhin,英特尔
我使用的是 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);
}
下面的 ASM 代码是为 _do()
函数生成的:
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 [=11=]x3,%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 [=11=]x60,%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 [=11=]x3e8,%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()
函数生成以下 ASM 代码:
0x0000000000400650 <+0>: lea 0x17700(%rdi),%r9
0x0000000000400657 <+7>: mov [=12=]x2a,%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 [=12=]x60,%rdi
0x0000000000400678 <+40>: lea 0x2(%r8),%edx
0x000000000040067c <+44>: vpbroadcastb %xmm1,%ymm1
0x0000000000400681 <+49>: vmovdqa %ymm2,-0x60(%rdi)
0x0000000000400686 <+54>: add [=12=]x3,%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 使用与 gcc 不同的未对齐移动 (vmovdqu)?
2) 在对齐内存上使用 vmovdqu 而不是 vmovdqa 时是否有惩罚?
P.S: 使用 SSE 时问题相同 instructions/registers。
谢谢
地址对齐时使用 VMOVDQU 没有任何惩罚。在这种情况下,行为与使用 VMOVDQA 相同。
至于"why"可能没有一个明确的答案。 可能 ICC 故意这样做,以便以后使用未对齐的参数调用 _do
的用户不会崩溃,但这也可能只是编译器的紧急行为。英特尔编译器团队有人可以回答这个问题,我们其他人只能推测。
解决更大问题的三个因素在起作用:
a) 错误行为可能有利于调试性能,但对生产代码却没有那么好——尤其是当涉及到第 3 方库的混合时——很少有人会因为他们的软件产品性能稍微慢一点而崩溃客户站点
b) 英特尔微架构解决了从 Nehalem 开始的关于对齐数据性能问题的 "unaligned" 指令形式,它们与 "aligned" 形式的性能相同,我认为 AMD 甚至在此之前就做到了
c) AVX+ 改进了 SSE 上 Load+OP 表单的架构行为,使其无故障,因此
VADDPS ymm0, ymm0, ymmword ptr [rax]; // no longer faults when rax is misaligned
由于对于 AVX+,我们希望编译器在从内在函数生成代码时仍然可以自由使用独立或 Load+OP 指令形式,例如这样的代码:
_mm256_add_ps( a, *(__m256*)data_ptr );
借助 AVX+,编译器可以将 vMOVU (VMOVUPS/VMOVUPD/VMOVDQU) 用于所有加载,并通过 Load+OP 形式保持统一的行为
当源代码略有变化或相同代码的代码生成发生变化(例如在不同 compilers/versions 之间或由于内联)并且代码生成从 Load+OP 指令切换到独立时需要它加载和 OP 指令,加载的行为与加载+OP 相同,即无故障。
因此,具有上述编译器实践的 AVX 和“未对齐”存储指令形式的使用总体上允许 SIMD 代码的统一无错误行为,而不会损失对齐数据的性能。
当然,仍然有(相对较少的)针对非临时存储 (vMOVNTDQ/vMOVNTPS/vMOVNTPD) 的使用目标指令和来自 WC 类型内存 (vMOVNDQA) 的加载,它们保持未对齐地址的错误行为。
-Max Locktyukhin,英特尔