请选择 进入手机版 | 继续访问电脑版
查看: 924|回复: 0

Cache不对齐程序反而更快 | 不是逗你玩儿

[复制链接]
  • TA的每日心情
    开心
    前天 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3296

    主题

    6541

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    31903
    最后登录
    2024-3-28
    发表于 2021-4-1 14:01:44 | 显示全部楼层 |阅读模式
    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的数据,只是“大概率”会被再次访问。那么,有没有小概率的情况呢?比如,遍历一遍数据块?如果是这样,又会有什么有意思的现象?接下来,就让小编给您娓娓道来。

    首先是代码展示环节(代码可以在这里找到https://github.com/CristXu/cache_test.git):
    我们的测试代码是一段RGB8888转RGB565的汇编代码,一次性读取8个words到寄存器中,每个word是一个RGB8888的像素,接下来在寄存器中把它们转换成RGB565格式:
    1. __attribute__((naked)) void rgb32Torgb565(uint32_t *src, uint16_t *dst, uint32_t len){
    2.         __asm volatile (
    3.         "push {r3-r11, lr} \n"        
    4.         // handle the remaining 8bytes first
    5.         "loop1: \n"
    6.         "        ldmia r0!, {r4-r11} \n"
    7.         
    8.         "        lsrs r4, #3 \n"      // pix 1
    9.         "        lsls r5, #8 \n"                 // pix 2

    10.         "        ubfx r3,r4,#8+2-3, #6 \n"  //green
    11.         "        ubfx ip,r5,#8+2+8, #6 \n"
    12.         
    13.         "        bfi r4,r3,#0+5,#6 \n"
    14.         "        bfi r5,ip,#16+5,#6 \n"
    15.         
    16.         "        ubfx r3,r4,#16+3-3,#5 \n" //red
    17.         "        ubfx ip,r5,#3+8,#5 \n"   //blue

    18.         "        bfi r4,r3,#5+6,#5 \n"
    19.         "        bfi r5,ip,#16,#5 \n"
    20.         
    21.         "        bfi r5,r4,#0,#16 \n"
    22.         "        nop \n"
    23.         
    24.         "        lsrs r6, #3 \n"      // pix 3
    25.         "        lsls r7, #8 \n"                // pix 4

    26.         "        ubfx r3,r6,#8+2-3, #6 \n"  //green
    27.         "        ubfx ip,r7,#8+2+8, #6 \n"
    28.         
    29.         "        bfi r6,r3,#0+5,#6 \n"
    30.         "        bfi r7,ip,#16+5,#6 \n"
    31.         
    32.         "        ubfx r3,r6,#16+3-3,#5 \n" //red
    33.         "        ubfx ip,r7,#3+8,#5 \n"   //blue

    34.         "        bfi r6,r3,#5+6,#5 \n"
    35.         "        bfi r7,ip,#16,#5 \n"
    36.         
    37.         "        bfi r7,r6,#0,#16 \n"
    38.         "        nop \n"
    39.         
    40.         "        lsrs r8, #3 \n"      // pix 5
    41.         "        lsls r9, #8 \n"                // pix 6

    42.         "        ubfx r3,r8,#8+2-3, #6 \n"  //green
    43.         "        ubfx ip,r9,#8+2+8, #6 \n"
    44.         
    45.         "        bfi r8,r3,#0+5,#6 \n"
    46.         "        bfi r9,ip,#16+5,#6 \n"
    47.         
    48.         "        ubfx r3,r8,#16+3-3,#5 \n" //red
    49.         "        ubfx ip,r9,#3+8,#5 \n"   //blue

    50.         "        bfi r8,r3,#5+6,#5 \n"
    51.         "        bfi r9,ip,#16,#5 \n"
    52.         
    53.         "        bfi r9,r8,#0,#16 \n"
    54.         "        nop \n"
    55.         
    56.         "        lsrs r10, #3 \n"      // pix 7
    57.         "        lsls r11, #8 \n"        // pix 8

    58.         "        ubfx r3,r10,#8+2-3, #6 \n"  //green
    59.         "        ubfx ip,r11,#8+2+8, #6 \n"
    60.         
    61.         "        bfi r10,r3,#0+5,#6 \n"
    62.         "        bfi r11,ip,#16+5,#6 \n"
    63.         
    64.         "        ubfx r3,r10,#16+3-3,#5 \n" //red
    65.         "        ubfx ip,r11,#3+8,#5 \n"   //blue

    66.         "        bfi r10,r3,#5+6,#5 \n"
    67.         "        bfi r11,ip,#16,#5 \n"
    68.         
    69.         "        bfi r11,r10,#0,#16 \n"
    70.         "        nop \n"
    71.                
    72.         "        stmia r1!,{r5,r7,r9,r11} \n"
    73.         "        subs r2, #32 \n"
    74.         "        bne loop1 \n"
    75.         "        pop {r3-r11, pc} \n"               
    76.                 );
    77.         }
    复制代码

    这里的重点在于,先一次性读入数据,中间不再做任何访存操作。
    (细心的朋友可能还发现了,这个程序很喜欢把像素“成双成对”地处理。嗯,听说过“双发射”的小伙伴可能会对此心有灵犀,这也是M7的另一个绝活,就是互相独立的操作有很多可以一个周期做两个,尤其是整数算术与逻辑部分)
    准备测试数据, 这里有一个隐含假设是,我们的测试数据地址要32bits对齐,这个后续会进行分析:
    1. #define ALIGN(n)  __attribute__((aligned(n)))
    2. #define OFFSET 4
    3. __attribute__((section(".sdram"))) ALIGN(32) uint32_t test_array[1280*720 + OFFSET/sizeof(uint32_t)];
    4. __attribute__((section(".sdram"))) ALIGN(32) uint16_t dst[1280*720];
    复制代码

    并且把数据放到SDRAM上:
    RW_SDRAM 0x80200000 {
                   *(.sdram)
      }

    编写测试代码,为了保证不受已经装填的cache所影响,在测试程序前运行invalidate cache的操作,还我们一个“纯净”的cache:
    1. PRINTF("BenchMark for the RGB32TORGB565 ASM function \r\n");
    2. memset(test_array, 0x56, sizeof(test_array) - OFFSET);

    3. SCB_CleanInvalidateDCache();
    4. uint32_t align_start = tick;
    5. rgb32Torgb565((void*)test_array, dst, sizeof(test_array) - OFFSET);
    6. uint32_t align_end = tick;
    7. PRINTF("Time costs with align address (0x%x): %d ms\r\n", test_array, align_end - align_start);

    8. SCB_CleanInvalidateDCache();
    9. SCB_DisableICache();
    10. SCB_EnableICache();
    11. uint32_t unalign_start = tick;
    12. rgb32Torgb565((void*)test_array + OFFSET, dst, sizeof(test_array) - OFFSET);
    13. uint32_t unalign_end = tick;
    14. PRINTF("Time costs with unalign address (0x%x): %d ms\r\n", (void*)test_array + OFFSET, unalign_end - unalign_start);
    复制代码

    当然了,要保证SDRAM区域是开了cache的:
    1. /* Region 8 setting: Memory with Normal type, not shareable, outer/inner write back */
    2. MPU->RBAR = ARM_MPU_RBAR(8, 0x80000000U);
    3. MPU->RASR = ARM_MPU_RASR(0, ARM_MPU_AP_FULL, 0, 0, 1, 1, 0, ARM_MPU_REGION_SIZE_32MB);
    复制代码

    这里先留个悬念,我们先来公布下答案:
    3.1.png

    所谓的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之后反而速度会有所提升,先上图,后分析:
    3.2.png

    图中灰色表示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加油站

    签到签到
    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-3-28 16:00 , Processed in 0.114168 second(s), 19 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表