我想使用增强型REP (ERMSB)为自定义memcpy
获得高带宽。
采用常春藤桥微体系结构引入了ERMSB。如果不知道ERMSB是什么,请参阅英特尔优化手册中的“增强型REP和STOSB操作(ERMSB)”一节。
我知道直接这样做的唯一方法是使用内联程序集。我从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
小得多。__movsb
为15 GB/s,memcpy
为26 GB/s,采用我的i7-6700HQ (Skylake)系统,Ubuntu16.10,DDR4 4@2400 MHz双通道32 GB,GCC 6.2。
为什么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在LLC中产生RFO,rep movsb不.当流到Ivybridge和Haswell上的内存时,rep比movntdqa要快得多(但是要注意,在科特迪瓦桥之前,它是慢的!)
以下是我在锡膜相同系统上的结果。
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
) 启用和不启用SpeedStep,使用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
这表明,使用powersave
,即使禁用了SpeedStep,0.8 GHz
的空闲频率仍然是CPU的时钟。只有在没有performance
的情况下,CPU才能以恒定的频率运行。
我使用sudo cpufreq-set -r performance
(因为cpufreq-set
给出了奇怪的结果)来更改电源设置。这使涡轮机恢复正常,所以我不得不在此之后禁用涡轮机。
发布于 2017-04-20 09:08:45
你说你想:
一个能说明什么时候再培训系统是有用的答案。
但我不确定这是什么意思。查看链接到的3.7.6.1文档,它显式地表示:
使用ERMSB实现memcpy可能无法达到与使用256位或128位AVX替代方案相同的吞吐量水平,这取决于长度和对齐因素。
因此,仅仅因为CPUID
表示对ERMSB的支持,这并不能保证because将是复制内存的最快方式。这只是意味着它不会像以前的CPU那样糟糕。
然而,仅仅因为在某些条件下可以运行更快的替代方案,并不意味着REP是无用的。现在,该指令所产生的性能惩罚已经消失,它可能再次成为有用的指令。
记住,这是一小部分代码(2个字节!)与我见过的一些更复杂的记忆例行公事相比。由于加载和运行大量代码也会造成损失(将其他一些代码从cpu的缓存中抛出),因此AVX等人的“好处”有时会被其对其余代码的影响所抵消。取决于你在做什么。
你还会问:
为什么REP的带宽要低得多?我能做些什么来改善它呢?
不可能“做些什么”来让REP运行得更快。它所做的一切。
如果你想从memcpy中看到更高的速度,你可以找出它的来源。就在外面的某个地方。或者您可以从调试器跟踪到它,并查看实际的代码路径。我的期望是,它使用一些AVX指令一次工作128或256位。
或者你可以..。你让我们别这么说的。
发布于 2017-04-22 13:36:47
这不是一个问题的答案,只有我的结果(和个人的结论)时,试图找出。
总之: GCC已经对memset()
/memmove()
/memcpy()
进行了优化(例如,见GCC源代码中的代表();也可以在同一个文件中查找stringop_algs
,以查看依赖于体系结构的变体)。因此,没有理由期望在GCC中使用您自己的变体(除非您已经忘记了对齐数据的对齐属性之类的重要内容,或者没有启用足够具体的优化,比如-O2 -march= -mtune=
)。如果你同意,那么这个问题的答案在实践中或多或少是不相关的。
(我只希望有一个memrepeat()
,与memcpy()
相反,与memmove()
相反,它会重复缓冲区的初始部分来填充整个缓冲区。)
我目前正在使用一台常春藤桥机(Corei5-6200U膝上型计算机,Linux4.0x86-64内核,带有erms
的/proc/cpuinfo
标志)。因为我想知道是否能够找到基于rep movsb
的自定义memcpy()变量优于简单的memcpy()
的情况,所以我编写了一个过于复杂的基准测试。
其核心思想是主程序分配三个大内存区域:original
、current
和correct
,每个区域大小完全相同,并且至少对页对齐。复制操作被分组为集合,每个集合具有不同的属性,就像所有源和目标被对齐(按照一定数量的字节),或者所有长度都在相同的范围内。每个集合都使用src
、dst
、n
三胞胎数组来描述,其中所有的src
到src+n-1
和dst
到dst+n-1
都完全在current
区域内。
* PRNG用于将original
初始化为随机数据。(就像我前面警告的那样,这太复杂了,但我想确保我不会为编译器留下任何简单的快捷方式。)correct
区域是通过从current
中的original
数据开始,应用当前集合中的所有三胞胎,使用C库提供的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" );
}
会打
static void normal_memcpy(void *dst, const void *src, size_t n)
{
memcpy(dst, src, n);
}
使用gcc -Wall -O2 -march=ivybridge -mtune=ivybridge
,在上面提到的Corei5-6200U笔记本电脑上使用GCC 5.4.0运行Linux4.4.0 64位内核。然而,复制4096字节对齐块和大小块非常接近。
这意味着,至少到目前为止,我还没有发现使用rep movsb
memcpy变体有意义的情况。这并不意味着没有这样的情况,我只是没有找到一个。
(在这一点上,代码是一个意大利面的混乱,我更感到羞愧,而不是骄傲,所以我将省略发布的消息来源,除非有人要求。上面的描述应该足够写一个更好的描述。)
不过,这并不让我感到惊讶。C编译器可以推断出许多关于操作数指针对齐的信息,以及要复制的字节数是否是编译时常量,是两个适当幂的倍数。编译器可以并且将/应该使用此信息来用自己的C库memcpy()
/memmove()
函数替换C库。
GCC正是这样做的(例如,请参阅GCC源代码中的代表();也可以在同一个文件中查找stringop_algs
,以查看依赖于体系结构的变体)。事实上,memcpy()
/memset()
/memmove()
已经为相当多的x86处理器变体进行了单独的优化;如果GCC的开发人员还没有包括erms支持,我会非常惊讶。
GCC提供了几个功能属性,开发人员可以使用这些功能属性来确保生成良好的代码。例如,alloc_align (n)
告诉GCC,函数返回与至少n
字节对齐的内存。应用程序或库可以通过创建“解析器函数”(返回函数指针)和使用ifunc (resolver)
属性定义函数来选择在运行时使用的函数的实现。
我在代码中使用的最常见的模式之一是
some_type *pointer = __builtin_assume_aligned(ptr, alignment);
其中ptr
是一些指针,alignment
是它对齐的字节数;GCC则知道/假设pointer
与alignment
字节对齐。
另一个有用的内置,尽管很难正确使用,是__builtin_prefetch()
。为了最大限度地提高整个带宽/效率,我发现在每个子操作中最小化延迟会产生最好的结果。(要将分散的元素复制到连续的临时存储中,这很困难,因为预取通常涉及完整的缓存行;如果预取的元素太多,则大部分缓存由于存储未使用的项而浪费。)
发布于 2017-04-11 10:34:40
有更有效的方法来移动数据。如今,memcpy
的实现将从编译器中生成特定于体系结构的代码,编译器将根据数据和其他因素的内存对齐性进行优化。这允许在x86世界中更好地使用非时态缓存指令、XMM和其他寄存器。
当您使用硬代码时,rep movsb
会阻止这种对本质的使用。
因此,对于像memcpy
这样的东西,除非您正在编写一些将绑定到非常特定的硬件上的东西,并且除非您要花时间在程序集中编写一个高度优化的memcpy
函数(或者使用C级本质),否则最好让编译器为您解决这个问题。
https://stackoverflow.com/questions/43343231
复制相似问题