我原来有如下C代码:
volatile register uint16_t counter asm("r12");
__uint24 getCounter() {
__uint24 res = counter;
res = (res << 8) | TCNT0;
return res;
}
这个函数运行在一些热点地方,而且是内联的,我想把很多东西塞进一个ATtiny13,所以是时候优化它了
该函数编译为:
getCounter:
movw r24,r12
ldi r26,0
clr r22
mov r23,r24
mov r24,r25
in r25,0x32
or r22,r25
ret
我想出了这个程序集:
inline __uint24 getCounter() {
//__uint24 res = counter;
//res = (res << 8) | TCNT0;
uint32_t result;
asm(
"in %A[result],0x32" "\n\t"
"movw %C[result],%[counter]" "\n\t"
"mov %B[result],%C[result]" "\n\t"
"mov %C[result],%D[result]" "\n\t"
: [result] "=r" (result)
: [counter] "r" (counter)
:
);
return (__uint24) result;
}
uint32_t
的原因是“分配”第四个连续的寄存器并且编译器理解它是被破坏的(因为我不能在破坏列表中做像"%D[result]"
这样的事情)
我的组装正确吗?从我的测试看来是这样。 有没有办法让编译器更好地优化
getCounter()
,这样就不需要混淆汇编了?
有没有更好的方法在装配中做到这一点?
编辑:
movw
的整个想法是保持读取原子性,因为counter
变量在中断内部递增。
从我在 GodBolt 中的实验来看,即使有
-O3
标志,avr-gcc 优化器也不够复杂。我怀疑是否有任何其他标志可以欺骗它进行更多优化(我尝试了一些但没有帮助)。所以,你使用内联汇编的方法看起来确实是合理的。
counter
变量存储在r12
(LSB)和r13
(MSB)寄存器中。TCNT0
从 I/O 空间地址 0x32 读取(通过 in Rd, 0x32
指令)。r22(LSB):r23:r24(MSB)
中返回24位值。r24 <-- r13
r23 <-- r12
r22 <-- TCNT0
它看起来正确并提供正确的结果。但是,我可以建议改进两件事:
mov
指令,需要3个周期来执行。 gcc 生成了类似的代码,因为movw
仅在均匀对齐的寄存器上运行。但是你可以用 2 mov
指令替换它们,它也将消除对更大的 uint32_t
变量的需要。TCNT0
地址以获得更好的代码可移植性。所以这里是你的代码的一个稍微修改的版本:
inline __uint24 getCounter() {
__uint24 result;
asm(
"in %A[result], %[tcnt0]" "\n\t"
"mov %B[result], %A[counter]" "\n\t"
"mov %C[result], %B[counter]" "\n\t"
: [result] "=r" (result)
: [counter] "r" (counter)
, [tcnt0] "I" (_SFR_IO_ADDR(TCNT0))
);
return result;
}
但是,请注意这个解决方案的缺点——我们在读取计数器时失去了原子性。如果在两个
mov
指令之间发生中断,并且 counter
在中断内部被修改,我们可能会得到正确的结果。但是,如果 counter
永远不会被中断修改,我宁愿使用两个单独的 mov
指令来提高性能。
Godbolt: https://godbolt.org/z/h3nT4or97 (我删除了
inline
关键字以显示生成的程序集)
你会读取R13:R12中
counter
的值,所以你需要两个MOV
和一个IN
来读取TCNT0
。所以使用内联汇编的工作版本是:
#include <avr/io.h>
register uint16_t counter asm("r12");
static inline __attribute__((__always_inline__))
__uint24 getCounter (void)
{
__uint24 result;
__asm ("mov %B0, %A1" "\n\t"
"mov %C0, %B1"
: "=r" (result)
: "r" (counter), "0" (TCNT0));
return result;
}
关于该解决方案的一些说明:
最大内联是通过静态内联和 always_inline 实现的。
TCNT0
在 C/C++ 代码中读取,而不是在程序集中读取,因此编译器可以选择最佳指令来读取该 SFR(IN
或 LDS
取决于架构)。它也更方便,因为不需要来自 AVR-LibC 的 __SFR_IO_ADDR
gobbledegook。
GCC 会将读取
TCNT0
的寄存器分配给与 result
相同的寄存器。由于avr-gcc ABI是little endian,所以会分配到result
的LSB。即使 TCNT0
和 result
具有不兼容的类型,这对于 GCC 内联汇编来说都很好。
像
count
这样的全局寄存器变量不能volatile,GCC会警告:
warning: optimization may eliminate reads and/or writes to register variables [-Wvolatile-register-var]
volatile register uint16_t counter asm("r12");
^~~~~~~~
Reason 是历史表征,其中 REG 的内部表征甚至没有
volatile
属性。所以你可能会重新考虑你的代码。例如,像 while (counter != 0) ...
这样的循环可能不会达到您的预期。
使用像
counter
这样的全局寄存器变量有一些注意事项:对于每个模块/编译单元,编译器必须知道它不能将变量分配给一些可以自由使用的寄存器。因此,您可以在每个模块中包含 counter
的声明,包括那些甚至不使用 counter
的模块。或者更好的是,使用 -ffixed-12 -ffixed-13
编译所有模块。为了减少对 calling convention 的干扰,最好使用 R2 而不是 R12。请注意,R12 可能用于传递参数,libc / libgcc 中的代码也可能使用 R12,因为这些库无法知道 R12(或 R2 就此而言)是被禁止的。
使用上面的代码并显示生成的程序集的示例是使用
-Os -save-temps
编译以下代码。
void f (int, __int24);
int main (void)
{
f (0, getCounter() /* in R22:R20 */);
}
*.s 将读作:
main:
in r20,0x32
/* #APP */
mov r21, r12
mov r22, r13
/* #NOAPP */
ldi r25,0
ldi r24,0
rcall f
...