我最初有以下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()
,这样就不需要混淆汇编了?有没有更好的方法在汇编中做到这一点?
EDIT:movw
的整个想法是保持读取原子,因为counter
变量在中断内部递增。
2条答案
按热度按时间kmbjn2e31#
从我在GodBolt中的实验来看,即使使用
-O3
标志,avr-gcc优化器也不够复杂。我怀疑是否有任何其他标志可以欺骗它进行更多优化(我尝试了一些,但没有帮助)。因此,您使用内联汇编的方法确实看起来是合理的。原代码分析
counter
变量存储在r12
(LSB)和r13
(MSB)寄存器中。TCNT0
从I/O空间地址0x 32读取(通过in Rd, 0x32
指令)。1.根据avr-gcc ABI,在
r22(LSB):r23:r24(MSB)
中返回24位值。1.因此,总结一下,我们希望发生以下转移:
更新方案
查看代码,我猜你有某种定时器中断,当定时器达到某个上限阈值时,它会递增
counter
。如果是这种情况,即使在纯C版本中,代码也有更深层次的问题。重要的是,TCNT0
和counter
的读取应该是原子的!否则,如果中断发生在movw
和in
指令之间,则您的结果将不准确。例如:有两种方法可以解决此问题:
1.将
CLI / SEI
Package 在两个读取周围,以将它们放在一起,而不可能在中间中断。1.读取
TCNT0
两次,在阅读计数器之前和之后。如果第二次读取的结果较小,则意味着发生了中断,我们不能信任这些值,请重复读取。因此,没有bug的正确解决方案可能是这样的(根据需要添加内联规范):
制作:
雷霆:https://godbolt.org/z/YrWrT8sT4
新方案
添加了原子性要求后,我们必须使用
movw
指令。下面是一个版本,它最大限度地减少了内联汇编的数量,并尽可能多地使用C:雷霆:https://godbolt.org/z/P9a9K6n76
旧方案(无原子性)
提供了组装分析
它看起来是正确的,并提供了正确的结果。然而,有两件事我可以建议改进:
1.它有3条
mov
指令,需要3个周期来执行。gcc生成了类似的代码,因为movw
只在均匀对齐的寄存器上运行。但是你可以用2条mov
指令来替换这些指令,并且它也将消除对更大的uint32_t
变量的需要。1.我会避免硬编码
TCNT0
地址,以获得更好的代码可移植性。建议组装
下面是代码的一个稍微修改的版本:
然而,请注意这个解决方案的一个缺点-我们在阅读计数器时失去了原子性。如果两个
mov
指令之间发生中断,并且counter
在中断中被修改,我们可能会得到正确的结果。但是如果counter
从未被中断修改过,我宁愿使用两个单独的mov
指令来提高性能。Godbolt:https://godbolt.org/z/h3nT4or97(我删除了
inline
关键字以显示生成的程序集)afdcj2ne2#
你将在R13:R12中读取
counter
的值,所以你需要两个MOV
和一个IN
来读取TCNT0
。所以使用内联汇编的工作版本是:关于该解决方案的一些说明:
TCNT0
是在C/C++代码中读取的,而不是在汇编中读取的,因此编译器可以选择最佳指令来读取该SFR(IN
或LDS
,具体取决于arch)。TCNT0
的reg分配给与result
相同的寄存器。由于avr-gcc ABI是小端字节序,因此它会分配给result
的LSB。这在GCC内联汇编中没有问题,即使TCNT0
和result
具有不兼容的类型。count
这样的全局寄存器变量不能是volatile的,GCC会发出警告:Reason是历史表示,REG的内部表示甚至没有
volatile
属性。因此,您可能需要重新考虑代码。例如,像while (counter != 0) ...
这样的循环可能无法实现预期的功能。counter
这样的全局寄存器变量有一些注意事项:对于每个模块/编译单元,编译器必须知道它不能将变量分配给一些可以自由使用的寄存器。因此,您可以在每个模块中包含counter
的decl,包括那些甚至不使用counter
的模块。或者更好的是,使用-ffixed-12 -ffixed-13
编译所有模块。为了减少对调用约定的干扰,最好使用R2而不是R12。请注意,R12可能用于传递参数,libc / libgcc中的代码也可能使用R12,因为这些库无法知道R12(或R2)是禁止的。使用上面的代码并显示生成的程序集的示例是使用
-Os -save-temps
编译以下代码。原子阅读
counter
正如在评论中提到的,
counter
应该被原子地读取。使用movw
是1个tick,因此比cli
/sei
序列更快。使用24位变量就足够了。虽然我不确定少一个寄存器是否会有区别。无论如何,下面是一个使用movw
的解决方案。SFR在程序集中读取,因此它变得不稳定:请注意,内联汇编操作数打印修饰符
i
是在v4.7中引入的,与__uint24
的版本相同;所以不要对%i
挠头。