首页 文章

为memcpy增强了REP MOVSB

提问于
浏览
53

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

ERMSB引入了Ivy Bridge微体系结构 . 如果您不知道ERMSB是什么,请参阅Intel optimization manual中的"Enhanced REP MOVSB and STOSB operation (ERMSB)"部分 .

我知道直接执行此操作的唯一方法是使用内联汇编 . 我从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 .

Why is the bandwidth so much lower with REP MOVSB? What can I do to improve it?

这是我用来测试它的代码 .

//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

我有时会看到超过20 GB / s的 rep movsb .

sudo cpufreq-set -r -g powersave

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


我检查了频率(使用 turbostatwith and without SpeedStep enabledperformancepowersave 用于空闲,1核心负载和4核心负载 . 我运行Intel的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重新开启,所以我不得不在之后禁用涡轮增压器 .

6 回答

  • 6

    这是一个非常接近我的心和最近的调查的主题,所以我将从几个角度来看它:历史,一些技术说明(主要是学术性的),我的盒子上的测试结果,最后尝试回答你的实际问题时间和地点 rep movsb 可能有意义 .

    部分地,这是 call to share results - 如果你可以运行Tinymembench并分享结果以及你的CPU和RAM配置的细节,那将是很棒的 . 特别是如果你有一个4通道设置,一个常 Spring 藤桥盒,一个服务器盒等 .

    历史和官方建议

    快速字符串复制指令的性能历史是一个阶梯性的事情 - 即,停滞性能的时期与大的升级交替,使得它们与竞争方法一致或甚至更快 . 例如,Nehalem(主要针对启动开销)和Ivy Bridge(大多数目标是大型副本的总吞吐量)的性能有所提升 . 您可以找到有关实施英特尔工程师in this threadrep movs 指令的困难的十年见解 .

    例如,在引入Ivy Bridge之前的指南中,典型的advice是为了避免它们或非常小心地使用它们1 .

    当前(2016年6月)指南有各种令人困惑且有些不一致的建议,例如2:

    基于数据布局,对齐和计数器(ECX)值,在执行时选择实现的特定变体 . 例如,带有REP前缀的MOVSB / STOSB应使用小于或等于3的计数器值以获得最佳性能 .

    那么对于3个或更少字节的副本?你首先不需要一个 rep 前缀,因为声称的启动延迟大约为9个周期,你几乎肯定会用一个简单的DWORD或QWORD mov 更好地用一点点来掩盖未使用的字节(或者可能有2个显式字节,如果你知道大小正好是3个字 mov ) .

    他们继续说:

    字符串MOVE / STORE指令具有多个数据粒度 . 为了有效地进行数据移动,较大的数据粒度是优选的 . 这意味着通过将任意计数器值分解为多个双字加上计数值小于或等于3的单字节移动,可以实现更高的效率 .

    在使用ERMSB的当前硬件上,这肯定是错误的,其中 rep movsb 至少与大型副本的 movdmovq 变体一样快或更快 .

    一般而言,本指南的该部分(3.7.5)包含合理的和过时的建议 . 这是英特尔手册的常见吞吐量,因为它们以增量方式针对每个体系结构进行更新(并且声称即使在当前手册中也涵盖了近二十年的体系结构),旧部分通常不会更新以替换或提供有条件的建议这不适用于当前的架构 .

    然后他们继续在3.7.6节中明确地介绍ERMSB .

    我不会详尽地讨论剩余的建议,但我将在下面总结“为何使用它”中的好部分 .

    该指南的其他重要声明是,在Haswell上, rep movsb 已得到增强,可在内部使用256位操作 .

    技术考虑因素

    这只是从实现的角度来看 rep 指令所具有的潜在优点和缺点的快速摘要 .

    rep movs的优点

    • 当发出 rep movs指令时,CPU知道要传输已知大小的整个块 . 这可以帮助它以不能使用离散指令的方式优化操作,例如:

    • 当知道整个缓存行时,将覆盖RFO请求 .

    • 立即和准确地发出预取请求 . 硬件预取在检测类似于 memcpy 的模式方面做得很好,但它仍然需要几次读取才会启动,并且会在复制区域末尾之外的许多高速缓存行中使用.2877430_ . rep movsb 确切地知道区域大小并且可以准确地预取 .

    • 显然,无法保证在单个 rep movs 中的商店之间进行排序,这有助于简化一致性流量以及块移动的其他方面,而不是必须遵守相当严格的内存排序的简单指令4 .

    • 原则上, rep movs 指令可以利用ISA中未公开的各种架构技巧 . 例如,体系结构可能具有更宽的内部数据路径,ISA公开5和 rep movs 可以在内部使用 .

    缺点

    • rep movsb 必须实现可能比基础软件要求更强的特定语义 . 特别是, memcpy 禁止重叠区域,因此可能会忽略这种可能性,但 rep movsb 允许它们并且必须产生预期结果 . 当前的实现主要影响启动开销,但可能不会影响大块吞吐量 . 类似地, rep movsb 必须支持字节粒度副本,即使您实际上正在使用它来复制大块,这些块是2的大功率的倍数 .

    • 如果使用 rep movsb ,软件可能包含有关对齐,复制大小和可能的别名的信息,这些信息无法传达给硬件 . 编译器通常可以确定内存块6的对齐方式,因此可以避免每次调用时必须执行的大量启动工作 .

    测试结果

    以下是我的i7-6700HQ上2.6GHz的tinymembench的许多不同复制方法的测试结果(太糟糕了,我有相同的CPU,所以我们没有得到新的数据点...):

    C copy backwards                                     :   8284.8 MB/s (0.3%)
     C copy backwards (32 byte blocks)                    :   8273.9 MB/s (0.4%)
     C copy backwards (64 byte blocks)                    :   8321.9 MB/s (0.8%)
     C copy                                               :   8863.1 MB/s (0.3%)
     C copy prefetched (32 bytes step)                    :   8900.8 MB/s (0.3%)
     C copy prefetched (64 bytes step)                    :   8817.5 MB/s (0.5%)
     C 2-pass copy                                        :   6492.3 MB/s (0.3%)
     C 2-pass copy prefetched (32 bytes step)             :   6516.0 MB/s (2.4%)
     C 2-pass copy prefetched (64 bytes step)             :   6520.5 MB/s (1.2%)
     ---
     standard memcpy                                      :  12169.8 MB/s (3.4%)
     standard memset                                      :  23479.9 MB/s (4.2%)
     ---
     MOVSB copy                                           :  10197.7 MB/s (1.6%)
     MOVSD copy                                           :  10177.6 MB/s (1.6%)
     SSE2 copy                                            :   8973.3 MB/s (2.5%)
     SSE2 nontemporal copy                                :  12924.0 MB/s (1.7%)
     SSE2 copy prefetched (32 bytes step)                 :   9014.2 MB/s (2.7%)
     SSE2 copy prefetched (64 bytes step)                 :   8964.5 MB/s (2.3%)
     SSE2 nontemporal copy prefetched (32 bytes step)     :  11777.2 MB/s (5.6%)
     SSE2 nontemporal copy prefetched (64 bytes step)     :  11826.8 MB/s (3.2%)
     SSE2 2-pass copy                                     :   7529.5 MB/s (1.8%)
     SSE2 2-pass copy prefetched (32 bytes step)          :   7122.5 MB/s (1.0%)
     SSE2 2-pass copy prefetched (64 bytes step)          :   7214.9 MB/s (1.4%)
     SSE2 2-pass nontemporal copy                         :   4987.0 MB/s
    

    一些关键的要点:

    • rep movs 方法比不是"non-temporal" 7的所有其他方法快,并且比一次复制8个字节的"C"方法快得多 .

    • "non-temporal"方法比 rep movs 方法更快,最多约26% - 但这比你报告的方法小得多(26 GB / s vs 15 GB / s = ~73%) .

    • 如果您不使用非临时存储,则使用C中的8字节副本几乎与128位宽SSE加载/存储一样好 . 这是因为良好的复制循环可以产生足够的存储器压力以使带宽饱和(例如,2.6 GHz * 1存储/周期* 8字节= 26 GB / s用于存储) .

    • tinymembench中没有明确的256位算法(除了可能是"standard" memcpy ),但由于上面的注释,它可能无关紧要 .

    • 非临时存储的吞吐量增加超过了暂时的约为1.45x,如果NT消除了3次传输中的1次(即1次读取,1次写入NT与2次读取,1次写入),则非常接近1.5倍 . rep movs 方法位于中间 .

    • 相当低的内存延迟和适度的2通道带宽的组合意味着这个特定的芯片恰好能够从单线程中饱和其内存带宽,这会显着改变行为 .

    • rep movsd 似乎在这个芯片上使用与 rep movsb 相同的魔力 . 这很有意思,因为ERMSB只明确定位 movsb 和早期的早期测试,ERMSB显示 movsb 的执行速度比 movsd 快得多 . 这主要是学术性的,因为 movsb 无论如何都比 movsd 更为通用 .

    Haswell

    看一下iwillnotexist在评论中提供的Haswell results,我们看到了相同的总体趋势(提取了最相关的结果):

    C copy                                               :   6777.8 MB/s (0.4%)
     standard memcpy                                      :  10487.3 MB/s (0.5%)
     MOVSB copy                                           :   9393.9 MB/s (0.2%)
     MOVSD copy                                           :   9155.0 MB/s (1.6%)
     SSE2 copy                                            :   6780.5 MB/s (0.4%)
     SSE2 nontemporal copy                                :  10688.2 MB/s (0.3%)
    

    rep movsb 方法仍然慢于非时间方法 memcpy ,但这里只有大约14%(相比于Skylake测试中的~26%) . NT技术优于其时间表兄弟的优势现在约为57%,甚至略高于带宽减少的理论效益 .

    什么时候应该使用rep movs?

    最后是你的实际问题:你什么时候或为什么要使用它?它借鉴了上述内容并介绍了一些新的想法 . 不幸的是,没有简单的答案:你将不得不权衡各种因素,包括你可能甚至不知道的一些因素,例如未来的发展 .

    请注意, rep movsb 的替代可能是优化的libc memcpy (包括编译器内联的副本),或者它可能是手动 memcpy 版本 . 下面的一些好处仅适用于与这些替代方案中的一个或另一个相比(例如,"simplicity"有助于反对手动滚动版本,但不反对内置 memcpy ),但有些优点适用于两者 .

    对可用说明的限制

    在某些环境中,某些指令或使用某些寄存器存在限制 . 例如,在Linux内核中,通常不允许使用SSE / AVX或FP寄存器 . 因此,大多数优化的 memcpy 变体不能使用,因为它们依赖于SSE或AVX寄存器,并且在x86上使用普通的64位 mov 副本 . 对于这些平台,使用 rep movsb 允许优化 memcpy 的大部分性能,而不会破坏对SIMD代码的限制 .

    更一般的示例可能是必须针对许多代硬件的代码,并且不使用特定于硬件的分派(例如,使用 cpuid ) . 在这里你可能被迫只使用较旧的指令集,它排除了任何AVX等. rep movsb 可能是一个很好的方法,因为它允许"hidden"访问更宽的负载和存储而不使用新的指令 . 如果你的目标是ERMSB之前的硬件,你必须看看那里是否可以接受 rep movsb 性能,不过......

    未来证明

    rep movsb 的一个很好的方面是理论上可以利用未来架构上的架构改进,而不需要源更改,显式移动不能 . 例如,当引入256位数据路径时, rep movsb 能够利用它们(如英特尔所声称的那样),而无需对软件进行任何更改 . 使用128位移动的软件(在Haswell之前是最佳的)必须进行修改和重新编译 .

    因此,它既是软件维护优势(无需更改源代码),也是现有二进制文件的优势(无需部署新的二进制文件来利用这些改进) .

    这有多重要取决于您的维护模型(例如,在实践中部署新二进制文件的频率)以及很难判断这些指令在未来可能的速度 . 至少英特尔在这个方向上是一种指导用途,通过承诺在未来至少合理的表现( 15.3.3.6 ):

    REP MOVSB和REP STOSB将继续在未来的处理器上表现得相当好 .

    与后续工作重叠

    当然,这种好处不会出现在一个简单的基准中,根据定义,后续工作不会重叠,因此必须在现实世界的情景中仔细衡量收益的大小 . 获得最大优势可能需要重新组织 memcpy 周围的代码 .

    指出了这个好处英特尔在其优化手册(第11.16.3.4节)中用他们的话说:

    当已知计数至少为千字节或更多时,使用增强型REP MOVSB / STOSB可以提供另一个优势来分摊非消费代码的成本 . 可以使用值Cnt = 4096和memset()作为示例来理解启发式:•memset()的256位SIMD实现将需要使用VMOVDQA发出/执行退出128个32字节存储操作的实例,之后非消费指令序列可以让他们退休 . •ECX = 4096的增强型REP STOSB实例被解码为硬件提供的长微操作流,但作为一条指令退出 . 有许多store_data操作必须在memset()的结果被消耗之前完成 . 因为商店数据操作的完成与程序订单退出脱离,所以非消费代码流的大部分可以通过发布/执行和退出进行处理,如果非消费序列不竞争则基本上没有成本用于存储缓冲区资源 .

    所以英特尔说,毕竟有些微博之后的代码已经发布了,但是虽然很多商店仍然在运行,而整个 rep movsb 还没有退役,但是按照以下说明进行操作可以通过以下方式取得更多进展如果代码在复制循环之后出现,则可以使用机器 .

    来自显式加载和存储循环的uops都必须按程序顺序单独退出 . 这必须发生在ROB中为跟随uops腾出空间 .

    似乎没有太多详细的信息,关于如何长时间的微编码指令,如 rep movsb 工作 . 我们不必单独退休,也许整个指令只占用ROB中的一个插槽?

    当供给OoO机器的前端在uop缓存中看到 rep movsb 指令时,它会激活微代码序列器ROM(MS-ROM)以将微代码uop发送到为该问题/重命名阶段提供信息的队列中 . 当 rep movsb 仍在发出时,任何其他uop可能无法与其混合并发出/执行8,但后续指令可以被提取/解码并在最后 rep movsb uop之后立即发出,而某些副本还没有't executed yet. This is only useful if at least some of your subsequent code doesn't依赖关于 memcpy 的结果(这并不罕见) .

    现在,这个好处的大小是有限的:最多你可以在慢速 rep movsb 指令之外执行N指令(实际上是uops),此时你将停止,其中N是ROB size . 当前的ROB大小约为200(Haswell为192,Skylake为224),对于IPC为1的后续代码,最大可获得约200个循环的免费工作 . 在200个周期内,您可以复制大约800字节的10 GB / s,因此对于那个大小的副本,您可以获得接近副本成本的免费工作(以某种方式使副本免费) .

    然而,随着拷贝大小变得更大,其相对重要性迅速降低(例如,如果您复制80 KB,则免费工作仅占复制成本的1%) . 对于适度大小的副本,它仍然非常有趣 .

    复制循环也不会完全阻止后续指令执行 . 英特尔不会详细说明利益的大小,也不会详细说明最有益的副本或周围代码 . (热或冷目标或源,高ILP或低ILP高延迟代码之后) .

    代码大小

    与典型的优化 memcpy 例程相比,执行的代码大小(几个字节)是微观的 . 如果性能完全受到i-cache(包括uop cache)未命中的限制,那么减小的代码大小可能会带来好处 .

    同样,我们可以根据副本的大小限制此权益的大小 . 我实际上不会在数字上解决这个问题,但直觉是将动态代码大小减少B个字节可以节省大多数 C * B 缓存未命中,对于某些常量C.每次调用 memcpy 都会导致缓存未命中成本(或收益)曾经,但更高吞吐量的优势随着复制的字节数而变化 . 因此,对于大型传输,更高的吞吐量将主导缓存效果 .

    同样,这不会出现在普通基准测试中,整个循环无疑会适合uop缓存 . 您需要一个真实的就地测试来评估这种效果 .

    架构特定优化

    您报告在您的硬件上, rep movsb 比平台 memcpy 慢得多 . 但是,甚至这里有早期硬件(如Ivy Bridge)相反结果的报告 .

    这完全是合理的,因为看起来字符串移动操作定期得到爱 - 但不是每一代,所以它可能更快或至少并联(在此时它可能基于其他优势赢得)在架构上提到最新,只落后于后续硬件 .

    Quoting Andy Glew,在P6上实现这些之后应该对此有所了解:

    在微代码中做快速字符串的一大弱点是,微代码与每一代都失调了,越来越慢,直到有人来解决它 . 就像图书馆一样,男人的副本也会失控 . 我想错过的机会之一是有可能在它们可用时使用128位加载和存储,等等 .

    在这种情况下,它可以看作只是另一种"platform specific"优化应用于您在标准库和JIT编译器中找到的典型的每个技巧--2877499_例程:但仅适用于更好的架构 . 对于JIT或AOT编译的东西,这很容易,但对于静态编译的二进制文件,这确实需要特定于平台的调度,但这通常已经存在(有时在链接时实现),或者 mtune 参数可用于做出静态决策 .

    简洁

    即使在Skylake,它似乎已经落后于绝对最快的非时间技术,它仍然比大多数方法更快,并且非常简单 . 这意味着更少的验证时间,更少的神秘错误,更少的时间调整和更新怪物 memcpy 实现(或者相反,如果你依赖它,对标准库实现者的想法的依赖性较小) .

    延迟绑定平台

    内存吞吐量限制算法9实际上可以在两个主要的整体方案中运行:DRAM带宽限制或并发/延迟限制 .

    第一种模式是您可能熟悉的模式:DRAM子系统具有一定的理论带宽,您可以根据通道数,数据速率/宽度和频率轻松计算 . 例如,我的带有2个通道的DDR4-2133系统的最大带宽为2.133 * 8 * 2 = 34.1 GB / s,与reported on ARK相同 .

    在套接字上的所有内核中,您不会从DRAM中获得超过该速率(并且通常会因为各种低效率而稍微减少)(即,它是单插槽系统的全局限制) .

    另一个限制是核心实际可以向内存子系统发出多少并发请求 . 想象一下,如果一个核心一次只能有1个请求正在进行,对于一个64字节的缓存行 - 当请求完成时,你可以发出另一个请求 . 假设也有非常快的50ns内存延迟 . 然后,尽管有大的34.1 GB / s DRAM带宽,但实际上只有64字节/ 50 ns = 1.28 GB / s,或者不到最大带宽的4% .

    实际上,核心可以一次发出多个请求,但不是无限制的数量 . 通常可以理解,在L1和存储器层次结构的其余部分之间每个核心只有10个行填充缓冲区,并且在L2和DRAM之间可能有16个左右的填充缓冲区 . 预取竞争相同的资源,但至少有助于减少有效延迟 . 有关详细信息,请查看任何一篇很棒的帖子Dr. Bandwidth has written on the topic,主要是在英特尔论坛上 .

    尽管如此,最新的CPU受此因素限制,而不是RAM带宽 . 通常,它们每个内核可达到12 - 20 GB / s,而RAM带宽可能为50 GB / s(在4通道系统上) . 只有一些最新的2通道"client"核心,似乎有更好的非核心,可能更多的线路缓冲器可以达到单核心的DRAM限制,我们的Skylake芯片似乎是其中之一 .

    当然,英特尔设计具有50 GB / s DRAM带宽的系统是有原因的,而由于并发限制,每个核心仅维持<20 GB / s:前者限制是套接字,后者是每个核心 . 因此,8核系统上的每个核心都可以推送20 GB / s的请求,此时它们将再次受到DRAM的限制 .

    为什么我要继续这样做?因为最好的实现通常取决于您正在使用哪种方案 . 一旦您的DRAM BW受限(因为我们的芯片显然是,但大多数不在单核上),使用非时间写入变得非常重要,因为它节省了读取所有权通常会占用带宽的1/3 . 您完全在上面的测试结果中看到:不使用NT存储的memcpy实现会丢失1/3的带宽 .

    但是,如果你的并发性有限,那么情况会有所 balancer ,有时甚至会逆转 . 你有多余的DRAM带宽,所以NT存储没有帮助,它们甚至可以受到伤害,因为它们可能会增加延迟,因为行缓冲区的切换时间可能比预取将RFO线路带入LLC的情况更长(或者甚至更长) L2)然后商店在LLC中完成有效的低延迟 . 最后,服务器的核心往往比客户端(和高带宽)具有更慢的NT存储,这突出了这种效果 .

    因此,在其他平台上,您可能会发现NT存储不太有用(至少当您关心单线程性能时)并且可能 rep movsb 在哪里获胜(如果它在两个世界中都是最好的) .

    真的,这最后一项是大多数测试的电话 . 我知道NT商店在大多数拱门(包括当前服务器拱门)的单线程测试中失去了明显的优势,但我不知道 rep movsb 将如何相对表现......

    参考文献

    其他很好的信息来源没有集成在上面 .

    comp.arch investigationrep movsb 与替代品 . 关于分支预测的很多好注意事项,以及我经常为小块建议的方法的实现:使用重叠的第一次和/或最后一次读/写而不是仅尝试精确写入所需的字节数(例如,实现所有副本从9到16个字节作为两个8字节副本,可能最多重叠7个字节) .


    1据推测,目的是将其限制在例如代码大小非常重要的情况下 .

    2请参阅 Section 3.7.5: REP前缀和数据移动 .

    3需要注意的是,这仅适用于单一指令本身内的各种商店:一旦完成,商店街区仍然按照先前和后续商店的顺序排列 . 因此,代码可以看到 rep movs 中的商店相对于彼此无序,但与先前或后续商店无关(并且这是后者通常需要的保证) . 如果您将复制目标的末尾用作同步标志而不是单独的存储,则只会出现问题 .

    4请注意,非时间离散存储也避免了大多数排序要求,尽管实际上 rep movs 具有更大的自由度,因为WC / NT存储仍然存在一些排序约束 .

    5这在32位时代的后期是常见的,其中许多芯片具有64位数据路径(例如,支持支持64位 double 类型的FPU) . 今天,像奔腾或赛扬等品牌的芯片已经禁用了AVX,但据推测,微码仍然可以使用256b的加载/存储 .

    6例如,由于语言对齐规则,对齐属性或运算符,别名规则或在编译时确定的其他信息 . 在对齐的情况下,即使不能确定精确对准,它们也可以至少能够将对齐检查从循环中提升或以其他方式消除冗余检查 .

    7我假设"standard" memcpy 正在选择非时间方法,这很可能适用于这种大小的缓冲区 .

    8这不一定是显而易见的,因为可能是 rep movsb 生成的uop流只是垄断调度,然后它看起来非常像显式 mov 情况 . 似乎它不能像那样工作 - 来自后续指令的uops可以与微编码 rep movsb 中的uop混合 .

    9即,那些可以发出大量独立内存请求并因此使可用的DRAM到核心带宽饱和的那些,其中 memcpy 将是一个二等分子(并且与纯粹的延迟绑定负载如指针追逐相关) .

  • 65

    增强型REP MOVSB(Ivy Bridge及更高版本)

    Ivy Bridge微体系结构(2012年和2013年发布的处理器)引入了增强型REP MOVSB(我们仍然需要检查相应的位)并允许我们快速复制内存 .

    最便宜的后续处理器版本 - 2017年发布的Kaby Lake Celeron和Pentium,没有可用于快速内存复制的AVX,但仍然具有增强型REP MOVSB .

    如果块大小至少为256字节,则REP MOVSB(ERMSB)仅比AVX复制或通用寄存器复制更快 . 对于低于64字节的块,它会慢很多,因为内部很高在ERMSB启动 - 大约35个周期 .

    请参阅“英特尔优化手册”第3.7.6节“增强型REP MOVSB和STOSB操作”(ERMSB)http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

    启动成本为35个周期;源地址和目标地址都必须与16字节边界对齐;源区域不应与目标区域重叠;长度必须是64的倍数才能产生更高的性能;方向必须向前(CLD) .

    正如我之前所说,当MOVSB长度至少为256字节时,它开始优于其他方法,但要看到AVX拷贝的明显优势,长度必须超过2048字节 .

    如果REP MOVSB与AVX副本的对齐效果,则“英特尔手册”提供以下信息:

    如果源缓冲区未对齐,则对ERMSB实现与128位AVX的影响相似;如果目标缓冲区未对齐,则对ERMSB实现的影响可能会降低25%,而相对于16字节对齐方案,memcpy的128位AVX实现可能仅降低5% .

    我已经在64位以下的Intel Core i5-6600上进行了测试,并且我将REP MOVSB memcpy()与简单的MOV RAX,[SRC]进行了比较; MOV [夏令时],RAX实施 when the data fits L1 cache

    REP MOVSB memcpy():

    - 1622400000 data blocks of  32 bytes took 17.9337 seconds to copy;  2760.8205 MB/s
     - 1622400000 data blocks of  64 bytes took 17.8364 seconds to copy;  5551.7463 MB/s
     - 811200000 data blocks of  128 bytes took 10.8098 seconds to copy;  9160.5659 MB/s
     - 405600000 data blocks of  256 bytes took  5.8616 seconds to copy; 16893.5527 MB/s
     - 202800000 data blocks of  512 bytes took  3.9315 seconds to copy; 25187.2976 MB/s
     - 101400000 data blocks of 1024 bytes took  2.1648 seconds to copy; 45743.4214 MB/s
     - 50700000 data blocks of  2048 bytes took  1.5301 seconds to copy; 64717.0642 MB/s
     - 25350000 data blocks of  4096 bytes took  1.3346 seconds to copy; 74198.4030 MB/s
     - 12675000 data blocks of  8192 bytes took  1.1069 seconds to copy; 89456.2119 MB/s
     - 6337500 data blocks of  16384 bytes took  1.1120 seconds to copy; 89053.2094 MB/s
    

    MOV RAX ... memcpy():

    - 1622400000 data blocks of  32 bytes took  7.3536 seconds to copy;  6733.0256 MB/s
     - 1622400000 data blocks of  64 bytes took 10.7727 seconds to copy;  9192.1090 MB/s
     - 811200000 data blocks of  128 bytes took  8.9408 seconds to copy; 11075.4480 MB/s
     - 405600000 data blocks of  256 bytes took  8.4956 seconds to copy; 11655.8805 MB/s
     - 202800000 data blocks of  512 bytes took  9.1032 seconds to copy; 10877.8248 MB/s
     - 101400000 data blocks of 1024 bytes took  8.2539 seconds to copy; 11997.1185 MB/s
     - 50700000 data blocks of  2048 bytes took  7.7909 seconds to copy; 12710.1252 MB/s
     - 25350000 data blocks of  4096 bytes took  7.5992 seconds to copy; 13030.7062 MB/s
     - 12675000 data blocks of  8192 bytes took  7.4679 seconds to copy; 13259.9384 MB/s
    

    因此,即使在128位块上,REP MOVSB也比循环中的简单MOV RAX副本慢(未展开) . ERMSB实现开始优于仅从256字节块开始的MOV RAX循环 .

    正常(未增强)REP MOVS在Nehalem及更高版本上

    令人惊讶的是,以前的架构(Nehalem和更高版本),还没有增强型REP MOVB,对于大型块具有相当快的REP MOVSD / MOVSQ(但不是REP MOVSB / MOVSW)实现,但不足以超大L1缓存 .

    英特尔优化手册(2.5.6 REP字符串增强)提供以下信息与Nehalem微体系结构 - 2009年和2010年发布的英特尔酷睿i5,i7和Xeon处理器有关 .

    REP MOVSB

    如果ECX <4,则MOVSB的延迟为9个周期;否则,ECX> 9的REP MOVSB具有50个周期的启动成本 .

    • tiny string(ECX <4):REP MOVSB的延迟为9个周期;

    • 小字符串(ECX介于4和9之间):英特尔手册中没有官方信息,可能超过9个周期但少于50个周期;

    • 长字符串(ECX> 9):50周期启动成本 .

    我的结论:REP MOVSB在Nehalem几乎没用 .

    MOVSW / MOVSD / MOVSQ

    引用英特尔优化手册(2.5.6 REP String Enhancement):

    短串(ECX <= 12):REP MOVSW / MOVSD / MOVSQ的延迟约为20个周期 . 快速字符串(ECX> = 76:不包括REP MOVSB):处理器实现通过移动尽可能多的16字节数据来提供硬件优化 . 如果其中一个16字节数据传输跨越缓存行边界,则REP字符串延迟的延迟会有所不同:=无分裂:延迟包括大约40个周期的启动成本,每64个字节的数据增加4个周期 . =缓存拆分:延迟包括大约35个周期的启动成本,每64个字节的数据增加6个周期 . 中间字符串长度:REP MOVSW / MOVSD / MOVSQ的延迟具有大约15个周期的启动成本加上word / dword / qword中数据移动的每次迭代的一个周期 .

    英特尔在这里似乎不正确 . 从上面的引用我们了解到,对于非常大的内存块,REP MOVSW和REP MOVSD / MOVSQ一样快,但是测试表明只有REP MOVSD / MOVSQ很快,而REP MOVSW甚至比Nehalem和Westmere上的REP MOVSB慢 .

    根据英特尔在手册中提供的信息,在以前的英特尔微体系结构(2008年之前)中,启动成本甚至更高 .

    结论:如果您只需要复制适合L1缓存的数据,那么复制64字节数据只需4个周期就可以了,而且您不需要使用XMM寄存器!

    REP MOVSD / MOVSQ是通用解决方案,如果数据适合L1缓存,则在所有英特尔处理器上都能很好地工作(不需要ERMSB)

    以下是当源和目标位于L1缓存中时,REP MOVS *的测试,其大小足以不受启动成本的严重影响,但不会超过L1缓存大小 . 资料来源:http://users.atw.hu/instlatx64/

    约拿(2006-2008)

    REP MOVSB 10.91 B/c
        REP MOVSW 10.85 B/c
        REP MOVSD 11.05 B/c
    

    Nehalem(2009-2010)

    REP MOVSB 25.32 B/c
        REP MOVSW 19.72 B/c
        REP MOVSD 27.56 B/c
        REP MOVSQ 27.54 B/c
    

    Westmere(2010-2011)

    REP MOVSB 21.14 B/c
        REP MOVSW 19.11 B/c
        REP MOVSD 24.27 B/c
    

    Ivy Bridge(2012-2013) - 增强型REP MOVSB

    REP MOVSB 28.72 B/c
        REP MOVSW 19.40 B/c
        REP MOVSD 27.96 B/c
        REP MOVSQ 27.89 B/c
    

    SkyLake(2015-2016) - 增强型REP MOVSB

    REP MOVSB 57.59 B/c
        REP MOVSW 58.20 B/c
        REP MOVSD 58.10 B/c
        REP MOVSQ 57.59 B/c
    

    Kaby Lake(2016-2017) - 与增强的REP MOVSB

    REP MOVSB 58.00 B/c
        REP MOVSW 57.69 B/c
        REP MOVSD 58.00 B/c
        REP MOVSQ 57.89 B/c
    

    如您所见,REP MOVS的实现与一个微体系结构有很大不同 . 在某些处理器上,比如Ivy Bridge - REP MOVSB速度最快,虽然比REP MOVSD / MOVSQ略快,但毫无疑问,自Nehalem以来所有处理器,REP MOVSD / MOVSQ运行良好 - 你甚至不需要"Enhanced REP MOVSB",因为在Ivy Bridge(2013)上,使用Enhacnced REP MOVSB,REP MOVSD显示与Nehalem(2010)相同的每字节数据字节,而没有Enhacnced REP MOVSB,而事实上REP MOVSB仅在SkyLake(2015)之后变得非常快 - 速度提高了一倍就像常 Spring 藤桥一样 . 所以CPUID中的这个增强的REP MOVSB位可能会令人困惑 - 它只显示 REP MOVSB 本身是可以的,但并不是说任何 REP MOVS* 都更快 .

    最令人困惑的ERMBSB实现是在Ivy Bridge微体系结构上 . 是的,在非常旧的处理器上,在ERMSB之前,针对大型块的REP MOVS *确实使用了常规代码(no-RFO)无法使用的缓存协议功能 . 但是这个协议不再用于具有ERMSB的Ivy Bridge . 根据Andy Glew's comments on an answer to "why are complicated memcpy/memset superior?" from a Peter Cordes answer,常规代码无法使用的缓存协议功能曾在旧版处理器上使用,但不再在Ivy Bridge上使用 . 并且解释了为什么REP MOVS *的启动成本如此之高:“选择和设置正确方法的巨大开销主要是由于缺少微码分支预测” . 还有一个有趣的说明,Pentium Pro(P6)在1996年实现了带有64位微码加载和存储的REP MOVS *以及无RFO缓存协议 - 它们没有违反内存排序,这与Ivy Bridge中的ERMSB不同 .

    免责声明

    • 此答案仅适用于源和目标数据适合L1缓存的情况 . 根据具体情况,应考虑内存访问(缓存等)的特殊性 . 在某些情况下,预取和NTI可能会提供更好的结果,尤其是在尚未具有增强型REP MOVSB的处理器上 . 即使在这些较旧的处理器上,REP MOVSD也可能使用了常规代码无法使用的缓存协议功能 .

    • 本回答中的信息仅与英特尔处理器相关,而与AMD等其他制造商的处理器无关,这些处理器可能有更好或更差的REP MOVS *指令实现 .

    • 我为了确认而提供了SkyLake和Kaby Lake的测试结果 - 这些架构具有相同的每指令周期数据 .

    • 所有产品名称,商标和注册商标均为其各自所有者的 property .

  • 7

    你说你想要:

    一个答案,显示ERMSB何时有用

    但我不确定这意味着你的意思 . 查看链接到的3.7.6.1文档,它明确说:

    使用ERMSB实现memcpy可能无法达到与使用256位或128位AVX替代方案相同的吞吐量水平,具体取决于长度和对齐因子 .

    所以只是因为 CPUID 表示支持ERMSB,这并不像以前的某些CPU那样糟糕 .

    然而,仅仅因为可能存在替代方案,在某些条件下,运行得更快并不意味着REP MOVSB无用 . 既然该指令过去产生的性能损失已经消失,那么它可能再次成为有用的指令 .

    请记住,与我所看到的一些更复杂的memcpy例程相比,它只是一小段代码(2个字节!) . 由于加载和运行大块代码也会受到惩罚(将一些其他代码从cpu的缓存中抛出),有时AVX等人的“好处”会被它对其余部分的影响所抵消 . 码 . 取决于你在做什么 .

    你也问:

    为什么REP MOVSB的带宽要低得多?我该怎么做才能改善它?

    为了让REP MOVSB运行得更快,“做某事”是不可能的 . 它做它做的事 .

    如果你想从memcpy中看到更高的速度,你可以挖掘它的来源 . 它在某处 . 或者您可以从调试器跟踪它并查看正在采用的实际代码路径 . 我的期望是它使用一些AVX指令一次使用128或256位 .

    或者你可以......嗯,你让我们不要说出来 .

  • 3

    这不是对所述问题的回答,只是我试图找出答案时的结果(和个人结论) .

    总结: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内核, /proc/cpuinfo 标志中有 erms ) . 因为我想知道我是否能找到一个基于 rep movsb 的自定义memcpy()变体胜过一个简单的 memcpy() 的情况,我写了一个过于复杂的基准 .

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

    Xorshift* 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);
    }
    

    在前面提到的运行linux-4.4.0 64位内核的Core i5-6200U笔记本电脑上使用 gcc -Wall -O2 -march=ivybridge -mtune=ivybridge 使用GCC 5.4.0 . 然而,复制4096字节对齐和大小的块是接近的 .

    这意味着至少到目前为止,我还没有找到使用 rep movsb memcpy变体有意义的情况 . 这并不意味着没有这种情况;我还没找到一个 .

    (此时代码是一个意大利面条混乱,我比骄傲更惭愧,所以我会省略发布消息来源,除非有人问 . 但上面的描述应该足以写出更好的了 . )


    不过,这并不让我感到惊讶 . C编译器可以推断出很多关于操作数指针对齐的信息,以及要复制的字节数是否是编译时常量,是2的合适幂的倍数 . 编译器可以并且将/应该使用此信息将C库 memcpy() / memmove() 函数替换为自己的函数 .

    GCC就是这样做的(参见例如GCC源中的gcc/config/i386/i386.c:expand_set_or_movmem_via_rep();也在同一文件中查找 stringop_algs 以查看依赖于体系结构的变体) . 实际上, memcpy() / memset() / memmove() 已经针对相当多的x86处理器变体进行了单独优化;如果GCC开发人员还没有包含erms支持,我会感到非常惊讶 .

    GCC提供了几个function attributes,开发人员可以使用它来确保生成良好的代码 . 例如, alloc_align (n) 告诉GCC该函数返回的内存至少与 n 字节对齐 . 应用程序或库可以选择在运行时使用哪个函数实现,方法是创建一个"resolver function"(返回一个函数指针),并使用 ifunc (resolver) 属性定义函数 .

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

    some_type *pointer = __builtin_assume_aligned(ptr, alignment);
    

    其中 ptr 是某个指针, alignment 是它对齐的字节数;然后GCC知道/假设 pointeralignment 字节对齐 .

    另一个有用的内置,虽然更难以正确使用,但是__builtin_prefetch() . 为了最大化整体带宽/效率,我发现最小化每个子操作中的延迟会产生最佳结果 . (为了将分散的元素复制到连续的临时存储,这很困难,因为预取通常涉及完整的缓存行;如果预取了太多的元素,则通过存储未使用的项来浪费大部分缓存 . )

  • 9

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

    当您硬编码时 rep movsb 阻止使用内在函数 .

    因此,对于像 memcpy 这样的东西,除非你正在写一些与特定硬件相关的东西,除非你花时间在汇编中(或使用C级内在函数)编写高度优化的 memcpy 函数,你最好不要让编译器为你解决问题 .

  • 1

    作为一般指导 memcpy() 指南:

    a)如果被复制的数据很小(少于20个字节)并且具有固定大小,那么让编译器执行此操作 . 原因:编译器可以使用正常的 mov 指令并避免启动开销 .

    b)如果要复制的数据很小(小于约4 KiB)且保证对齐,请使用 rep movsb (如果支持ERMSB)或 rep movsd (如果不支持ERMSB) . 原因:在复制任何内容之前,使用SSE或AVX替代方案会有大量的"startup overhead" .

    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
    

    原因:这将是如此之慢,它将迫使程序员找到一个不涉及复制大量数据的替代方案;由此产生的软件将大大加快,因为避免了复制大量数据 .

相关问题