我想使用增强型REP MOVSB(ERMSB)为自定义memcpy
获取高带宽。
ERMSB引入了Ivy Bridge微体系结构。请参阅"增强型REP MOVSB和STOSB操作(ERMSB)"如果您不知道ERMSB是什么,请在Intel optimization manual中。
我知道直接执行此操作的唯一方法是使用内联汇编。我从https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE
获得了以下功能static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
然而,当我使用它时,带宽远小于memcpy
。
我的i7-6700HQ(Skylake)系统,Ubuntu 16.10,DDR4 @ 2400 MHz双通道32 GB,GCC 6.2,__movsb
获得15 GB / s,memcpy
获得26 GB / s。
为什么带宽REP MOVSB
的带宽要低得多?我该怎么做才能改善它?
这是我用来测试它的代码。
//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>
static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
int main(void) {
int n = 1<<30;
//char *a = malloc(n), *b = malloc(n);
char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
memset(a,2,n), memset(b,1,n);
__movsb(b,a,n);
printf("%d\n", memcmp(b,a,n));
double dtime;
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) __movsb(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) memcpy(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
}
我对rep movsb
感兴趣的原因是基于这些评论
请注意,在Ivybridge和Haswell上,缓冲区大到适合MLC,你可以使用rep movsb击败movntdqa; movntdqa引入RFO到LLC,rep movsb不... 当在Ivybridge和Haswell上流式传输到内存时,rep movsb比movntdqa快得多(但要注意前Ivybridge它很慢!)
What's missing/sub-optimal in this memcpy implementation?
以下是来自tinymembnech的同一系统的结果。
C copy backwards : 7910.6 MB/s (1.4%)
C copy backwards (32 byte blocks) : 7696.6 MB/s (0.9%)
C copy backwards (64 byte blocks) : 7679.5 MB/s (0.7%)
C copy : 8811.0 MB/s (1.2%)
C copy prefetched (32 bytes step) : 9328.4 MB/s (0.5%)
C copy prefetched (64 bytes step) : 9355.1 MB/s (0.6%)
C 2-pass copy : 6474.3 MB/s (1.3%)
C 2-pass copy prefetched (32 bytes step) : 7072.9 MB/s (1.2%)
C 2-pass copy prefetched (64 bytes step) : 7065.2 MB/s (0.8%)
C fill : 14426.0 MB/s (1.5%)
C fill (shuffle within 16 byte blocks) : 14198.0 MB/s (1.1%)
C fill (shuffle within 32 byte blocks) : 14422.0 MB/s (1.7%)
C fill (shuffle within 64 byte blocks) : 14178.3 MB/s (1.0%)
---
standard memcpy : 12784.4 MB/s (1.9%)
standard memset : 30630.3 MB/s (1.1%)
---
MOVSB copy : 8712.0 MB/s (2.0%)
MOVSD copy : 8712.7 MB/s (1.9%)
SSE2 copy : 8952.2 MB/s (0.7%)
SSE2 nontemporal copy : 12538.2 MB/s (0.8%)
SSE2 copy prefetched (32 bytes step) : 9553.6 MB/s (0.8%)
SSE2 copy prefetched (64 bytes step) : 9458.5 MB/s (0.5%)
SSE2 nontemporal copy prefetched (32 bytes step) : 13103.2 MB/s (0.7%)
SSE2 nontemporal copy prefetched (64 bytes step) : 13179.1 MB/s (0.9%)
SSE2 2-pass copy : 7250.6 MB/s (0.7%)
SSE2 2-pass copy prefetched (32 bytes step) : 7437.8 MB/s (0.6%)
SSE2 2-pass copy prefetched (64 bytes step) : 7498.2 MB/s (0.9%)
SSE2 2-pass nontemporal copy : 3776.6 MB/s (1.4%)
SSE2 fill : 14701.3 MB/s (1.6%)
SSE2 nontemporal fill : 34188.3 MB/s (0.8%)
请注意,我的系统SSE2 copy prefetched
也快于MOVSB copy
。
在我原来的测试中,我没有禁用涡轮增压器。我禁用了涡轮增压器并再次进行测试,但似乎没有太大的区别。但是,改变电源管理确实会产生很大的不同。
当我这样做时
sudo cpufreq-set -r -g performance
我有时看到rep movsb
超过20 GB / s。
带
sudo cpufreq-set -r -g powersave
我看到的最好的是大约17 GB / s。但memcpy
似乎对电源管理不敏感。
我检查了频率(使用turbostat
)with and without SpeedStep enabled,performance
和powersave
用于空闲,1核心负载和4核心负载。我运行了英特尔的MKL密集矩阵乘法来创建负载并使用OMP_SET_NUM_THREADS
设置线程数。以下是结果表(以GHz为单位的数字)。
SpeedStep idle 1 core 4 core
powersave OFF 0.8 2.6 2.6
performance OFF 2.6 2.6 2.6
powersave ON 0.8 3.5 3.1
performance ON 3.5 3.5 3.1
这表明即使使用SpeedStep powersave
也禁用了CPU
仍然降至0.8 GHz
的空闲频率。只有performance
没有SpeedStep,CPU才能以恒定频率运行。
我使用例如sudo cpufreq-set -r performance
(因为cpufreq-set
给出了奇怪的结果)来更改电源设置。这会让Turbo重新开启,所以我不得不在之后禁用涡轮增压器。
答案 0 :(得分:7)
你说你想要:
显示ERMSB何时有用的答案
但我不确定这意味着你的意思。查看链接到的3.7.6.1文档,它明确地说:
使用ERMSB实现memcpy可能无法达到与使用256位或128位AVX备选方案相同的吞吐量水平,具体取决于长度和对齐因子。
因为CPUID
表示支持ERMSB,这并不能保证REP MOVSB将是复制内存的最快方法。它只是意味着它不会像以前的CPU一样糟糕。
然而,仅仅因为可能存在替代方案,在某些情况下,运行得更快并不意味着REP MOVSB无用。既然这条指令过去产生的性能损失已经消失,那么它可能是一个有用的指令。
请记住,与我所看到的一些更复杂的memcpy例程相比,它只是一小段代码(2个字节!)。由于加载和运行大块代码也会受到惩罚(将一些其他代码从cpu的缓存中抛出),有时AVX等人的“好处”会被它对其余部分的影响所抵消。码。取决于你在做什么。
你也问:
为什么REP MOVSB的带宽要低得多?我该怎么做才能改善它?
不可能“做某事”使REP MOVSB运行得更快。它做它做的。
如果你想从memcpy看到更高的速度,你可以挖掘它的来源。它在某处。或者您可以从调试器跟踪它并查看正在采用的实际代码路径。我的期望是它使用一些AVX指令一次使用128或256位。
或者你可以......嗯,你让我们不要说出来。
答案 1 :(得分:6)
这不是对所述问题的回答,只是我试图找出答案时的结果(和个人结论)。
总结:GCC已经优化memset()
/ memmove()
/ memcpy()
(请参阅GCC来源中的gcc/config/i386/i386.c:expand_set_or_movmem_via_rep();同时查找stringop_algs
文件以查看依赖于体系结构的变体)。因此,没有理由期望通过GCC使用您自己的变体来获得巨大收益(除非您已经忘记了对齐数据的对齐属性等重要内容,或者没有启用-O2 -march= -mtune=
等充分特定的优化)。如果您同意,那么所述问题的答案在实践中或多或少都无关紧要。
(我只希望有一个memrepeat()
,与memcpy()
相反,与memmove()
相比,它会重复缓冲区的初始部分以填充整个缓冲区。)
我目前正在使用Ivy Bridge机器(Core i5-6200U笔记本电脑,Linux 4.4.0 x86-64内核,erms
标记/proc/cpuinfo
。因为我想知道我是否能找到基于rep movsb
的自定义memcpy()变体优于简单memcpy()
的情况,我写了一个过于复杂的基准。
核心思想是主程序分配三个大的内存区域:original
,current
和correct
,每个区域大小完全相同,并且至少是页面对齐的。复制操作被分组为集合,每个集合具有不同的属性,例如所有源和目标被对齐(到某个字节数),或者所有长度都在相同的范围内。使用src
,dst
,n
三元组数组描述每个集合,其中所有src
到src+n-1
和dst
到{{1完全在dst+n-1
区域内。
Xorshift* PRNG用于将current
初始化为随机数据。 (就像我上面提到的那样,这过于复杂,但我想确保我不会为编译器留下任何简单的快捷方式。)original
区域是从correct
数据开始获得的。 original
,使用C库提供的current
应用当前集合中的所有三元组,并将memcpy()
区域复制到current
。这样可以验证每个基准测试函数的行为是否正确。
每组复制操作使用相同的函数定时很多次,并且这些中间值用于比较。 (在我看来,中位数在基准测试中最有意义,并且提供了合理的语义 - 至少在一半时间内,该函数至少是快速的。)
为避免编译器优化,我让程序在运行时动态加载函数和基准。这些函数都具有相同的格式correct
- 请注意,与void function(void *, const void *, size_t)
和memcpy()
不同,它们不会返回任何内容。基准(命名的复制操作集)是通过函数调用动态生成的(将指针指向memmove()
区域,其大小作为参数等)。
不幸的是,我还没有找到任何设置
current
会击败
static void rep_movsb(void *dst, const void *src, size_t n)
{
__asm__ __volatile__ ( "rep movsb\n\t"
: "+D" (dst), "+S" (src), "+c" (n)
:
: "memory" );
}
在前面提到的运行linux-4.4.0 64位内核的Core i5-6200U笔记本电脑上使用GCC 5.4.0 static void normal_memcpy(void *dst, const void *src, size_t n)
{
memcpy(dst, src, n);
}
。然而,复制4096字节对齐和大小的块非常接近。
这意味着至少到目前为止,我还没有找到使用gcc -Wall -O2 -march=ivybridge -mtune=ivybridge
memcpy变体有意义的情况。这并不意味着没有这种情况;我还没找到一个。
(此时代码是一个意大利面条混乱,我感到羞耻而不感到骄傲,所以除非有人问,否则我将省略发布消息来源。但上面的描述应该足以写出更好的消息。)
但这并不让我感到惊讶。 C编译器可以推断出很多关于操作数指针对齐的信息,以及要复制的字节数是否是编译时常量,是2的合适幂的倍数。编译器可以并且将/应该使用此信息将C库rep movsb
/ memcpy()
函数替换为自己的函数。
GCC就是这样做的(请参阅GCC源代码中的gcc/config/i386/i386.c:expand_set_or_movmem_via_rep();同时在同一文件中查找memmove()
以查看与体系结构相关的变体)。实际上,stringop_algs
/ memcpy()
/ memset()
已针对相当多的x86处理器变体进行了单独优化;如果GCC开发人员还没有包含erms支持,我会感到非常惊讶。
GCC提供了几个function attributes,开发人员可以使用它来确保生成良好的代码。例如,memmove()
告诉GCC该函数返回的内存至少与alloc_align (n)
个字节对齐。应用程序或库可以通过创建&#34;解析器函数来选择在运行时使用哪个函数实现。 (返回函数指针),并使用n
属性定义函数。
我在代码中使用的最常见模式之一是
ifunc (resolver)
其中some_type *pointer = __builtin_assume_aligned(ptr, alignment);
是某个指针,ptr
是它对齐的字节数;然后,GCC知道/假设alignment
与pointer
字节对齐。
另一个有用的内置功能,虽然更难以正确使用 ,但是__builtin_prefetch()
。为了最大化整体带宽/效率,我发现最小化每个子操作中的延迟会产生最佳结果。 (为了将分散的元素复制到连续的临时存储中,这很困难,因为预取通常涉及完整的缓存行;如果预取了太多的元素,则通过存储未使用的项来浪费大部分缓存。)
答案 2 :(得分:3)
有更有效的方法来移动数据。目前,memcpy
的实现将从编译器生成体系结构特定代码,该代码基于数据的内存对齐和其他因素进行优化。这样可以更好地在x86世界中使用非临时缓存指令和XMM以及其他寄存器。
当您使用硬编码rep movsb
阻止使用内在函数时。
因此,对于类似memcpy
的内容,除非您正在编写与特定硬件相关的内容,除非您打算花时间编写高度优化的memcpy
在程序集中使用函数(或使用C级内在函数),你远更好地允许编译器为你解决它。
答案 3 :(得分:1)
作为一般memcpy()
指南:
a)如果被复制的数据很小(少于20个字节)并且具有固定大小,那么让编译器执行此操作。原因:编译器可以使用正常的mov
指令并避免启动开销。
b)如果要复制的数据很小(小于约4 KiB)且保证对齐,请使用rep movsb
(如果支持ERMSB)或rep movsd
(如果不支持ERMSB) )。原因:使用SSE或AVX替代方案有大量的启动开销&#34;在它复制之前。
c)如果要复制的数据很小(小于约4 KiB)且无法保证对齐,请使用rep movsb
。原因:使用SSE或AVX,或者使用rep movsd
作为大部分内容加上开头或结尾的rep movsb
,会产生太多开销。
d)对于所有其他情况,请使用以下内容:
mov edx,0
.again:
pushad
.nextByte:
pushad
popad
mov al,[esi]
pushad
popad
mov [edi],al
pushad
popad
inc esi
pushad
popad
inc edi
pushad
popad
loop .nextByte
popad
inc edx
cmp edx,1000
jb .again
原因:这将是如此之慢,以至于它将迫使程序员找到一种不会涉及复制大量数据的替代方案;由于避免了复制大量数据,因此生成的软件速度会明显加快。