首页 > 解决方案 > ARM Cortex M0+:如何在 C 代码中使用“Branch if Carry”指令?

问题描述

我有一些逐位处理数据的 C 代码。简化示例:

// input data, assume this is initialized
uint32_t data[len];

for (uint32_t idx=0; idx<len; idx++)
{
    uint32_t tmp = data[idx];
    
    // iterate over all bits
    for (uint8_t pos=0; pos<32; pos++)
    {
        if (tmp & 0b1)
        {
            // some code
        }
    
        tmp = tmp >> 1;
    }
}

在我的应用程序len中比较大,所以我想尽可能地优化内部循环。该// some code部分很小,并且已经进行了大量优化。

我正在使用 ARM Cortex M0+ MCU,如果设置了进位位,它有一个分支指令(参见cortex-m0+ 手册,第 45 页)。方便地移位位将 LSB(或 MSB)放入进位标志,因此理论上它可以在没有比较的情况下进行分支,如下所示:

// input data, assume this is initialized
uint32_t data[len];

for (uint32_t idx=0; idx<len; idx++)
{
    uint32_t tmp = data[idx];

    // iterate over all bits
    for (uint8_t pos=0; pos<32; pos++)
    {
        tmp = tmp >> 1;

        if ( CARRY_SET )
        {
            // some code
        }
    }
}

使用 C 代码和/或内联汇编器归档此文件的最佳方法是什么?// come code理想情况下,为了简单和更好的可读性,我想保留C 语言。


编辑 1:我已经使用 -O1、-O2 和 -03在GCC 5.4 GCC 6.3 上测试了此代码。对于每个设置,它都会生成以下汇编代码(注意tst我尝试获取的专用指令):

        if (data & 0b1)             
00000218   movs r3, #1       
0000021A   tst  r3, r6       
0000021C   beq  #4

编辑 2:最小的可重现示例。我正在 Atmel Studio 7 中编写代码(因为它适用于 MCU)并检查内置调试器中的值。如果您使用不同的环境,您可能需要添加 som IO 代码:

int main(void)
{
    uint32_t tmp = 0x12345678;
    volatile uint8_t bits = 0;  // volatile needed in this example to prevent compiler from optimizing away all code.

    // iterate over all bits
    for (uint8_t pos=0; pos<32; pos++)
    {
        if (tmp & 1)
        {
            bits++;    // the real code isn't popcount.  Some compilers may transform this example loop into a different popcount algorithm if bits wasn't volatile.
        }
        tmp = tmp >> 1;
    }

    // read bits here with debugger
    while(1);
}

标签: cassemblyarmcompiler-optimizationmicro-optimization

解决方案


我没有找到一个“简单”的解决方案,所以我不得不在汇编程序中编写我的简短算法。这是演示代码的样子:

// assume these values as initialized
uint32_t data[len];     // input data bit stream
uint32_t out;           // algorithm input + output
uint32_t in;            // algorithm input (value never written in asm)

for (uint32_t idx=0; idx<len; idx++)
{
    uint32_t tmp = data[idx];
    
    // iterate over all bits
    for (uint8_t pos=0; pos<32; pos++)
    {
        // use optimized code only on supported devices
        #if defined(__CORTEX_M) && (__CORTEX_M <= 4)
        asm volatile  // doesn't need to be volatile if you use the result
        (
            "LSR    %[tmp], %[tmp], #1" "\n\t"  // shift data by one. LSB is now in carry
            "BCC    END_%="             "\n\t"  // branch if carry clear (LSB was not set)
            
            /* your code here */        "\n\t"
        
            "END_%=:"                   "\n\t"  // label only, doesn't generate any instructions
        
            : [tmp]"+l"(tmp), [out]"+l"(out)    // out; l = register 0..7 = general purpose registers
            : [in]"l"(in)                       // in;
            : "cc"                              // clobbers: "cc" = CPU status flags have changed
                               // Add any other registers you use as temporaries, or use dummy output operands to let the compiler pick registers.
        );
        #else
        if (tmp & 0b1)
        {
            // some code
        }
        tmp = tmp >> 1;
        #endif
    }
}

对于您的应用程序,在标记的位置添加您的汇编代码,并使用寄存器从 C 函数中输入数据。请记住,在 Thumb 模式下,许多指令只能使用 16 个通用寄存器中的 8 个,因此您不能传递更多的值。

内联汇编很容易以微妙的方式出错,这些方式看似有效,但在内联到不同的周围代码后可能会中断。(例如,忘记声明一个clobber。) https://gcc.gnu.org/wiki/DontUseInlineAsm除非你需要(包括性能),但如果是这样,请确保你检查文档(https://stackoverflow. com/tags/inline-assembly/info)。

请注意,从技术上讲,正确的移位指令是LSRS(带有s设置标志的后缀)。但是,在 GCC 6.3 + GASlsrs上,在 asm 代码中编写会导致在 thumb 模式下组装错误,但如果您编写lsr它会成功组装成一条lsrs指令。(在 ARM 模式下,Cortex-M 不支持,lsr两者lsrs都按预期组装成单独的指令。)


虽然我不能分享我的应用程序代码,但我可以告诉你这个改变有多大的加速:

-O1 -O2 -O3
原来的 812us 780us 780us
带asm 748us 686us 716us
w/ asm + 一些循环展开 732us 606us 648us

因此,使用我的 ASM 代码和 -O2 而不是 -O1 我得到了 15% 的加速,并且通过额外的循环展开我得到了 25% 的加速。

将函数放在 RAM 中会__attribute__ ((section(".ramfunc")))产生另外 1% 的改进。(请务必在您的设备上进行测试,一些 MCU 的闪存缓存未命中惩罚非常严重。)

有关更通用的优化,请参阅下面的 old_timer 答案。


推荐阅读