首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >增强REP MOVSB用于memcpy

增强REP MOVSB用于memcpy
EN

Stack Overflow用户
提问于 2017-04-11 10:22:09
回答 4查看 26.1K关注 0票数 88

我想使用增强型REP (ERMSB)为自定义memcpy获得高带宽。

采用常春藤桥微体系结构引入了ERMSB。如果不知道ERMSB是什么,请参阅英特尔优化手册中的“增强型REP和STOSB操作(ERMSB)”一节。

我知道直接这样做的唯一方法是使用内联程序集。我从fE获得了以下函数

代码语言:javascript
运行
复制
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**的带宽比低得多我能做些什么来改善它?**

下面是我用来测试这个的代码。

代码语言:javascript
运行
复制
//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要快得多(但是要注意,在科特迪瓦桥之前,它是慢的!)

在这个memcpy实现中缺少什么/次优?

以下是我在锡膜相同系统上的结果。

代码语言:javascript
运行
复制
 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快。

在我最初的测试中,我没有禁用涡轮。我禁用了涡轮,并再次测试,这似乎并没有太大的区别。然而,改变权力管理确实产生了很大的不同。

当我这么做

代码语言:javascript
运行
复制
sudo cpufreq-set -r -g performance

我有时在rep movsb中看到超过20 GB/s。

使用

代码语言:javascript
运行
复制
sudo cpufreq-set -r -g powersave

我看到最好的是17 GB/s,但是memcpy似乎对电源管理不敏感。

我检查了频率(使用turbostat) 启用和不启用SpeedStep,使用performancepowersave检查空闲、1核心负载和4核心负载。我运行英特尔的MKL密集矩阵乘法来创建一个负载,并使用OMP_SET_NUM_THREADS设置线程数。这是一个结果表(GHz中的数字)。

代码语言:javascript
运行
复制
              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给出了奇怪的结果)来更改电源设置。这使涡轮机恢复正常,所以我不得不在此之后禁用涡轮机。

EN

回答 4

Stack Overflow用户

发布于 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位。

或者你可以..。你让我们别这么说的。

票数 9
EN

Stack Overflow用户

发布于 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()的情况,所以我编写了一个过于复杂的基准测试。

其核心思想是主程序分配三个大内存区域:originalcurrentcorrect,每个区域大小完全相同,并且至少对页对齐。复制操作被分组为集合,每个集合具有不同的属性,就像所有源和目标被对齐(按照一定数量的字节),或者所有长度都在相同的范围内。每个集合都使用srcdstn三胞胎数组来描述,其中所有的srcsrc+n-1dstdst+n-1都完全在current区域内。

* PRNG用于将original初始化为随机数据。(就像我前面警告的那样,这太复杂了,但我想确保我不会为编译器留下任何简单的快捷方式。)correct区域是通过从current中的original数据开始,应用当前集合中的所有三胞胎,使用C库提供的memcpy(),以及将current区域复制到correct来获得的。这样就可以验证每个基准函数的行为是否正确。

使用相同的函数对每组复制操作进行大量计时,并使用这些操作的中间值进行比较。(在我看来,中位数在基准测试中是最有意义的,并且提供了合理的语义--这个函数的速度至少是一半。)

为了避免编译器优化,我让程序在运行时动态加载函数和基准测试。函数都有相同的形式,void function(void *, const void *, size_t) --请注意,与memcpy()memmove()不同,它们不返回任何内容。基准测试(称为复制操作集)是通过函数调用(该函数调用将指向current区域的指针及其大小作为参数等)动态生成的。

不幸的是,我还没有找到

代码语言:javascript
运行
复制
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" );
}

会打

代码语言:javascript
运行
复制
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)属性定义函数来选择在运行时使用的函数的实现。

我在代码中使用的最常见的模式之一是

代码语言:javascript
运行
复制
some_type *pointer = __builtin_assume_aligned(ptr, alignment);

其中ptr是一些指针,alignment是它对齐的字节数;GCC则知道/假设pointeralignment字节对齐。

另一个有用的内置,尽管很难正确使用,是__builtin_prefetch()。为了最大限度地提高整个带宽/效率,我发现在每个子操作中最小化延迟会产生最好的结果。(要将分散的元素复制到连续的临时存储中,这很困难,因为预取通常涉及完整的缓存行;如果预取的元素太多,则大部分缓存由于存储未使用的项而浪费。)

票数 8
EN

Stack Overflow用户

发布于 2017-04-11 10:34:40

有更有效的方法来移动数据。如今,memcpy的实现将从编译器中生成特定于体系结构的代码,编译器将根据数据和其他因素的内存对齐性进行优化。这允许在x86世界中更好地使用非时态缓存指令、XMM和其他寄存器。

当您使用硬代码时,rep movsb会阻止这种对本质的使用。

因此,对于像memcpy这样的东西,除非您正在编写一些将绑定到非常特定的硬件上的东西,并且除非您要花时间在程序集中编写一个高度优化的memcpy函数(或者使用C级本质),否则最好让编译器为您解决这个问题。

票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/43343231

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档