c - 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);
}
解决方案
我没有找到一个“简单”的解决方案,所以我不得不在汇编程序中编写我的简短算法。这是演示代码的样子:
// 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 答案。
推荐阅读
- java - 使用 Java WebFilter 删除 Hashtag
- angular - 基于 Observable 的 paramMap 方法的用例
- flutter - 按下按钮时如何更改按钮图像
- java - SwitchCompat 无法与 firebase 数据库一起正常工作
- django - 如果表单数据无效,为什么 Django 会返回 http 200?
- python - pandas 中 .at 和 .loc 的时间复杂度是多少?
- powerbi - 用于度量的 Power BI REST API BadRequest
- git - 在不知道提交的情况下从 github 恢复更改
- winforms - 以编程方式关闭/取消打开 X509Certificate2UI 证书选择和 pin 对话框,无需进一步的用户交互
- r - 在 GenomicRanges 对象中合并具有相同属性的相邻 bin