18

Arm memcpy, memset指令(FEAT_MOPS)介绍

引言:告别,无休的memcpy, memset软件优化。Three instructions are all you need

Arm A-profile构架v8.8-A/v9.3-A引入了专门用于memcpy和memset的指令(构架特性名为FEAT_MOPS),能利用不同CPU核微构架内存访问硬件设计带来的好处的同时,这些指令可以使软件广泛使用的memcpy, memset操作在arm构架上标准化。可以克服之前的问题:需要针对不同的CPU核和不同的数据大小/地址对齐,使用不同的软件路径或是不同的软件库实现,才能利用CPU微构架内存访问设计带来的好处。

为什么需要专门的memcpy, memset指令?

memcpy, memset在软件中广泛,频繁使用,可以明显影响软件性能。
Google研究表明,软件对memcpy, memmove的库函数调用能达到数据中心时间的4~5%.
image.png
https://static.googleusercont...
Facebook的研究也表明,memcpy,memmove消耗了最大的内存周期:
image.png
https://dl.acm.org/doi/10.114...
还有研究表明,将近70%的memcpy是针对小于128B的数据,~80%是对小于256B的数据。

Arm构架是RISC,它有简洁的LDR (Load), STR (Store)内存访问指令。之前在A64指令集中,有基本的:

  • 单寄存器LDR/STR指令: 一条LDR指令可以从内存中load一个 1-byte, 2-byte, 4-byte, 8-byte, 16-byte数据到一个寄存器,一条STR指令可以将一个寄存器中的1-byte, 2-byte, 4-byte, 8-byte, 16-byte数据存储到内存中.
  • 双寄存器LDP/STP指令: 一条LDP指令可以从内存中load两个4-byte, 8-byte数据到两个寄存器,一条STP指令可以将两个寄存器中的两个4-byte, 8-byte数据存储到内存中。 LDP/STP有数据地址对齐要求。

之前的memcpy, memset的实现可以利用这些指令组合实现,还可以利用NEON/FPU, SVE2的LDR/STR, LDP/STP,这些指令支持16-byte数据访问。

因为CPU微构架的不同,为一个微构架上优化过的memcpy, memset函数并不总能在其他微构架的CPU核上得到同样优化的性能,这可能是因为:
• 不同CPU微构架有不同的内存访问pipeline数量,不同的硬件数据通路位宽,不同outstanding访问数量支持
• 不同的硬件prefetch设计
• 大小核有不同的硬件内存访问设计
• 不同CPU微构架对非对齐访问的支持效率不一样
• 有些CPU可以从使用NEON,SVE2内存访问获益,而有些CPU不能

另外采用LDR/STR指令组合实现的memcpy, memset的过程中通常需要使用更多指令和寄存器,这会带来一定的寄存器使用压力。

为了帮助软件可以得到优化的memcpy和memset性能,arm通常会提供这些CPU的software optimization guide, 其中会包含基本的pipeline信息和如何通过LDR/STR指令得到好的memcpy和memset性能。例如在Cortex-A76的software optimization guide中提到:

https://documentation-service...

The Cortex-A76 processor includes two load/store pipelines, which allow it to execute two 128 bit load uops and one 128 bit store uop every cycle:

因此建议采用NEON/FPU寄存器的 LDP/STP指令组合实现memcpy:

Loop_start:
    SUBS    X2,X2,#192
    LDP    Q3,Q4,[x1,#0]
    STP    Q3,Q4,[x0,#0]
    LDP    Q3,Q4,[x1,#32]
    STP    Q3,Q4,[x0,#32]
    LDP    Q3,Q4,[x1,#64]
    STP    Q3,Q4,[x0,#64]
    LDP    Q3,Q4,[x1,#96]
    STP    Q3,Q4,[x0,#96]
    LDP    Q3,Q4,[x1,#128]
    STP    Q3,Q4,[x0,#128]
    LDP    Q3,Q4,[x1,#160]
    STP    Q3,Q4,[x0,#160]
    ADD    X1,X1,#192
    ADD    X0,X0,#192
    BGT    Loop_start

而在通常与Cortex-A76配对使用的Cortex-A55 CPU的software optimization guide
https://developer.arm.com/doc...
中提到:

the store pipeline width is 128 bits,  the load data path width is 64 bits, all load instructions that read 64 bits of data take a single cycle to issue if the address is 64-bit  aligned and all load instructions that read 128-bits of data take two cycles to issue if the address is 64-bit aligned.

因为issue能力和data path位宽不同,因此建议采用通用寄存器的 LDP/STP指令组合实现memcpy:

loop_start:
      STP  x3, x4, [x0, #-0x30]
      SUBS x2, x2, #0x40
      LDP  x3, x4, [x1, #-0x30]
      STP  x5, x6, [x0, #-0x20]
      LDP  x5, x6, [x1, #-0x20]
      STP  x7, x8, [x0, #-0x10]
      LDP  x7, x8, [x1, #-0x10]
      STP  x9, x10, [x0], #0x40
      LDP  x9, x10, [x1], #0x40
      B.NE    loop_start

通常, 有可能的话,我们建议指定合适的-mcpu编译和链接选项来得到较好的结果。但在大小核同时存在的系统,软件难以为运行在不同的CPU核上动态选择不同的memcpy, memset实现,而是使用统一的实现。

https://chromium.googlesource...

我们可以看到,它针对不同数据大小和对齐设计了不同代码路径进行处理。指令数量和寄存器的使用较多。

Arm引入的专门的memcpy, memset指令正是用来解决这些问题的:使用标准化统一的指令进行memcpy, memset,同时可以跨CPU微构架实现地利用硬件优化。

Arm memcpy, memset指令

FEAT_MOPS构架扩展引入了专门的memcpy, memset指令序列:
CPYxxx: Memcpy指令系列
SETxxx: Memset指令序列

这些指令的设计需要考虑如何进行安全的memcpy,如何处理在大块内存memcpy或memset指令过程中出现中断或是异常的情况,还有memcpy, memset过程中出现迁移到不同CPU核上(不同核有不同的memcpy,memset指令微构架实现)等问题。

有一个经典的memcpy问题,如何对memcpy(src, dst, size)进行安全的copy操作?
这里要考虑的是src和dst的内存有没有overlap的问题, 可以分成3种情况:

  • 没有overlap
    image.png
    这种情况不需要特殊考虑
  • dst overlaps src
    image.png
    这种情况下dst前部与source后部有重合,如果从src开头拷贝,会出现src前部拷贝到dst 前部(也即src的后部),导致src后部在拷贝之前被覆盖的情况:
    image.png
    如果从src的尾部往头部方向拷贝,可以避免这个问题。
  • src overlaps dst
    image.png
    这种情况下src前部与dst后部有重合,从src头部开始拷贝还是安全的。

在src和dst有 overlap的情况下,需要考虑如何避免overlap部分的src不能被dst覆盖。这涉及到memcpy是前向(Forward,从内存的低地址往高地址方向)拷贝还是后向(Backward,从内存的高地址往地址方向)拷贝。

Arm memcpy, memset指令三部曲

Arm的CPYxxx, SETxxx 指令序列(x表示指令还有附加信息)采用了三部曲的设计来完成memcpy, memset操作:
• Prologue -- 开头
• Main -- 主体
• Epilogue -- 结尾
image.png

例如以下指令:

  • CPYP** : Memcpy Prologue
  • CPYM** : Memcpy Main
  • CPYE** : Memcpy Epilogue

任何数据大小的Memcpy的操作可以由CPYP ,CPYM , CPYE** 这三条指令(三步)来完成。
构架上并未规定每步微构架应该做什么,这个自由度留个CPU硬件微构架。一般来说,

  • 在Prologue阶段,它做一些为后面Main的准备工作,如配置copy的方向,一部分的开头的拷贝工作,如拷贝一些数据直到数据地址达到一定的对齐要求,更新寄存器等。
  • 在Main阶段,预期它完成主体的Memcpy工作,更新寄存器
  • 在Epilogue 阶段,它完成扫尾工作,更新寄存器

在Prologue,Main,Epilogue阶段各拷贝多少数据由微构架实现决定(Implementation defined)。对于一些较小的memcpy数据,允许在Prologue阶段就拷贝完,Main, Epilogue阶段的指令并不需要做什么事情。
虽然在软件上看这三步合理,但在硬件实现上还需考虑:

  • 硬件上每个micro-ops (uops,CPU硬件微构架通常会将这些指令分解为pipeline里的微操作)单次可以copy的block size
  • 指令的latency和中断,异常处理
  • 这三阶段为不同的对齐,拷贝大小做的工作
  • 如何更新存储地址指针和数据大小的寄存器

memcpy, memset指令的基地址和数据大小寄存器的更新

为了CPU核微构架设计的灵活性,FEAT_MOPS构架扩展支持memcpy, memset过程采用两种基地址和数据大小的更新方式: Option A和Option B. 由CPU核微构架设计选择Option A还是Option B.
Option A
这种方式在Prologue阶段将地址指针调整为memcpy, memset的最终地址。
地址指针在Main, Epilogue过程中无需再次调整。
对于数据大小,当采用后向拷贝时它是个正值,当采用前向拷贝时它是个负值。数据大小在Prologue, Main, Epilogue过程朝着0进行调整。
构架上规定,Prologue阶段硬件设置PSTATE.C为0来表示采用方式为Option A.
Option B
这种方式在Prologue, Main, Epilogue 阶段根据每阶段拷贝大小调整地址指针。
对于数据大小,它总是个正值,拷贝的方式(Forward or Backward)由PSTATE.N来指示。
构架上规定,Prologue阶段硬件设置PSTATE.C为1来表示采用方式为Option B.

Memcpy指令

结合拷贝方向(Forward or Backward), FEAT_MOPS增加了如下memcpy指令:

CPY[F]Px [Xd]!, [Xs]!, Xn!  //CPY[F]Px [dst]!, [src]!, num_bytes!
CPY[F]Mx [Xd]!, [Xs]!, Xn!  //CPY[F]Mx [dst]!, [src]!, num_bytes!
CPY[F]Ex [Xd]!, [Xs]!, Xn!  //CPY[F]Ex [dst]!, [src]!, num_bytes!

如果指令中带有F,表示它只支持Forward copy。没有F的话,表示由指令自己选择拷贝方向。

不管哪种方式,第一步CPYP指令执行前,CPYP指令中的Xd, Xs, Xn应被软件赋值为memcpy(dst, src, size)中dst, src和size,CPYP指令后,和后续的CPYM, CPYE中的Xd, Xs, Xn由前面所述的Option A,Option B方式更新。
可以让软件通过简单三条Prologue, Main,Epilogue指令完成任何数据大小,地址对齐的memcpy操作。例如可以通过如下代码实现memcpy:

      //memcpy(dst, src, size)
     // dst is passed X0, src is passed X1, size is passed X2,
memcpy:
    MOV    x3, x0
    CPYP   [x3]!, [x1]!, x2!  
    CPYM   [x3]!, [x1]!, x2!  
    CPYE   [x3]!, [x1]!, x2!  
    RET
Memcpy Forward only指令

我们先来看CPYFPx, CPYFMx,CPYFEx 这些Forward only指令。
刚开始软件在CPYFP指令之前将copy的src基地址Xs_init设置到Xs, dst基地址Xd_init设置到Xd, 数据大小n设置到Xn。

image.png

下面我们以一个CPYF* (前向copy) Option A为例,演示其方式。
1. 最初软件将要copy的src基地址Xs_init设置到Xs, dst基地址Xd_init设置到Xd, 数据大小n设置到Xn
image.png

2. 执行 CPYFPx [Xd]!, [Xs]!, Xn!
Prologue阶段,拷贝implementation Defined大小(假设为p)数据,调整Xs=Xs_init+n, Xd=Xs_init+n, Xn=-(n-p)
image.png

3. 执行 CPYFMx [Xd]!, [Xs]!, Xn!
Main阶段,拷贝implementation Defined大小(假设为m)数据,调整Xn=-(n-p-m)
image.png

4. 执行 CPYFEx [Xd]!, [Xs]!, Xn!
Epilogue阶段,拷贝implementation Defined大小(假设为e)数据,调整Xn=0
image.png

下面我们再以一个CPYF* (前向copy) Option B为例,演示其方式。
1. 最初软件将要copy的src基地址Xs_init设置到Xs, dst基地址Xd_init设置到Xd, 数据大小n设置到Xn
image.png

2. 执行CPYFPx [Xd]!, [Xs]!, Xn!
Prologue阶段,拷贝implementation Defined大小(假设为p)数据,调整Xs=Xs_init+p, Xd=Xd_init+p, Xn=n-p
image.png

  1. 执行CPYFMx [Xd]!, [Xs]!, Xn!
    Main阶段,拷贝implementation Defined大小(假设为m)数据,调整Xs=Xs_init +p+m, Xd=Xd_init+p+m, Xn=n-p-m
    image.png

4. 执行CPYFEx [Xd]!, [Xs]!, Xn!
Epilogue阶段,拷贝implementation Defined大小(假设为e)数据,调整Xs=Xs_init +p+m+e, Xd=Xd_init+p+m+e, Xn=n-p-m-e=0
image.png

Memcpy Generic指令

对于CPYPx, CPYMx, CPYEx这些没有在指令中指定前向copy(指令中没有F)的通用指令,CPU微构架实现可以选择Option A或是Option B, 拷贝过程是前向拷贝还是后向拷贝由src, dst, size来决定。采用Option B时,硬件通过设置PSTATE.N为表明前向(PSTATE.N=1)还是后向拷贝(PSTATE.N=0)。
指令copy的方向在CPYP时,由以下方式决定:

  1. 如果Xn<63:55> != 000000000, Xn设置钳位为0x7FFFFFFFFFFFFF. 这个步骤后的值称之为Xn_staturated
  2. 然后,如果Xs > Xd并且 (Xd + Xn_staturated ) > Xs, 那么方向为Forward;
    image.png
    如果 Xs < Xd 并且 (Xs + Xn_staturated ) > Xd, 那么方向为Backward;
    image.png
    其他情况由CPU微构架硬件实现决定哪个方向。

如果是前向拷贝,这些指令更新寄存器的方式与CPYFPx, CPYFMx, CPYFEx类似,这里不再赘述。下面主要讲一下后向拷贝更新寄存器的方式:
image.png

下面以Option A +Backward拷贝为例,演示其方式:
1. 最初软件将要copy的src基地址Xs_init设置到Xs, dst基地址Xd_init设置到Xd, 数据大小n设置到Xn
image.png

2. 执行CPYPx [Xd]!, [Xs]!, Xn!
Prologue阶段,拷贝implementation Defined大小(假设为p)数据,调整寄存器Xn=n-p, Xs,  Xd寄存器不变
image.png

3.执行 CPYMx [Xd]!, [Xs]!, Xn!
Main阶段,拷贝implementation Defined大小(假设为m)数据,调整寄存器Xn=n-p-m
image.png

4. 执行 CPYEx [Xd]!, [Xs]!, Xn!
Epilogue阶段,拷贝implementation Defined大小(假设为e)数据,调整寄存器Xn=n-p-m-e=0
image.png

Memset指令

FEAT_MOPS引入了memset指令:
SETPx [dst]!, num_bytes!, src_data
SETMx [dst]!, num_bytes!, src_data
SETEx [dst]!, num_bytes!, src_data
Memset指令也有Option A,Option B,但没有memcpy的Forward和Backward的区分。

Memcpy, Memset指令额外访问控制

指令中的x用来额外指示访问是不是non-temporal和是不是以特权级方式访问。
image.png
例如CPYPTN, CPYMTN, CPYETN。

Arm memcpy, memset指令过程中的中断和异常处理

对于任何数据大小的memcpy, memset,都可以使用Prologue, Main, Epilogue三条指令来实现,例如,

CPYP [Xd]!, [Xs]!, Xn!
CPYM [Xd]!, [Xs]!, Xn!
CPYE [Xd]!, [Xs]!, Xn!

构架必须考虑这三条指令的过程中,如果出现中断或异常,应该怎样处理,如果等到这些指令完成全部拷贝才能响应中断或异常,那么会带来大的中断异常延时。想象一下如果拷贝几MB甚至更大的内存的场景。
因此构架允许在每个阶段过程中可以响应中断或异常, 在响应中断或异常时:

  • 硬件更新Xn, Xs, Xd寄存器为Prologue, Main或是Epilogue过程中第一个还没有被memcpy或是memset的元素,以便中断异常返回时,可以继续未完成的memcpy或是memset
  • 在这个时间点之前的所有内存都被写更新
  • 硬件设置ELR_ELx异常返回地址为被中断的指令地址

这样的处理使得被中断的Prologue, Main或是Epilogue过程可以不需要软件特别处理就可以在中断异常返回后继续。

Arm memcpy, memset指令软件支持

较新的GCC和LLVM compiler都已经支持FEAT_MOPS, 通过加上-march=+mops选项编译下面代码

#include <string.h>

void memcopy_test(void * dst, void *src, int size)
{
 memcpy(dst, src, size);
}

可以得到使用memcpy指令的代码:

memcopy_test:
 sxtw    x2, w2
 cpyfp    [x0]!, [x1]!, x2!
 cpyfm    [x0]!, [x1]!, x2!
 cpyfe    [x0]!, [x1]!, x2!
 ret

Linux kernel 已经支持FEAT_MOPS特性,可参见下面Linux kernel 代码:
https://elixir.bootlin.com/li...  

#ifdef CONFIG_AS_HAS_MOPS
    .arch_extension mops
SYM_FUNC_START(__pi_memcpy)
alternative_if_not ARM64_HAS_MOPS
    b    __pi_memcpy_generic
alternative_else_nop_endif

    mov    dst, dstin
    cpyp    [dst]!, [src]!, count!
    cpym    [dst]!, [src]!, count!
    cpye    [dst]!, [src]!, count!
    ret
SYM_FUNC_END(__pi_memcpy)

https://elixir.bootlin.com/li...  

#ifdef CONFIG_AS_HAS_MOPS
    .arch_extension mops
SYM_FUNC_START(__pi_memset)
alternative_if_not ARM64_HAS_MOPS
    b    __pi_memset_generic
alternative_else_nop_endif

    mov    dst, dstin
    setp    [dst]!, count!, val_x
    setm    [dst]!, count!, val_x
    sete    [dst]!, count!, val_x
    ret
SYM_FUNC_END(__pi_memset)
SYM_FUNC_START(__pi_copy_page)
#ifdef CONFIG_AS_HAS_MOPS
    .arch_extension mops
/*
 * Copy a page from src to dest (both are page aligned)
 *
 * Parameters:
 *    x0 - dest
 *    x1 - src
 */
SYM_FUNC_START(__pi_copy_page)
#ifdef CONFIG_AS_HAS_MOPS
    .arch_extension mops
alternative_if_not ARM64_HAS_MOPS
    b    .Lno_mops
alternative_else_nop_endif

    mov    x2, #PAGE_SIZE
    cpypwn    [x0]!, [x1]!, x2!
    cpymwn    [x0]!, [x1]!, x2!
    cpyewn    [x0]!, [x1]!, x2!
    ret
.Lno_mops:

总结

通过FEAT_MOPS引入专门的memcpy, memset指令,可以让软件通过简单三条指令(Prologue, Main,Epilogue三步)完成任何数据大小,地址对齐的memcpy, memset操作。同时让CPU微构架实现硬件自己来优化内存访问过程,让memcpy, memset这类软件中最常用的操作标准化,从而避免不同的memcpy, memset函数实现带来的软件复杂度。FEAT_MOPS让arm平台软件更加简单标准高效。

更多有关FEAT_MOPS特性,请参照arm构架文档,https://developer.arm.com/doc...  

推荐阅读
关注数
8659
内容数
62
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息