c - ARM 皮质 M0+ : How to use "Branch if Carry" instructions in C-code?

标签 c assembly arm compiler-optimization micro-optimization

我有一些 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,如果设置了进位位,该 MCU 具有分支指令(请参阅 cortex-m0+ manual ,第 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)并检查内置调试器中的值。如果您使用不同的环境,您可能需要添加一些 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);
}

最佳答案

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

// 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 + GAS 写入 lsrs在 asm 代码中会导致在拇指模式下组装错误,但如果你写 lsr它成功组装成 lsrs操作说明。 (在 Cortex-M 不支持的 ARM 模式下,lsrlsrs 都按预期汇编为单独的指令。)

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



-O1
-O2
-O3


原来的
812us
780us
780us

带汇编
748us
686us
716us

带 asm + 一些循环展开
732us
606us
648us


因此,使用我的 ASM 代码和 -O2 而不是 -O1,我获得了 15% 的加速,并且通过额外的循环展开,我获得了 25% 的加速。
使用 __attribute__ ((section(".ramfunc"))) 将函数放在 RAM 中产生另外 1% 的改进。 (请务必在您的设备上对此进行测试,某些 MCU 的闪存缓存未命中惩罚很严重。)
有关更多通用优化的信息,请参阅下面的 old_timer 答案。

关于c - ARM 皮质 M0+ : How to use "Branch if Carry" instructions in C-code?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68601363/

相关文章:

c - 我们可以读取并错误注入(inject)另一个线程的程序计数器吗?

position - 尝试在 cortex-m3 上加载与位置无关的代码

c - 将 GCC 内联汇编与采用立即值的指令一起使用

从 ‘int’ 转换为 ‘float’ 可能会改变它的值

c - argv 是字符数组时怎么会是字符串数组?

c - GCC 内联汇编不允许我覆盖 $esp

xcode - 错误 : unexpected token in '.section' directive . 部分 .text

c - float 不精确

c - 如何用 Bison 解析 C 字符串

assembly - 在 ARM 程序集中将立即值放入括号中的目的是什么?