我正在为ARM9处理器编写一些日志记录C代码。如果存在动态模块,此代码将记录一些数据。该模块通常不会出现在生产版本中,但日志代码将始终被编译。想法是,如果客户遇到错误,我们可以加载此模块,日志代码将转储调试信息。
当模块不存在时,日志代码必须具有最小的影响,因此每个周期都很重要。通常,日志记录代码如下所示:
__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
if (NULL == pProvider)
return;
... logging code goes here ...
}
启用优化后,RVCT 4.0会生成如下所示的代码:
ldr r4,[r0,#0x2C] ; pProvider,[r0,#44]
cmp r4,#0x0 ; pProvider,#0
beq 0x23BB4BE (usually taken)
... logging code goes here...
... regular code starts at 0x23BB4BE
此处理器没有分支预测器,我的理解是每当采用分支时都会有2个周期的惩罚(如果不采用分支则不会受到惩罚)。
我希望NULL == pProvider
的常见情况是快速的情况,其中不采用分支。如何让RVCT 4.0生成这样的代码?
我尝试使用__builtin_expect
,如下所示:
if (__builtin_expect(NULL == pProvider, 1))
return;
不幸的是,这对生成的代码没有影响。我错误地使用__builtin_expect
了吗?还有另一种方法(希望没有内联汇编)吗?
答案 0 :(得分:1)
因此,如果没有分支预测器,并且在获取分支时会得到两个周期的惩罚,那么为什么不相应地重写程序呢? (实际上,您认为上面的示例已经会产生“正确”的代码,但我们可以尝试)
__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
if (pProvider) {
... logging code goes here ...
}
}
“可以”编译为:
ldr r4,[r0,#0x2C] ; pProvider,[r0,#44]
cmp r4,#0x0 ; pProvider,#0
bneq logging_code (usually NOT taken)
... regular code here
logging_code: .. well anywhere
如果你很幸运,但即使现在这样做,编译器的每次更改都可能会改变它,我不知道它是否会导致汇编代码与你正在使用的任何编译器。 所以可能无论如何都要在内联汇编中写它?没有那么多的代码和gcc(以及VC;我认为其他人也这样做)使这很容易。最简单的是你只需用日志代码定义一个额外的方法并调用它(不知道ARM ABI,所以你必须自己编写)
答案 1 :(得分:1)
如果您使用以下构造:
void log_some_stuff_implementation(Provider *pProvider, int x, int y, char const* str);
__inline void log_some_stuff(Provider *pProvider, int x, int y, char const* str)
{
if (__builtin_expect( pProvider != NULL, 0)) {
log_some_stuff_implementation(pProvider, x, y, str);
}
return;
}
带有-O2
的GCC 4.5.2会为log_some_stuff()
调用生成以下代码(至少对我的简单测试而言):
// r0 already has the Provider* in it - r2 has a value that indicates whether
// r0 was loaded with a valid pointer or not
cmp r2, #0
ldrne r3, [r1, #0]
addne r1, r2, #1
ldrneb r2, [r3, #0] @ zero_extendqisi2
blne log_some_stuff_implementation
因此在常见情况下(Provider *为NULL),使用了4条指令但由于条件没有执行,但ARM的管道没有被刷新。我认为这可能与您实际上不希望日志代码运行的常见情况一样好。
我认为关键是实际执行日志记录工作的代码是在单独的函数中非内联完成的,因此编译器可以合理地设置并调用该函数是一些有条件执行的指令的内联序列。由于实际的日志记录代码不需要优化,因此没有理由将其内联。它不应该是常见的情况,并且可能是代码将会做一些真正的工作。因此,函数调用的开销应该是可接受的(至少这是我的假设)。
顺便说一句,对于我的简单测试,即使遗漏了__builtin_expect()
,也会生成相同的代码序列(或基本上相同的序列),但是我想,在比我的简单测试更复杂的序列中,内置可能会帮助编译器。所以我可能会留下它,但我也可能使用更可读的版本,如Linux内核的宏:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
答案 2 :(得分:0)
您的分支优化将获得很少的收益。如果您执行以下操作,您可以获得更多收益:
#define log_some_stuff(pProvider, other_arg) \
do {\
if(pProvider != NULL) \
real_log_some_stuff(pProvider, other_arg); \
} \
while(0)
这样做会将NULL检查内联到所有调用代码中。这可能看起来像是一种损失,但真正发生的是编译器可以避免函数调用的开销,包括推送寄存器,分支本身,并使r0-r3和lr通过简单的NULL检查无效(你会不管怎样都不得不这样做。总的来说,我敢打赌,这会比通过提前退出一条指令而节省的单一周期获得更多。
答案 3 :(得分:0)
您可以使用goto
:
__inline void log_some_stuff(Provider *pProvider, other args go here...)
{
if (pProvider != NULL)
goto LOGGING;
return;
LOGGING:
... logging code goes here ...
}
使用__builtin_expect更容易,但我不确定RVCT是否拥有它。