优化 ARM Cortex M3 代码

Posted

技术标签:

【中文标题】优化 ARM Cortex M3 代码【英文标题】:Optimizing ARM Cortex M3 code 【发布时间】:2014-06-08 19:42:14 【问题描述】:

我有一个 C 函数,它试图将帧缓冲区复制到 FSMC RAM。

这些函数将游戏循环的帧速率吃到 10FPS。我想知道如何分析反汇编的函数,我应该计算每个指令周期吗?我想知道 CPU 把时间花在哪里,在哪一部分。我确定算法也是一个问题,因为它的 O(N^2)

C 函数是:

void LCD_Flip()


    u8  i,j;


    LCD_SetCursor(0x00, 0x0000);
    LCD_WriteRegister(0x0050,0x00);//GRAM horizontal start position
    LCD_WriteRegister(0x0051,239);//GRAM horizontal end position
    LCD_WriteRegister(0x0052,0);//Vertical GRAM Start position
    LCD_WriteRegister(0x0053,319);//Vertical GRAM end position
    LCD_WriteIndex(0x0022);

    for(j=0;j<fbHeight;j++)
    
        for(i=0;i<240;i++)
        
            u16 color = frameBuffer[i+j*fbWidth];
            LCD_WriteData(color);

        
    


反汇编函数:

08000fd0 <LCD_Flip>:
 8000fd0:   b580        push    r7, lr
 8000fd2:   b082        sub sp, #8
 8000fd4:   af00        add r7, sp, #0
 8000fd6:   2000        movs    r0, #0
 8000fd8:   2100        movs    r1, #0
 8000fda:   f7ff fde9   bl  8000bb0 <LCD_SetCursor>
 8000fde:   2050        movs    r0, #80 ; 0x50
 8000fe0:   2100        movs    r1, #0
 8000fe2:   f7ff feb5   bl  8000d50 <LCD_WriteRegister>
 8000fe6:   2051        movs    r0, #81 ; 0x51
 8000fe8:   21ef        movs    r1, #239    ; 0xef
 8000fea:   f7ff feb1   bl  8000d50 <LCD_WriteRegister>
 8000fee:   2052        movs    r0, #82 ; 0x52
 8000ff0:   2100        movs    r1, #0
 8000ff2:   f7ff fead   bl  8000d50 <LCD_WriteRegister>
 8000ff6:   2053        movs    r0, #83 ; 0x53
 8000ff8:   f240 113f   movw    r1, #319    ; 0x13f
 8000ffc:   f7ff fea8   bl  8000d50 <LCD_WriteRegister>
 8001000:   2022        movs    r0, #34 ; 0x22
 8001002:   f7ff fe87   bl  8000d14 <LCD_WriteIndex>
 8001006:   2300        movs    r3, #0
 8001008:   71bb        strb    r3, [r7, #6]
 800100a:   e01b        b.n 8001044 <LCD_Flip+0x74>
 800100c:   2300        movs    r3, #0
 800100e:   71fb        strb    r3, [r7, #7]
 8001010:   e012        b.n 8001038 <LCD_Flip+0x68>
 8001012:   79f9        ldrb    r1, [r7, #7]
 8001014:   79ba        ldrb    r2, [r7, #6]
 8001016:   4613        mov r3, r2
 8001018:   011b        lsls    r3, r3, #4
 800101a:   1a9b        subs    r3, r3, r2
 800101c:   011b        lsls    r3, r3, #4
 800101e:   1a9b        subs    r3, r3, r2
 8001020:   18ca        adds    r2, r1, r3
 8001022:   4b0b        ldr r3, [pc, #44]   ; (8001050 <LCD_Flip+0x80>)
 8001024:   f833 3012   ldrh.w  r3, [r3, r2, lsl #1]
 8001028:   80bb        strh    r3, [r7, #4]
 800102a:   88bb        ldrh    r3, [r7, #4]
 800102c:   4618        mov r0, r3
 800102e:   f7ff fe7f   bl  8000d30 <LCD_WriteData>
 8001032:   79fb        ldrb    r3, [r7, #7]
 8001034:   3301        adds    r3, #1
 8001036:   71fb        strb    r3, [r7, #7]
 8001038:   79fb        ldrb    r3, [r7, #7]
 800103a:   2bef        cmp r3, #239    ; 0xef
 800103c:   d9e9        bls.n   8001012 <LCD_Flip+0x42>
 800103e:   79bb        ldrb    r3, [r7, #6]
 8001040:   3301        adds    r3, #1
 8001042:   71bb        strb    r3, [r7, #6]
 8001044:   79bb        ldrb    r3, [r7, #6]
 8001046:   2b63        cmp r3, #99 ; 0x63
 8001048:   d9e0        bls.n   800100c <LCD_Flip+0x3c>
 800104a:   3708        adds    r7, #8
 800104c:   46bd        mov sp, r7
 800104e:   bd80        pop r7, pc

【问题讨论】:

您是否尝试将其复制到 RAM 中?该函数看起来像您正在将缓冲区打印到 LCD。 @Étienne 是的,实际上这就是我通过 FSMC 控制器所做的。 我给你发了电子邮件。那你不能用 DMA 来加速拷贝吗? @Étienne DMA 是一个选项,但问题是根据 STM32 LCD 接口应用说明,性能不会那么高。 st.com/st-web-ui/static/active/en/resource/technical/document/… 【参考方案1】:

没有完全回答你的问题,但我看到你渴望快速 循环的执行。

以下是书中的一些提示: 'ARM 系统开发人员指南:设计和优化系统 软件(计算机体系结构中的摩根考夫曼系列 和设计)' http://www.amazon.com/ARM-System-Developers-Guide-Architecture/dp/1558608745

第 5 章包含名为“C 循环结构”的部分。 以下是该部分的摘要:

高效地编写循环

使用倒数到零的循环。那么编译器就不需要分配一个寄存器来保存终止值,与零的比较是免费的。 默认使用无符号循环计数器和继续条件 i!=0 而不是 i>0。这将确保循环开销只有两条指令。 当您知道循环将至少迭代一次时,使用 do-while 循环而不是 for 循环。这样可以节省编译器检查循环计数是否为零的时间。 展开重要循环以减少循环开销。不要过度展开。如果循环开销占总开销的比例很小,那么展开会增加代码大小并损害缓存的性能。 尝试排列数组中的元素数是四或八的倍数。然后,您可以轻松地将循环展开两次、四次或八次,而不必担心剩余的数组元素。

根据摘要,您的内部循环可能如下所示。

uinsigned int i = 240/4;  // Use unsigned loop counters by default
                          // and the continuation condition i!=0

do

    // Unroll important loops to reduce the loop overhead
    LCD_WriteData( (u16)frameBuffer[ (i--) + (j*fbWidth) ] );
    LCD_WriteData( (u16)frameBuffer[ (i--) + (j*fbWidth) ] );
    LCD_WriteData( (u16)frameBuffer[ (i--) + (j*fbWidth) ] );
    LCD_WriteData( (u16)frameBuffer[ (i--) + (j*fbWidth) ] );

while ( i != 0 )  // Use do-while loops rather than for
                  // loops when you know the loop will
                  // iterate at least once

您可能还想尝试使用“pragmas”,例如:

#pragma Otime

http://www.keil.com/support/man/docs/armcc/armcc_chr1359124989673.htm

#pragma unroll(n)

http://www.keil.com/support/man/docs/armcc/armcc_chr1359124992247.htm

因为它是 Cortex-M3,所以尝试找出 MCU 硬件是否让您有机会安排代码/数据以利用其Harvard architecture(我体验到了 30% 的速度提升)。

see here my other answer

也许并非所有内容都适用于您的应用程序 (以相反的顺序填充缓冲区)。我只是想画画 您对本书的关注和可能的优化点。

【讨论】:

当您按降序遍历数组时,ARM 硬件预取器是否也能正常工作?在某些 x86 uarches 中,在内存中向上循环(从较低的地址开始)可能会更快一些。您仍然可以使用for (i=-size ; i != 0 ; ++i) sum += arr[size + i]; 计数为零(即从数组末尾开始的索引,负索引向上计数为零。) 我不知道 ARM 是不是按降序排列更快,但很有趣。 (顺便说一句,您的循环不是按访问顺序遍历数组吗?) 在我的循环中,i 增加,所以arr[size+i] 的地址按升序排列。我不确定我是否理解您的问题。 您说“即从数组末尾开始索引”。 当循环第一次执行i = -size 并且数组从它的开头被索引时:arr[size + (-size)] = arr[0]。我以为你的意思是这个循环for (i = 1 ; i == size; ++i) sum += arr[size - i]; 【参考方案2】:

您应该首先编译启用了速度优化的 C 代码。您提供的反汇编代码似乎将ij 计数器存储在堆栈上,这向内部循环添加了3 个加载/存储操作。您可能还想在内部循环中内联 LCD_WriteData

另一方面,如果你真的在内部循环中写入 LCD,那么性能可能会受到该接口的限制。

【讨论】:

最大更新率为 30Hz,我可以使用 FSMC 内存控制器写入 LCD。我很想知道该函数需要多少个周期,我应该手动计算指令周期吗? 计算指令是一个开始,但您必须注意,执行分支所需的时钟数会因重新填充管道所需的时间而异。此外,加载和存储有一些会影响时间的怪癖。如果LCD_WriteData 进行任何推送或弹出,则需要考虑推送或弹出的寄存器数量。 所以从您的角度来看,O(N^2) 的嵌套循环不会占用 CPU 时间那么多? 当然可以。我认为您的问题是关于如何measureestimate 延迟,计算指令周期是一种方法。如果只是想让代码更快,请专注于内部循环。首先让编译器尽可能多地做,然后从那里开始工作。【参考方案3】:

只是为了纯粹减少循环操作的数量,您可以这样做。我确实做了一些可能不准确的假设:您有一个来自i=0:239 的循环,我假设fbWidth240 相同。如果这不是真的,那么循环就必须更复杂。

void LCD_Flip()

    u16 i,limit = fbHeight+fbWidth;
    // We will use a precalculated limit and one single loop

    LCD_SetCursor(0x00, 0x0000);
    LCD_WriteRegister(0x0050,0x00);//GRAM horizontal start position
    LCD_WriteRegister(0x0051,239);//GRAM horizontal end position
    LCD_WriteRegister(0x0052,0);//Vertical GRAM Start position
    LCD_WriteRegister(0x0053,319);//Vertical GRAM end position
    LCD_WriteIndex(0x0022);

    // Single loop from 0:limit-1 takes care of having to do an
    // x,y conversion each iteration.
    for(i=0;i<limit;j++)
    
        u16 color = frameBuffer[i];
        LCD_WriteData(color);
    

这去掉了两个循环,取而代之的是一个 for 循环,每次迭代只有一个条件测试。最重要的是,frameBuffer 的索引现在是线性的,所以我们不需要乘以从 x,y 到线性存储的宽度。您的循环迭代不会减少(即它仍然是 O(N)N = height*width),但指令的数量应该已经减少了。

正如@Joe Hass 在他的回答中指出的那样,如果您真的受到 LCD 界面的限制,这实际上可能根本没有帮助。根据您使用的 STM32,FSMC 可能不会特别快,我无法想象 LCD 控制器也会非常快。

【讨论】:

我已将复杂性转换为线性。但是当直接写入 FSMC 时,我的 FPS 仍然很慢。我不确定是时钟问题还是什么。 我不希望我发布的内容有巨大的增长。我会检查你的 FSMC 配置,看看它是否可以更快地计时。我从来没有用它来驱动 LCD,所以我不确定控制器的运行速度有多快。我不知道它是否有帮助,但有一些 STM32 变体内置了 LCD 控制器,而不是使用 FSMC。

以上是关于优化 ARM Cortex M3 代码的主要内容,如果未能解决你的问题,请参考以下文章

ARM (Cortex M3) 的应用内编程如何工作?

ARM基础教程 | 深入 Cortex‐M3 的 Faults异常

如何展开ARM Cortex M3堆栈

ARM Cortex M3上的GCC:从特定地址调用函数

ARM Cortex M4(或M3)上的周期计数器?

ARM cortex M3寄存器及指令集