Cache不对齐程序反而更快 | 不是逗你玩儿
这是一个关于cache的小秘密。最近,小编在编写一段汇编代码的时候,有了一个利用cache的特性进行代码优化的小发现:有时,不对齐的Cache反而使程序执行更快!乍听上去,是不是很不可思议?话不多说,直接进入正题。
首先,对于cache这个东西,大家一定不陌生,它对于程序运行性能的提升可谓是堪称火箭助推,简直就是从外置Flash里执行程序,从SDRAM里读写数据的性能大救星。
不过,在Cortex-M7之前,M3、M4、M33这样的CPU上可是没有原配的Cache功能,当我们的Cortex-M7初出茅庐的时候,cache才粉墨登场,登上MCU这片广袤的大地。让我们这些常对MCU爱的深沉的人们,感受到了性能提升的快乐。 而更加让人喜悦的是,我们的i.MX RT 1050/60系列可是拥有32K的L1 I/D cache,是的你没有看错,不仅有cache,而且有两种,无论代码从何种介质运行,内部RAM,外部RAM,亦或是外部Flash期间,都会极大的提高性能。
Cache能发挥作用是利用了,程序大概率的空间局部性和时间局部性的规律。通俗地说,就是现在被访问过的数据,很可能未来还要被访问;在一个地方被访问的数据,很可能它的“密切接触者”接下来也要被访问(嗯,怎么有点像在排查病毒?) 在Cortex-M7中,Cache的使用是“小规模批发”式的:以32字节为单位装填,并且地址是32字节对齐的。也就是说,就算程序只用了其中一个字节,整个32字节都会被读进来。不过,M7不会让CPU干等。只要在装填过程中,发现CPU要求的数已经读进来了,就先放CPU走,然后自己继续干活。(注意:这个性质对于接下来要讲的程序很关键)。
不知大家注意到没有:上面说了的首次访问被装入到cache的数据,只是“大概率”会被再次访问。那么,有没有小概率的情况呢?比如,遍历一遍数据块?如果是这样,又会有什么有意思的现象?接下来,就让小编给您娓娓道来。
我们的测试代码是一段RGB8888转RGB565的汇编代码,一次性读取8个words到寄存器中,每个word是一个RGB8888的像素,接下来在寄存器中把它们转换成RGB565格式: - __attribute__((naked)) void rgb32Torgb565(uint32_t *src, uint16_t *dst, uint32_t len){
- __asm volatile (
- "push {r3-r11, lr} \n"
- // handle the remaining 8bytes first
- "loop1: \n"
- " ldmia r0!, {r4-r11} \n"
-
- " lsrs r4, #3 \n" // pix 1
- " lsls r5, #8 \n" // pix 2
- " ubfx r3,r4,#8+2-3, #6 \n" //green
- " ubfx ip,r5,#8+2+8, #6 \n"
-
- " bfi r4,r3,#0+5,#6 \n"
- " bfi r5,ip,#16+5,#6 \n"
-
- " ubfx r3,r4,#16+3-3,#5 \n" //red
- " ubfx ip,r5,#3+8,#5 \n" //blue
- " bfi r4,r3,#5+6,#5 \n"
- " bfi r5,ip,#16,#5 \n"
-
- " bfi r5,r4,#0,#16 \n"
- " nop \n"
-
- " lsrs r6, #3 \n" // pix 3
- " lsls r7, #8 \n" // pix 4
- " ubfx r3,r6,#8+2-3, #6 \n" //green
- " ubfx ip,r7,#8+2+8, #6 \n"
-
- " bfi r6,r3,#0+5,#6 \n"
- " bfi r7,ip,#16+5,#6 \n"
-
- " ubfx r3,r6,#16+3-3,#5 \n" //red
- " ubfx ip,r7,#3+8,#5 \n" //blue
- " bfi r6,r3,#5+6,#5 \n"
- " bfi r7,ip,#16,#5 \n"
-
- " bfi r7,r6,#0,#16 \n"
- " nop \n"
-
- " lsrs r8, #3 \n" // pix 5
- " lsls r9, #8 \n" // pix 6
- " ubfx r3,r8,#8+2-3, #6 \n" //green
- " ubfx ip,r9,#8+2+8, #6 \n"
-
- " bfi r8,r3,#0+5,#6 \n"
- " bfi r9,ip,#16+5,#6 \n"
-
- " ubfx r3,r8,#16+3-3,#5 \n" //red
- " ubfx ip,r9,#3+8,#5 \n" //blue
- " bfi r8,r3,#5+6,#5 \n"
- " bfi r9,ip,#16,#5 \n"
-
- " bfi r9,r8,#0,#16 \n"
- " nop \n"
-
- " lsrs r10, #3 \n" // pix 7
- " lsls r11, #8 \n" // pix 8
- " ubfx r3,r10,#8+2-3, #6 \n" //green
- " ubfx ip,r11,#8+2+8, #6 \n"
-
- " bfi r10,r3,#0+5,#6 \n"
- " bfi r11,ip,#16+5,#6 \n"
-
- " ubfx r3,r10,#16+3-3,#5 \n" //red
- " ubfx ip,r11,#3+8,#5 \n" //blue
- " bfi r10,r3,#5+6,#5 \n"
- " bfi r11,ip,#16,#5 \n"
-
- " bfi r11,r10,#0,#16 \n"
- " nop \n"
-
- " stmia r1!,{r5,r7,r9,r11} \n"
- " subs r2, #32 \n"
- " bne loop1 \n"
- " pop {r3-r11, pc} \n"
- );
- }
复制代码
这里的重点在于,先一次性读入数据,中间不再做任何访存操作。 (细心的朋友可能还发现了,这个程序很喜欢把像素“成双成对”地处理。嗯,听说过“双发射”的小伙伴可能会对此心有灵犀,这也是M7的另一个绝活,就是互相独立的操作有很多可以一个周期做两个,尤其是整数算术与逻辑部分) 准备测试数据, 这里有一个隐含假设是,我们的测试数据地址要32bits对齐,这个后续会进行分析: - #define ALIGN(n) __attribute__((aligned(n)))
- #define OFFSET 4
- __attribute__((section(".sdram"))) ALIGN(32) uint32_t test_array[1280*720 + OFFSET/sizeof(uint32_t)];
- __attribute__((section(".sdram"))) ALIGN(32) uint16_t dst[1280*720];
复制代码
并且把数据放到SDRAM上: RW_SDRAM 0x80200000 { *(.sdram) }
编写测试代码,为了保证不受已经装填的cache所影响,在测试程序前运行invalidate cache的操作,还我们一个“纯净”的cache: - PRINTF("BenchMark for the RGB32TORGB565 ASM function \r\n");
- memset(test_array, 0x56, sizeof(test_array) - OFFSET);
- SCB_CleanInvalidateDCache();
- uint32_t align_start = tick;
- rgb32Torgb565((void*)test_array, dst, sizeof(test_array) - OFFSET);
- uint32_t align_end = tick;
- PRINTF("Time costs with align address (0x%x): %d ms\r\n", test_array, align_end - align_start);
- SCB_CleanInvalidateDCache();
- SCB_DisableICache();
- SCB_EnableICache();
- uint32_t unalign_start = tick;
- rgb32Torgb565((void*)test_array + OFFSET, dst, sizeof(test_array) - OFFSET);
- uint32_t unalign_end = tick;
- PRINTF("Time costs with unalign address (0x%x): %d ms\r\n", (void*)test_array + OFFSET, unalign_end - unalign_start);
复制代码
当然了,要保证SDRAM区域是开了cache的: - /* Region 8 setting: Memory with Normal type, not shareable, outer/inner write back */
- MPU->RBAR = ARM_MPU_RBAR(8, 0x80000000U);
- MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_32MB);
复制代码
这里先留个悬念,我们先来公布下答案:
所谓的align address是指对32字节对齐的地址进行操作,而unalign address是刻意将地址+4,以满足不对齐到32字节,结果可能让大家大吃一惊,同样的程序,为何运行时间不同呢?更诡异的是,不对齐Cache反而让程序跑得更快!
请听小编娓娓道来,如分析不对,还请在评论区留言圈小编。 先说为何是32字节对齐的地址,因为我们的cache line是32字节的,即每次cache会load一条完整的cache line,即32字节的数据到cache,这里有个隐含的假设,即无论你需要多少数据,即便少于32字节,cache也会不辞辛苦的load一条完整的cache line进来。 而在这里,cache还有一个小彩蛋留给我们,当cache发现已经填满我们所需要的数据时,cache会直接放手,告诉CPU,Hi,你需要的数据我已经准备好了,你可以走了。这样,CPU可以将需要的数据拿走,而cache会回过头去继续默默地装填。
而小编所发现的正是利用这两个小福利对代码进行优化,为何将地址设置成不对齐到cacheline之后反而速度会有所提升,先上图,后分析:
图中灰色表示cache中没有数据,深绿色表示命中,浅绿色表示cache自动填充,而红色是没有命中的数据。
首先是程序刚开始的时候,开始前,我们的cache是干干净净的,第一轮运行时,因为我们要一次性读取32字节的数据, 由于此时地址不是32字节对齐的,cache会装填两条cache line,第一条cache line由于地址不对齐,会只装载28字节的数据,但是,当装填第二条时,有趣的事情发生了,当cache刚好填完一个word之后,刚好收够了CPU所需要的32字节的数据,此时cache就会通知CPU,你可以拿走了。
CPU开心的拿过数据之后,cache可没有闲着,刚才也说过了,cache一次性会装填一整条cache line。这时,cache会继续将当前的cache line填满,而此时CPU其实已经在运行我们的代码了。当第二轮运行时,CPU所需要的前28字节数据估计已经有不少,甚至全都躺在cache中晒太阳了,可以直接被拿走。此时,只需要cache再多读取一个word即可,放CPU走。如此循环往复,直到程序运行结束。
总结一下,所依据的理论基础就是,刻意利用非对齐到32字节s的地址访问,可以保证CPU在干活的时候,cache可以自动装填剩余数据到cache line,利用cache的装载特性实现一种数据与代码执行之间的并发操作,提高性能。
好了,这就是小编最近发现的关于cache的一个”骚”操作,希望能够帮到大家。 不过呢,大家还是要见机行事,不保证所有的程序都可以利用这个“花招”的形式进行程序的加速。一个典型的规律就是:如果要对一个大块内存遍历操作,并且一次先读入32字节(16字节也行,差点儿),再处理,就可以使用这个花招。
文章出处: 恩智浦MCU加油站
|