ARM处理器为何预取重叠内存地址?Linux中pld指令偏移疑问
首先贴出你提到的copy_template.S片段代码:
UNWIND( .fnstart ) enter r4, UNWIND(fpreg,) lr UNWIND( .setfp fpreg, sp ) UNWIND( mov fpreg, sp ) subs r2, r2, #4 blt 8f ands ip, r0, #3 PLD( pld [r1, #0] ) bne 9f ands ip, r1, #3 bne 10f 1: subs r2, r2, #(28) stmfd sp!, {r5, r6, r8, r9} blt 5f CALGN( ands ip, r0, #31 ) CALGN( rsb r3, ip, #32 ) CALGN( sbcsne r4, r3, r2 ) @ C is always set here CALGN( bcs 2f ) CALGN( adr r4, 6f ) CALGN( subs r2, r2, r3 ) @ C gets set CALGN( add pc, r4, ip ) PLD( pld [r1, #0] ) 2: PLD( subs r2, r2, #96 ) PLD( pld [r1, #28] ) PLD( blt 4f ) PLD( pld [r1, #60] ) PLD( pld [r1, #92] )
一、ARM处理器为何要预取重叠的内存地址?
PLD是ARM的预取提示指令,仅用于告知处理器“后续大概率会访问该地址附近内存,建议提前加载到缓存”,处理器可根据硬件特性选择执行或忽略。
出现重叠预取的核心原因:
- 覆盖边界访问场景:在连续内存拷贝的循环中,当前循环的末尾访问区域常与下一个循环的起始区域重叠。提前预取这些重叠区域,能确保指令执行到对应内存访问时,缓存已完成加载,避免缓存未命中导致的性能等待。
- 兼容多架构硬件特性:不同ARM架构的预取器行为存在差异,部分旧架构对“明确指定即将访问的地址”响应更积极。即使地址属于已预取的缓存行,重复发送PLD指令也无额外开销,反而能强化预取提示,确保缓存行被保留在高速缓存中。
- 高性能拷贝的冗余保障:
copy_template.S是Linux针对ARM的高性能memcpy实现,这类代码会尽可能覆盖所有可能的内存访问模式,通过看似冗余的重叠预取,最大化缓存命中率。
二、标签2处pld [r1, #28]的设计原因(缓存行32字节)
你的猜测“避免未对齐导致缓存未命中”是核心逻辑,具体拆解如下:
兼容源地址r1的未对齐场景:
若r1不是32字节对齐(比如r1地址模32等于28),pld [r1, #0]会加载r1所在的缓存行,而pld [r1, #28]会指向下一个缓存行的起始附近地址(例如r1=32n+28时,r1+28=32(n+1)+24,属于32(n+1)开头的缓存行),一次预取就能覆盖当前和下一个两个缓存行,完美适配后续跨缓存行的拷贝操作。
若换成pld [r1, #32],当r1未对齐时,虽也能覆盖下一个缓存行,但r1+28能更早覆盖到跨行的访问区域——当拷贝执行到r1+28地址时,对应的缓存行已提前就绪。匹配后续循环的访问节奏:
标签2之后的代码每次处理96字节(subs r2, r2, #96),后续的pld [r1, #60]、pld [r1, #92]与pld [r1, #28]刚好间隔32字节(60-28=32,92-60=32),完全对应32字节的缓存行大小。预取的地址刚好覆盖接下来要拷贝的三个缓存行区域,确保循环执行时缓存始终就绪。无额外性能损耗:
即使r1是32字节对齐的,pld [r1, #28]指向当前缓存行的末尾4字节,预取该地址只会加载已被pld [r1, #0]预取过的缓存行,处理器会自动忽略重复请求,不会带来性能开销,还能确保缓存行被持续保留在高速缓存中,避免被置换。
内容的提问来源于stack exchange,提问作者전기현

