Pio*_*icz 8 c optimization performance assembly gcc
我有以下代码,它将数据从内存复制到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;
}
Run Code Online (Sandbox Code Playgroud)
这就是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>
Run Code Online (Sandbox Code Playgroud)
注意最后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>
Run Code Online (Sandbox Code Playgroud)
通过正确排序的指令,我得到每秒~13 668 313个数据包.因此很明显,通过gcc降低性能引入的重新排序.
你有遇到过吗?这是一个已知的错误还是应该填写错误报告?
编译标志:
-O3 -pipe -g -msse4.1 -mavx
Run Code Online (Sandbox Code Playgroud)
我的gcc版本:
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)
Run Code Online (Sandbox Code Playgroud)
Nom*_*mal 10
我觉得这个问题很有意思.GCC以生成不是最优的代码而闻名,但我发现找到"鼓励"它来生成更好的代码(当然只针对最热门/瓶颈代码)的方法很有吸引力,而不需要过多的微管理.在这个特殊情况下,我查看了三种用于此类情况的"工具":
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];
Run Code Online (Sandbox Code Playgroud)
不幸的是,这对商店没有帮助,因为我们想要发布流媒体商店.A *(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;
}
}
Run Code Online (Sandbox Code Playgroud)
example.c使用GCC-4.8.4 编译它()
gcc -std=c99 -mavx2 -march=x86-64 -mtune=generic -O2 -S example.c
Run Code Online (Sandbox Code Playgroud)
yield(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
Run Code Online (Sandbox Code Playgroud)
实际编译(-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
Run Code Online (Sandbox Code Playgroud)
在没有任何优化的情况下,代码完全令人作呕,充满了不必要的动作,因此需要进行一些优化.(以上用途-O2,通常是我使用的优化级别.)
如果针对size(-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
Run Code Online (Sandbox Code Playgroud)
直到你看到的最后一个jmp是比较,基本上是做一个jmp,cmp以及jae在每次迭代,这可能产生很糟糕的结果.
注意:如果您为实际代码执行类似的操作,请添加注释(特别是对于__asm__ __volatile__ ("");),并记住定期检查所有可用的编译器,以确保代码编译得不是很糟糕.
看看Peter Cordes的优秀答案,我决定进一步迭代这个功能,只是为了好玩.
正如Ross Ridge在评论中提到的那样,当使用_mm256_load_si256()指针时不会取消引用(在重新转换为对齐__m256i *作为函数的参数之前),因此volatile在使用时无效_mm256_load_si256().在另一篇评论中,Seb提出了一个解决方法:_mm256_load_si256((__m256i []){ *(volatile __m256i *)(src) })它src通过一个易失性指针访问元素并将其转换为数组,为函数提供指针.对于简单的对齐加载,我更喜欢直接易失性指针; 它符合我在代码中的意图.(我确实以KISS为目标,虽然我经常只打它的愚蠢部分.)
在x86-64上,内部循环的开始对齐为16个字节,因此函数"header"部分中的操作数量并不重要.尽管如此,一般来说,避免多余的二进制AND(屏蔽要以字节为单位的数量的五个最低有效位)肯定是有用的.
GCC为此提供了两种选择.一个是__builtin_assume_aligned()内置的,它允许程序员将各种对齐信息传递给编译器.另一种是typedef是一种具有额外属性的类型,这里__attribute__((aligned (32)))可以用来传达函数参数的对齐性.这两个都应该在clang中提供(虽然支持是最近的,而不是3.5),并且可能在其他如icc(尽管ICC,AFAIK,使用__assume_aligned())中可用.
减轻GCC注册混乱的一种方法是使用辅助函数.经过一些进一步的迭代,我到达了这个,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));
}
Run Code Online (Sandbox Code Playgroud)
它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
Run Code Online (Sandbox Code Playgroud)
进一步优化-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
Run Code Online (Sandbox Code Playgroud)
即使-Os生成的代码非常好,
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
Run Code Online (Sandbox Code Playgroud)
当然,如果没有优化,GCC-4.8.4仍会产生相当糟糕的代码.随着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
Run Code Online (Sandbox Code Playgroud)
我喜欢的another.c代码(它很适合我的编码风格),我很高兴与通过GCC-4.8.4和铛-3.5生成的代码-O1,-O2,-O3,和-Os两个,所以我认为这是对我不够好.(但是,请注意,我实际上没有对此进行基准测试,因为我没有相关代码.我们使用时间和非时间(nt)内存访问,以及缓存行为(以及缓存与周围环境的交互)代码)对于像这样的事情是至关重要的,所以我认为微观标记是没有意义的.)
首先,普通人使用gcc -O3 -march=native -S然后编辑.s以测试对编译器输出的小修改.我希望你有趣的十六进制编辑改变.:P您还可以使用Agner Fog的优秀objconv功能进行反汇编,可以使用您选择的NASM,YASM,MASM或AT&T语法将其组装回二进制文件.
使用与Nominal Animal相同的一些想法,我制作了一个版本,编译成同样好的asm.我有信心为什么它编译成好的代码,我猜测为什么订单这么重要:
CPU只有少量(~10?)写入组合填充缓冲区用于NT加载/存储.
请参阅此文章,了解如何使用流式加载从视频内存进行复制,以及使用流式存储写入主内存.实际上通过小缓冲区(远小于L1)反弹数据实际上更快,以避免流加载和流存储竞争填充缓冲区(尤其是无序执行).请注意,从正常内存使用"流"NT加载是没有用的.据我了解,流加载仅对I/O有用(包括视频RAM,它映射到Uncacheable Software-Write-Combining(USWC)区域中的CPU地址空间).主存储器RAM映射为WB(写回),因此允许CPU以推测方式预取它并对其进行缓存,这与USWC不同.无论如何,即使我正在链接一篇关于使用流媒体加载的文章,我也不建议使用流媒体加载.这只是为了说明填充缓冲区的争用几乎肯定是gcc奇怪的代码导致一个大问题的原因,而不是正常的非NT存储.
另请参阅John McAlpin在此主题末尾的评论,另一个来源确认WC一次存储到多个缓存行可能会大幅放缓.
gcc的原始代码输出(对于某些我无法想象的脑死亡原因)存储了第一个高速缓存行的第二半,然后是第二个高速缓存行的两半,然后是第一个高速缓存行的第一半.可能有时候第一个高速缓存行的写入组合缓冲区在写入两半之前都会被刷新,从而导致外部总线的使用效率降低.
clang没有对我们的3个版本(我的,OP和Nominal Animal的)中的任何一个进行任何奇怪的重新排序.
无论如何,使用仅停止编译器重新排序但不发出屏障指令的编译器屏障是阻止它的一种方法.在这种情况下,它是一种在头部命中编译器并说"愚蠢的编译器,不要那样做"的方法.我不认为你通常需要在任何地方都这样做,但显然你不能相信gcc与写合并存储(订购真的很重要).因此,在使用NT加载和/或存储时,至少使用您正在开发的编译器来查看asm可能是个好主意. 我已经为gcc报道了这个.理查德比纳指出,这-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不会直接记录所有不同的内存排序选项.C++页面atomic_thread_fence是cppreference上的一个位置,其中有一些示例和更多内容.
这就是我没有使用Nominal Animal将src声明为指向易失性的想法的原因.gcc决定以与商店相同的顺序保持负载.
鉴于此,仅展开2可能不会在微基准测试中产生任何吞吐量差异,并将在生产中节省uop缓存空间.每次迭代仍然会执行完整的缓存行,这似乎很好.
SnB系列CPU不能微融合2-reg寻址模式,因此最小化循环开销(获取指向src和dst结尾的指针,然后将负指数向上计数为零)的明显方法不起作用.商店不会微熔.尽管如此,你很快就会将填充缓冲区填充到额外的uops无关紧要的程度.该循环可能在每个周期几乎没有接近4个uop.
仍然有一种方法可以减少循环开销:使用我可怕的丑陋和不可读的C语言来使编译器只执行一个sub(和a cmp/jcc)作为循环开销,不进行任何展开就会产生4-uop即使在SnB上,每个时钟应该在一次迭代中发出的循环.(注意,它vmovntdq是AVX2,而vmovntps只是AVX1.Clang已经在这段代码中使用vmovaps/ vmovntps用于si256内在函数!它们具有相同的对齐要求,并且不关心它们存储的是什么位.它不保存任何insn字节,只有兼容性.)
请参阅第一段,了解与此相关的一个Godbolt链接.
我猜你在Linux内核中这样做了,所以我输入了适当的#ifdefs,所以这应该是正确的内核代码或者为用户空间编译时.
#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
}
Run Code Online (Sandbox Code Playgroud)
它编译为(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:
Run Code Online (Sandbox Code Playgroud)
Clang做了一个非常相似的循环,但是介绍要长得多:clang并没有假设src并且dest实际上都是对齐的.如果没有32B对齐,它可能没有利用负载和存储会出错的知识?(它知道它可以使用...aps指令而不是...dqa,所以它肯定会对gcc(它们经常变成相关指令)的内在函数进行更多的编译器式优化.clang可以将一对左/右向量移位转换为掩码例如,来自常数.)
| 归档时间: |
|
| 查看次数: |
997 次 |
| 最近记录: |