我有以下代码,它将数据从内存复制到DMA缓冲区:
for (; likely(l > 0); l-=128)
{
__m256i m0 = _mm256_load_si256( (__m256i*) (src) );
__m256i m1 = _mm256_load_si256( (__m256i*) (src+32) );
__m256i m2 = _mm256_load_si256( (__m256i*) (src+64) );
__m256i m3 = _mm256_load_si256( (__m256i*) (src+96) );
_mm256_stream_si256( (__m256i *) (dst), m0 );
_mm256_stream_si256( (__m256i *) (dst+32), m1 );
_mm256_stream_si256( (__m256i *) (dst+64), m2 );
_mm256_stream_si256( (__m256i *) (dst+96), m3 );
src += 128;
dst += 128;
}
这就是gcc
程序集输出的样子:
405280: c5 fd 6f 50 20 vmovdqa 0x20(%rax),%ymm2
405285: c5 fd 6f 48 40 vmovdqa 0x40(%rax),%ymm1
40528a: c5 fd 6f 40 60 vmovdqa 0x60(%rax),%ymm0
40528f: c5 fd 6f 18 vmovdqa (%rax),%ymm3
405293: 48 83 e8 80 sub $0xffffffffffffff80,%rax
405297: c5 fd e7 52 20 vmovntdq %ymm2,0x20(%rdx)
40529c: c5 fd e7 4a 40 vmovntdq %ymm1,0x40(%rdx)
4052a1: c5 fd e7 42 60 vmovntdq %ymm0,0x60(%rdx)
4052a6: c5 fd e7 1a vmovntdq %ymm3,(%rdx)
4052aa: 48 83 ea 80 sub $0xffffffffffffff80,%rdx
4052ae: 48 39 c8 cmp %rcx,%rax
4052b1: 75 cd jne 405280 <sender_body+0x6e0>
请注意上次vmovdqa
和vmovntdq
指令的重新排序。使用上面生成的gcc
代码,我可以在我的应用程序中达到每秒~10 227 571个数据包的吞吐量。
接下来,我在hexeditor中手动重新排序指令。这意味着现在循环看起来如下:
405280: c5 fd 6f 18 vmovdqa (%rax),%ymm3
405284: c5 fd 6f 50 20 vmovdqa 0x20(%rax),%ymm2
405289: c5 fd 6f 48 40 vmovdqa 0x40(%rax),%ymm1
40528e: c5 fd 6f 40 60 vmovdqa 0x60(%rax),%ymm0
405293: 48 83 e8 80 sub $0xffffffffffffff80,%rax
405297: c5 fd e7 1a vmovntdq %ymm3,(%rdx)
40529b: c5 fd e7 52 20 vmovntdq %ymm2,0x20(%rdx)
4052a0: c5 fd e7 4a 40 vmovntdq %ymm1,0x40(%rdx)
4052a5: c5 fd e7 42 60 vmovntdq %ymm0,0x60(%rdx)
4052aa: 48 83 ea 80 sub $0xffffffffffffff80,%rdx
4052ae: 48 39 c8 cmp %rcx,%rax
4052b1: 75 cd jne 405280 <sender_body+0x6e0>
使用正确排序的指令,我每秒得到~13 668 313个数据包。很明显,gcc
引入的重新排序会降低性能。
编译标志:
-O3 -pipe -g -msse4.1 -mavx
我的gcc版本:
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
答案 0 :(得分:10)
我觉得这个问题很有意思。海湾合作委员会以产生不是最优的代码而闻名,但我发现找到鼓励&#34;的方法很有吸引力。它可以产生更好的代码(当然只用于最热门/瓶颈代码),而不需要过多的微管理。在这个特殊情况下,我查看了三个&#34;工具&#34;我用于这种情况:
volatile
:如果重要的是内存访问按特定顺序发生,那么volatile
是一个合适的工具。请注意,它可能过度,并且每次取消引用volatile
指针时都会导致单独加载。
SSE / AVX加载/存储内在函数不能与volatile
指针一起使用,因为它们是函数。使用类似_mm256_load_si256((volatile __m256i *)src);
的内容会将其隐式转换为const __m256i*
,从而失去volatile
限定符。
我们可以直接取消引用volatile指针。 (只有当我们需要告诉编译器数据可能是未对齐的,或者我们想要一个流媒体存储时,才需要加载/存储内在函数。)
m0 = ((volatile __m256i *)src)[0];
m1 = ((volatile __m256i *)src)[1];
m2 = ((volatile __m256i *)src)[2];
m3 = ((volatile __m256i *)src)[3];
不幸的是,这对商店没有帮助,因为我们想要发行流媒体商店。 *(volatile...)dst = tmp;
不会给我们提供我们想要的东西。
__asm__ __volatile__ ("");
作为编译器重新排序障碍。
这是GNU C编写的一个编译器内存屏障。 (停止编译时重新排序而不发出像mfence
这样的实际屏障指令。它阻止编译器在此语句中重新排序内存访问。
使用循环结构的索引限制。
GCC以糟糕的寄存器使用而闻名。早期版本在寄存器之间进行了许多不必要的移动,尽管现在这种移动很少。但是,在许多版本的GCC上对x86-64进行测试表明,在循环中,最好使用索引限制而不是独立的循环变量来获得最佳结果。
结合以上所有内容,我构建了以下函数(经过几次迭代):
#include <stdlib.h>
#include <immintrin.h>
#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)
void copy(void *const destination, const void *const source, const size_t bytes)
{
__m256i *dst = (__m256i *)destination;
const __m256i *src = (const __m256i *)source;
const __m256i *end = (const __m256i *)source + bytes / sizeof (__m256i);
while (likely(src < end)) {
const __m256i m0 = ((volatile const __m256i *)src)[0];
const __m256i m1 = ((volatile const __m256i *)src)[1];
const __m256i m2 = ((volatile const __m256i *)src)[2];
const __m256i m3 = ((volatile const __m256i *)src)[3];
_mm256_stream_si256( dst, m0 );
_mm256_stream_si256( dst + 1, m1 );
_mm256_stream_si256( dst + 2, m2 );
_mm256_stream_si256( dst + 3, m3 );
__asm__ __volatile__ ("");
src += 4;
dst += 4;
}
}
使用
使用GCC-4.8.4编译它(example.c
)
gcc -std=c99 -mavx2 -march=x86-64 -mtune=generic -O2 -S example.c
收益率(example.s
):
.file "example.c"
.text
.p2align 4,,15
.globl copy
.type copy, @function
copy:
.LFB993:
.cfi_startproc
andq $-32, %rdx
leaq (%rsi,%rdx), %rcx
cmpq %rcx, %rsi
jnb .L5
movq %rsi, %rax
movq %rdi, %rdx
.p2align 4,,10
.p2align 3
.L4:
vmovdqa (%rax), %ymm3
vmovdqa 32(%rax), %ymm2
vmovdqa 64(%rax), %ymm1
vmovdqa 96(%rax), %ymm0
vmovntdq %ymm3, (%rdx)
vmovntdq %ymm2, 32(%rdx)
vmovntdq %ymm1, 64(%rdx)
vmovntdq %ymm0, 96(%rdx)
subq $-128, %rax
subq $-128, %rdx
cmpq %rax, %rcx
ja .L4
vzeroupper
.L5:
ret
.cfi_endproc
.LFE993:
.size copy, .-copy
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
.section .note.GNU-stack,"",@progbits
实际编译的(-c
代替-S
)代码的反汇编是
0000000000000000 <copy>:
0: 48 83 e2 e0 and $0xffffffffffffffe0,%rdx
4: 48 8d 0c 16 lea (%rsi,%rdx,1),%rcx
8: 48 39 ce cmp %rcx,%rsi
b: 73 41 jae 4e <copy+0x4e>
d: 48 89 f0 mov %rsi,%rax
10: 48 89 fa mov %rdi,%rdx
13: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
18: c5 fd 6f 18 vmovdqa (%rax),%ymm3
1c: c5 fd 6f 50 20 vmovdqa 0x20(%rax),%ymm2
21: c5 fd 6f 48 40 vmovdqa 0x40(%rax),%ymm1
26: c5 fd 6f 40 60 vmovdqa 0x60(%rax),%ymm0
2b: c5 fd e7 1a vmovntdq %ymm3,(%rdx)
2f: c5 fd e7 52 20 vmovntdq %ymm2,0x20(%rdx)
34: c5 fd e7 4a 40 vmovntdq %ymm1,0x40(%rdx)
39: c5 fd e7 42 60 vmovntdq %ymm0,0x60(%rdx)
3e: 48 83 e8 80 sub $0xffffffffffffff80,%rax
42: 48 83 ea 80 sub $0xffffffffffffff80,%rdx
46: 48 39 c1 cmp %rax,%rcx
49: 77 cd ja 18 <copy+0x18>
4b: c5 f8 77 vzeroupper
4e: c3 retq
没有任何优化,代码完全令人作呕,充满了不必要的动作,因此需要进行一些优化。 (以上使用-O2
,这通常是我使用的优化级别。)
如果针对大小(-Os
)进行优化,乍一看代码看起来很棒,
0000000000000000 <copy>:
0: 48 83 e2 e0 and $0xffffffffffffffe0,%rdx
4: 48 01 f2 add %rsi,%rdx
7: 48 39 d6 cmp %rdx,%rsi
a: 73 30 jae 3c <copy+0x3c>
c: c5 fd 6f 1e vmovdqa (%rsi),%ymm3
10: c5 fd 6f 56 20 vmovdqa 0x20(%rsi),%ymm2
15: c5 fd 6f 4e 40 vmovdqa 0x40(%rsi),%ymm1
1a: c5 fd 6f 46 60 vmovdqa 0x60(%rsi),%ymm0
1f: c5 fd e7 1f vmovntdq %ymm3,(%rdi)
23: c5 fd e7 57 20 vmovntdq %ymm2,0x20(%rdi)
28: c5 fd e7 4f 40 vmovntdq %ymm1,0x40(%rdi)
2d: c5 fd e7 47 60 vmovntdq %ymm0,0x60(%rdi)
32: 48 83 ee 80 sub $0xffffffffffffff80,%rsi
36: 48 83 ef 80 sub $0xffffffffffffff80,%rdi
3a: eb cb jmp 7 <copy+0x7>
3c: c3 retq
直到您注意到最后一次jmp
是比较,基本上每次迭代都会执行jmp
,cmp
和jae
,这可能会导致相当差结果
注意:如果您为实际代码执行类似的操作,请添加注释(尤其是__asm__ __volatile__ ("");
),并记住定期检查所有可用的编译器,以确保代码不会编译任何人都很糟糕。
看Peter Cordes' excellent answer,我决定进一步迭代这个函数,只是为了好玩。
正如Ross Ridge在评论中提到的那样,当使用_mm256_load_si256()
时,指针未被解除引用(在重新转换为对齐__m256i *
作为函数的参数之前),因此{{1}使用volatile
时无法提供帮助。在另一篇评论中,Seb提出了一个解决方法:_mm256_load_si256()
,它通过一个易失性指针访问该元素并将其转换为数组,为函数提供指向_mm256_load_si256((__m256i []){ *(volatile __m256i *)(src) })
的指针。对于简单的对齐加载,我更喜欢直接易失性指针;它符合我在代码中的意图。 (我确实以KISS为目标,虽然我经常只打它的愚蠢部分。)
在x86-64上,内部循环的开始对齐为16个字节,因此函数&#34; header&#34;中的操作数量。部分并不重要。尽管如此,避免多余的二进制AND(屏蔽要以字节为单位的数量的五个最低有效位)一般都是有用的。
GCC为此提供了两种选择。一个是内置的__builtin_assume_aligned()
,它允许程序员将各种对齐信息传递给编译器。另一种是typedef&#39;这是一种具有额外属性的类型,例如src
,它可用于传达函数参数的对齐性。这两个都应该在clang中可用(尽管支持是最近的,而不是3.5),并且可能在其他如icc中可用(尽管ICC,AFAIK,使用__attribute__((aligned (32)))
)。
减轻GCC注册混乱的一种方法是使用辅助函数。经过一些进一步的迭代,我到达了这个,__assume_aligned()
:
another.c
基本上用#include <stdlib.h>
#include <immintrin.h>
#define likely(x) __builtin_expect((x), 1)
#define unlikely(x) __builtin_expect((x), 0)
#if (__clang_major__+0 >= 3)
#define IS_ALIGNED(x, n) ((void *)(x))
#elif (__GNUC__+0 >= 4)
#define IS_ALIGNED(x, n) __builtin_assume_aligned((x), (n))
#else
#define IS_ALIGNED(x, n) ((void *)(x))
#endif
typedef __m256i __m256i_aligned __attribute__((aligned (32)));
void do_copy(register __m256i_aligned *dst,
register volatile __m256i_aligned *src,
register __m256i_aligned *end)
{
do {
register const __m256i m0 = src[0];
register const __m256i m1 = src[1];
register const __m256i m2 = src[2];
register const __m256i m3 = src[3];
__asm__ __volatile__ ("");
_mm256_stream_si256( dst, m0 );
_mm256_stream_si256( dst + 1, m1 );
_mm256_stream_si256( dst + 2, m2 );
_mm256_stream_si256( dst + 3, m3 );
__asm__ __volatile__ ("");
src += 4;
dst += 4;
} while (likely(src < end));
}
void copy(void *dst, const void *src, const size_t bytes)
{
if (bytes < 128)
return;
do_copy(IS_ALIGNED(dst, 32),
IS_ALIGNED(src, 32),
IS_ALIGNED((void *)((char *)src + bytes), 32));
}
编译(为简洁省略了注释和指令):
gcc -march=x86-64 -mtune=generic -mavx2 -O2 -S another.c
do_copy:
.L3:
vmovdqa (%rsi), %ymm3
vmovdqa 32(%rsi), %ymm2
vmovdqa 64(%rsi), %ymm1
vmovdqa 96(%rsi), %ymm0
vmovntdq %ymm3, (%rdi)
vmovntdq %ymm2, 32(%rdi)
vmovntdq %ymm1, 64(%rdi)
vmovntdq %ymm0, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rdx, %rsi
jb .L3
vzeroupper
ret
copy:
cmpq $127, %rdx
ja .L8
rep ret
.L8:
addq %rsi, %rdx
jmp do_copy
的进一步优化只是内联辅助函数,
-O3
甚至使用do_copy:
.L3:
vmovdqa (%rsi), %ymm3
vmovdqa 32(%rsi), %ymm2
vmovdqa 64(%rsi), %ymm1
vmovdqa 96(%rsi), %ymm0
vmovntdq %ymm3, (%rdi)
vmovntdq %ymm2, 32(%rdi)
vmovntdq %ymm1, 64(%rdi)
vmovntdq %ymm0, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rdx, %rsi
jb .L3
vzeroupper
ret
copy:
cmpq $127, %rdx
ja .L10
rep ret
.L10:
leaq (%rsi,%rdx), %rax
.L8:
vmovdqa (%rsi), %ymm3
vmovdqa 32(%rsi), %ymm2
vmovdqa 64(%rsi), %ymm1
vmovdqa 96(%rsi), %ymm0
vmovntdq %ymm3, (%rdi)
vmovntdq %ymm2, 32(%rdi)
vmovntdq %ymm1, 64(%rdi)
vmovntdq %ymm0, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rsi, %rax
ja .L8
vzeroupper
ret
生成的代码非常好,
-Os
当然,如果没有优化,GCC-4.8.4仍会产生相当糟糕的代码。使用do_copy:
.L3:
vmovdqa (%rsi), %ymm3
vmovdqa 32(%rsi), %ymm2
vmovdqa 64(%rsi), %ymm1
vmovdqa 96(%rsi), %ymm0
vmovntdq %ymm3, (%rdi)
vmovntdq %ymm2, 32(%rdi)
vmovntdq %ymm1, 64(%rdi)
vmovntdq %ymm0, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rdx, %rsi
jb .L3
ret
copy:
cmpq $127, %rdx
jbe .L5
addq %rsi, %rdx
jmp do_copy
.L5:
ret
和clang-3.5 -march=x86-64 -mtune=generic -mavx2 -O2
,我们基本上得到
-Os
我喜欢do_copy:
.LBB0_1:
vmovaps (%rsi), %ymm0
vmovaps 32(%rsi), %ymm1
vmovaps 64(%rsi), %ymm2
vmovaps 96(%rsi), %ymm3
vmovntps %ymm0, (%rdi)
vmovntps %ymm1, 32(%rdi)
vmovntps %ymm2, 64(%rdi)
vmovntps %ymm3, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rdx, %rsi
jb .LBB0_1
vzeroupper
retq
copy:
cmpq $128, %rdx
jb .LBB1_3
addq %rsi, %rdx
.LBB1_2:
vmovaps (%rsi), %ymm0
vmovaps 32(%rsi), %ymm1
vmovaps 64(%rsi), %ymm2
vmovaps 96(%rsi), %ymm3
vmovntps %ymm0, (%rdi)
vmovntps %ymm1, 32(%rdi)
vmovntps %ymm2, 64(%rdi)
vmovntps %ymm3, 96(%rdi)
subq $-128, %rsi
subq $-128, %rdi
cmpq %rdx, %rsi
jb .LBB1_2
.LBB1_3:
vzeroupper
retq
代码(它符合我的编码风格),我对GCC-4.8.4和clang-3.5在another.c
生成的代码感到满意,{{两者都有1}},-O1
和-O2
,所以我认为这对我来说已经足够了。 (但请注意,我实际上并没有对此进行基准测试,因为我没有相关代码。我们使用时间和非时间(nt)内存访问,以及缓存行为(和缓存)与周围代码的交互)对于这样的事情是至关重要的,所以我认为对微基准测试是没有意义的。)
答案 1 :(得分:5)
首先,普通人使用gcc -O3 -march=native -S
然后编辑.s
来测试对编译器输出的小修改。我希望你有趣的十六进制编辑改变。 :P您也可以使用Agner Fog的优秀objconv
来进行反汇编,可以使用您选择的NASM,YASM,MASM或AT&amp; T语法将其组合成二进制文件。
使用与Nominal Animal相同的一些想法,我制作了a version that compiles to similarly good asm。我对为什么编译成好的代码充满信心,但我猜测为什么订购非常重要:
CPU只有少量(~10?)write-combining fill buffers for NT loads / stores。
见this article about copying from video memory with streaming loads, and writing to main memory with streaming stores。实际上通过小缓冲区(远小于L1)反弹数据实际上更快,以避免流加载和流存储竞争填充缓冲区(尤其是无序执行)。请注意,使用&#34;流媒体&#34;来自普通内存的NT加载无用。据我了解,流加载仅对I / O有用(包括视频RAM,它被映射到Uncacheable Software-Write-Combining(USWC)区域中的CPU地址空间)。主存储器RAM映射为WB(写回),因此允许CPU以推测方式预取它并对其进行缓存,这与USWC不同。无论如何,即使我正在链接一篇关于使用流媒体负载的文章,我也没有建议使用流媒体加载。它只是为了说明填充缓冲区的争用几乎肯定是gcc的奇怪代码导致一个大问题的原因,它不适用于普通的非NT存储。
另见John McAlpin在this thread结束时的评论,另一个来源确认WC一次存储到多个缓存行可能会大幅放缓。
gcc的原始代码输出(对于某些我无法想象的脑死亡原因)存储了第一个缓存行的后半部分,然后是第二个缓存行的两半,然后是第一个缓存行的前半部分超高速缓存行。可能有时候第一个高速缓存行的写入组合缓冲区在写入两半之前都会被刷新,导致外部总线的使用效率降低。
clang并没有对我们的3个版本(我的,OP和Nominal Animal&#39;)中的任何一个进行任何奇怪的重新订购。
无论如何,使用compiler-only barriers that stop compiler reordering但不发出屏障指令是阻止它的一种方法。在这种情况下,它是一种在头部命中编译器的方式,并说'#s;愚蠢的编译器,不要那么做&#34;。我不认为你通常需要在任何地方都这样做,但显然你不能信任gcc与写组合商店(订购真的很重要)。因此,至少使用您在使用NT加载和/或存储时使用的编译器来查看asm可能是个好主意。 I've reported this for gcc。 Richard Biener指出-fno-schedule-insns2
是一种解决方法。
Linux(内核)已经有一个barrier()
宏,它充当编译器内存屏障。它几乎肯定只是一个GNU asm volatile("")
。在Linux之外,您可以继续使用该GNU扩展,或者您可以使用C11 stdatomic.h
工具。它们与C ++ 11 std::atomic
工具基本相同,AFAIK语义相同(谢天谢地)。
我在每家商店之间设置了一道隔离墙,因为无论如何都没有有用的重新排序,他们就会自由。事实证明,循环中只有一个屏障可以很好地保持一切顺序,这正是Nominal Animal的答案所做的。它实际上并不允许编译器重新排序没有隔离它们的屏障的商店;编译器只是选择不。这就是我在每家商店之间徘徊的原因。
我只问编译器一个写屏障,因为我希望只有NT存储的顺序才有意义,而不是负载。即使是交替的加载和存储指令也许并不重要,因为OOO执行无论如何都会管理所有内容。 (请注意,英特尔copy-from-video-mem文章甚至使用mfence
来避免在进行流媒体存储和流媒体加载之间重叠。)
atomic_signal_fence
并没有直接记录所有不同的内存排序选项。 atomic_thread_fence
的C ++页面是cppreference上的一个位置,其中有一些示例和更多内容。
这就是我没有使用Nominal Animal将src声明为指向易失性的想法的原因。 gcc决定以与商店相同的顺序保持负载。
鉴于此,仅展开2可能不会在微基准测试中产生任何吞吐量差异,并且将在生产中节省uop缓存空间。每次迭代仍然会执行完整的缓存行,这似乎很好。
SnB-family CPUs can't micro-fuse 2-reg addressing modes,所以最小化循环开销(获取指向src和dst结尾的指针,然后将负索引向上计数为零)的明显方法并不起作用。商店不会微熔。尽管如此,你很快就会将补充缓冲区填满到额外的微不足道的地步。该循环可能在每个循环中几乎没有接近4个uop。
仍然有一种方法可以减少循环开销:使用我可笑的丑陋且难以理解的C语言来使编译器只执行一个sub
(以及cmp/jcc
)循环开销,根本没有展开会产生一个4-uop循环,即使在SnB上也应该在每个时钟的一次迭代中发出。 (请注意,vmovntdq
是AVX2,而vmovntps
只是AVX1.Clang已在此代码中使用vmovaps
/ vmovntps
作为si256
内在函数!他们拥有相同的对齐要求,并不关心它们存储的位数。它不会保存任何insn字节,只能保存兼容性。)
请参阅第一段,了解与此相关的一个Godbolt链接。
我猜你在Linux内核中这样做了,所以我输入了适当的#ifdef
s,所以这应该是正确的内核代码或者为用户空间编译时。
#include <stdint.h>
#include <immintrin.h>
#ifdef __KERNEL__ // linux has it's own macro
//#define compiler_writebarrier() __asm__ __volatile__ ("")
#define compiler_writebarrier() barrier()
#else
// Use C11 instead of a GNU extension, for portability to other compilers
#include <stdatomic.h>
// unlike a single store-release, a release barrier is a StoreStore barrier.
// It stops all earlier writes from being delayed past all following stores
// Note that this is still only a compiler barrier, so no SFENCE is emitted,
// even though we're using NT stores. So from another core's perpsective, our
// stores can become globally out of order.
#define compiler_writebarrier() atomic_signal_fence(memory_order_release)
// this purposely *doesn't* stop load reordering.
// In this case gcc loads in the same order it stores, regardless. load ordering prob. makes much less difference
#endif
void copy_pjc(void *const destination, const void *const source, const size_t bytes)
{
__m256i *dst = destination;
const __m256i *src = source;
const __m256i *dst_endp = (destination + bytes); // clang 3.7 goes berserk with intro code with this end condition
// but with gcc it saves an AND compared to Nominal's bytes/32:
// const __m256i *dst_endp = dst + bytes/sizeof(*dst); // force the compiler to mask to a round number
#ifdef __KERNEL__
kernel_fpu_begin(); // or preferably higher in the call tree, so lots of calls are inside one pair
#endif
// bludgeon the compiler into generating loads with two-register addressing modes like [rdi+reg], and stores to [rdi]
// saves one sub instruction in the loop.
//#define ADDRESSING_MODE_HACK
//intptr_t src_offset_from_dst = (src - dst);
// generates clunky intro code because gcc can't assume void pointers differ by a multiple of 32
while (dst < dst_endp) {
#ifdef ADDRESSING_MODE_HACK
__m256i m0 = _mm256_load_si256( (dst + src_offset_from_dst) + 0 );
__m256i m1 = _mm256_load_si256( (dst + src_offset_from_dst) + 1 );
__m256i m2 = _mm256_load_si256( (dst + src_offset_from_dst) + 2 );
__m256i m3 = _mm256_load_si256( (dst + src_offset_from_dst) + 3 );
#else
__m256i m0 = _mm256_load_si256( src + 0 );
__m256i m1 = _mm256_load_si256( src + 1 );
__m256i m2 = _mm256_load_si256( src + 2 );
__m256i m3 = _mm256_load_si256( src + 3 );
#endif
_mm256_stream_si256( dst+0, m0 );
compiler_writebarrier(); // even one barrier is enough to stop gcc 5.3 reordering anything
_mm256_stream_si256( dst+1, m1 );
compiler_writebarrier(); // but they're completely free because we are sure this store ordering is already optimal
_mm256_stream_si256( dst+2, m2 );
compiler_writebarrier();
_mm256_stream_si256( dst+3, m3 );
compiler_writebarrier();
src += 4;
dst += 4;
}
#ifdef __KERNEL__
kernel_fpu_end();
#endif
}
编译为(gcc 5.3.0 -O3 -march=haswell
):
copy_pjc:
# one insn shorter than Nominal Animal's: doesn't mask the count to a multiple of 32.
add rdx, rdi # dst_endp, destination
cmp rdi, rdx # dst, dst_endp
jnb .L7 #,
.L5:
vmovdqa ymm3, YMMWORD PTR [rsi] # MEM[base: src_30, offset: 0B], MEM[base: src_30, offset: 0B]
vmovdqa ymm2, YMMWORD PTR [rsi+32] # D.26928, MEM[base: src_30, offset: 32B]
vmovdqa ymm1, YMMWORD PTR [rsi+64] # D.26928, MEM[base: src_30, offset: 64B]
vmovdqa ymm0, YMMWORD PTR [rsi+96] # D.26928, MEM[base: src_30, offset: 96B]
vmovntdq YMMWORD PTR [rdi], ymm3 #* dst, MEM[base: src_30, offset: 0B]
vmovntdq YMMWORD PTR [rdi+32], ymm2 #, D.26928
vmovntdq YMMWORD PTR [rdi+64], ymm1 #, D.26928
vmovntdq YMMWORD PTR [rdi+96], ymm0 #, D.26928
sub rdi, -128 # dst,
sub rsi, -128 # src,
cmp rdx, rdi # dst_endp, dst
ja .L5 #,
vzeroupper
.L7:
Clang做了一个非常相似的循环,但介绍要长得多:clang并不认为src
和dest
实际上都是对齐的。也许它没有利用加载和存储如果不是32B对齐的故障的知识? (它知道它可以使用...aps
指令而不是...dqa
,所以它肯定会对gcc(它们通常总是变成相关指令)的内在函数进行更多编译器式优化.clang可以转例如,左/右矢量对从常数转换为掩码。)