一、前言
我们知道在Arm Arch32里面有个突发传输指令LDM、STM,也就是说可以一次传输多个值,到底是多少个呢?根据手册里面所说:加载和存储多个寄存器。寄存器r0到r15的任何组合均可在ARM状态下传输。
也就是说传输到通用寄存器里面一次可以传输很多啊!
但是到了Arch64里面就取消掉这个指令了,取而代之的是LDP和STP,固定的一次最多只能取2个值,为何呢?
这篇文章接下来的部分就是为了探究这个问题的!
二、资料搜集
在 资料:《Cortex_A57_Software_Optimization_Guide_external》
中说到了用LDP做memcopy的好处是可以尽可能地利用load和store的pipeline:
4.5 Load/Store Throughput
The Cortex-A57 processor includes separate load and store pipelines, which allow it to execute one load μop and one store μop every cycle. .
译:由于a57是有分别独立存在的加载、存储流水线,也就是说配合多发射就可能在一个cycle内同时执行两条(ldr、str)指令。
To achieve maximum throughput for memory copy (or similar loops), one should do the following.
实现内存复制最大吞吐的指导思想如下:
- Unroll the loop to include multiple load and store operations per iteration, minimizing the overheads of looping.(
老生常谈:循环展开,使得循环部分产生的过冲减少,其实就是减少循环逻辑部分在整个指令执行数里面的比例,从而提高准确率以及减少分支预测的次数。
) - Use discrete, non-writeback forms of load and store instructions (such as LDRD and STRD), interleaving them so that one load and one store operation may be performed each cycle. Avoid load-/store-multiple instruction encodings (such as LDM and STM), which lead to separated bursts of load and store μops which may not allow concurrent utilization of both the load and store pipelines.
使用离散的、非写回的内存指令,并间隔开来从而最大化利用ld/st的双pipeline特性;避免使用LDM跟STM,因为这样子的话就会产生一些分散的突发传输,从而无法合理利用双pipeline特性。
The following example shows a recommended instruction sequence for a long memory copy in AArch32 state:
Loop_start:
SUBS r2,r2,#64
LDRD r3,r4,[r1,#0]
STRD r3,r4,[r0,#0]
LDRD r3,r4,[r1,#8]
STRD r3,r4,[r0,#8]
LDRD r3,r4,[r1,#16]
STRD r3,r4,[r0,#16]
LDRD r3,r4,[r1,#24]
STRD r3,r4,[r0,#24]
LDRD r3,r4,[r1,#32]
STRD r3,r4,[r0,#32]
LDRD r3,r4,[r1,#40]
STRD r3,r4,[r0,#40]
LDRD r3,r4,[r1,#48]
STRD r3,r4,[r0,#48]
LDRD r3,r4,[r1,#56]
STRD r3,r4,[r0,#56]
ADD r1,r1,#64
ADD r0,r0,#64
BGT Loop_start
A recommended copy routine for AArch64 would look similar to the sequence above, but would use LDP/STP instructions.
小结:上面的代码就是利用了循环展开、间隔LDRD和STRD从而利用双pipeline实现该两条指令并发执行。最后页也说了下,在Arch64模式下也是类似处理利用LDP/STP指令。
4.7 Non-Temporal Loads/Stores
The ARM v8-A architecture provides load/store non-temporal pair instructions (LDNP/STNP) that provide a hint to the memory system that an access is non-temporal or streaming, and unlikely to be repeated in the near future.
也就是说在ARM v8-A架构里面LDNP/STNP指令是non-temporal的,也就是说当你用这条指令的时候,就会给存储子系统传达这么一个消息:我现在处理的内存数据是流式的、非临时的,在不久的将来也是不会使用的。
Versions of the Cortex-A57 processor prior to r1p3 do not prefetch or cache non-temporal data and hence, there could be a performance degradation when using non-temporal loads/stores instead of normal loads/stores. Ignoring the non-temporal hint will avoid this degradation. To this effect, it is recommended that bit 52 of the CPU auxiliary control register (CPUACTLR_EL1[52]) be always enabled.
从上面的资料我们大概知道用LDP跟STP可以最大化利用ARM v8-A架构的存储双pipeline架构,但是其实里面还有很多细节的信息我们不清楚,因此我们继续进一步深挖:
(我想以后无论我干什么事情,都要是有这种耐心、严谨、深入实践的精神,应该人生是不会太困惑的吧!用心做好每一件事,不忘初心!)
- non-writeback指令代表什么意思?
- non-temporal指令代表什么意思?
- LDP/STP和LDNP/STNP指令的区别及各自特点?
三、思考&继续前行
把握了大的框架思想后,接下来就是填充细节了,不用急,沉下心来,慢慢来!
上面的writeback什么的是cache的一些特性参数,因此我们先看cache部分怎么解释。
这里提前介绍下我们的资料及其特点:
-
《ARM® Architecture Reference Manual ARMv8, for ARMv8-A architecture profile》Printed on: December 19, 2017
- 这个是Arm Archv8-A profile文档,跟一般芯片数据手册一样介绍寄存器信息的,指令具体怎么用,有什么细节就去这里查了!也就是说是给原厂的底层开发人员看的,或者是驱动开发人员看的,总之很底层了!参考对比当年的单片机开发人员。
-
《ARM® Cortex®-A Series Version: 1.0 Programmer’s Guide for ARMv8-A》Copyright © 2015 ARM. All rights reserved.
- 这里讲的很直白了,就是编程手册,给一般程序员看的,里面讲了ARM的一些宏观层面上的模块,也就是粗略介绍了下有哪些模块,有哪些特性,单数具体的参数就没有了!比如指令参数以及指令使用这里就没有了!
3.1 cache
我这里就不从cache的架构一路讲下来了,具体细节参看我以前写的cache专题文章。
先大概讲下哈:
我们写的时候是不是有两种情况,一种是写命中一种是写miss:
-
写命中:更新cache中的副本后
- write-through:直接一层一层往底层写回去,这样子回浪费很大的总线带宽啊!
- write-back: 尽可能地推迟更新,只有当这个cache line被踢除的时候才把它写回到下一层中,注意不是写回最底层哟,而是下一层;这样做的好处是减少总线流量,缺点是这样的操作复杂性就增高了啊!成本高了!
-
写不命中:
- write-allocate: 读取低一层中的块来更新这个告诉缓存块,注意都是以块也就是cache line为单位哈!
- non-write-allocate: 避开高速缓存,直接把这个字写入到低一层中。
直写高速缓存 --通常-->非写分配
写回高速缓存 --通常-->写分配
接下来主要就cache的一些策略结合官方文档进行介绍。
《ARM® Cortex®-A Series Version: 1.0 Programmer’s Guide for ARMv8-A》
高速缓存策略使我们能够描述何时应该将一行分配给数据缓存,以及当一个store指令在数据缓存中被执行时,应该发生什么情况。
cache的分配策略如下:
- 写分配:也就是当你只写1 byte miss的时候,硬件就不管你三七二十一,直接产生一个突发传输给memory子系统,然后获取到一个cache line的数据,cache line全部获取到位之后才把你要的那1 byte数据给你,怎么样是不是听着就很浪费。
这就好比在古代:杨贵妃说想要吃荔枝了,这可不得了,一道圣旨发下,底下的百官一阵折腾弄来了一大车的荔枝,但是最后到杨贵妃嘴里的最多就开始的那一盆子吧~你说是不是劳民伤财,浪费资源啊!
- 读分配:当读miss发生的时候就给其分配一个cache line。
3.2 prefetch
预取就是告诉memory子系统,我有个数据马上要用,你给安排一下!嘿嘿~像不像真实世界的走后门啊!提前打声招呼,提前准备好!VIP通道~~
这里分别介绍了在64bit跟32bit模式下指令的差异,我们主要关注的是预取的策略policy字段:
- KEEP就是保持在cache内部:又叫temporal prefetch,就是正常的额cache 分配策略;
- STRM就是流式处理,数据不会存储在cache里面的,也就是从cache上面流过,不留痕迹:non-temporal prefetch也就是说内存数据只用一次的,你不要放到cache 里面来浪费空间了,不要影响我的cache hit rate了。
3.3 LDP/STP
首先告诉你64bit模式下是没有32bit模式下的LDM跟STM指令的:
特点如下:
- 32bit模式下也是有LDRD跟STD指令的,但是在64bit模式下任意的两个整型寄存器都是可以读或者写的。
- 数据的读写是内存里面的连续位置的;
- 地址模式更严格:只能base + offset的模式进行访存,offset范围是正负127范围(7bit);
- 与32bit的LDRD跟STD指令不同的是:64bit模式下支持非对齐访问。
- 注意:LDP是支持SIMD的啊!
下面可以看到各种允许的访问方式:
这里对比看一下你就知道上述的两个参考文件的区别,这是profile文件,里面讲了指令具体怎么用的:
3.4 LDNP/STNP
LDNP跟STNP是Armv8才有的机制,non-temporal也就是说我读写的时候数据是不会存到cache里面来的!也就是说假如我只要2byte的数据,通常系统会加载1个cache line进来,但是只要用了LDNP这个指令,系统就不会一下加载一整个cache line进来了,而是只加载2byte进来,从而减少总线流量,进而可能减少取数据的时间;因此LDNP这种指令只适合那种数据量比较少的情况(一般小于一个cahche line),避免cache line加载的耗时耗费资源的情况。
non-temporal加载和存储能够放松对访存顺序的要求,如下图中的例子,LDNP可能比LDR要提前执行(因为我不需要进行cache line fill操作啊,我是流式读的呀,所以你LDR慢慢去cache line fill吧,我先把媳妇,哦不,数据给先娶回去了!),但是这里这样子就出问题了,因此要加barriar,从而保证执行顺序。
- 只支持一种访存模式;
- 这个指令会告诉memory子系统:我这是流式访存,这个数据我暂时只用一次的,因此就不劳烦你把数据给cache 缓存起来了!但是基于memory子系统类型,可能支持读预取和写打包加速等操作!
In addition, there is an exception to the usual memory ordering rules.
If an address dependency exists between two memory reads, and a Load Non-temporal Pair instruction generated the second read, then in the absence of any other barrier mechanism to achieve order, the memory accesses can be observed in any order by the other observers within the shareability domain of the memory addresses being accessed.
此外,通常的内存排序规则有一个例外。如果两个内存读之间存在地址依赖关系,第二个读指令是LDNP产生的,然后在没有任何其他屏障机制来保证顺序的情况下,这个可能就会乱序执行的!
是不是说可以利用这个来增大吞吐呢?
四、总结
在Arm v8-A架构里面因为有两个分别独立的ld、st pipeline,因此交叉使用LDP、STP指令可以并行使用pipeline,增大吞吐(因为armv7一般是单个加载存储单元,因此LDM、STM更有效);
LDNP、STNP是流式处理数据的,不会进行cache line fill操作,因此适合少量数据处理的情形,减少总线流量,比如堆栈恢复的时候,我堆栈就那么几个字节的数据,你常规操作是要加载一个cache line回来的啊,污染cahche空间不说还增大总线流量,因此我用LDNP更有效;