为memcpy

时间:2017-04-11 10:22:09

标签: c gcc assembly x86 memcpy

我想使用增强型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似乎对电源管理不敏感。

我检查了频率(使用turbostatwith and without SpeedStep enabledperformancepowersave用于空闲,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重新开启,所以我不得不在之后禁用涡轮增压器。

4 个答案:

答案 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()的情况,我写了一个过于复杂的基准。

核心思想是主程序分配三个大的内存区域:originalcurrentcorrect,每个区域大小完全相同,并且至少是页面对齐的。复制操作被分组为集合,每个集合具有不同的属性,例如所有源和目标被对齐(到某个字节数),或者所有长度都在相同的范围内。使用srcdstn三元组数组描述每个集合,其中所有srcsrc+n-1dst到{{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知道/假设alignmentpointer字节对齐。

另一个有用的内置功能,虽然更难以正确使用 ,但是__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

原因:这将是如此之慢,以至于它将迫使程序员找到一种不会涉及复制大量数据的替代方案;由于避免了复制大量数据,因此生成的软件速度会明显加快。