3

我正在为 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 个周期的惩罚(如果不采用分支则不会受到惩罚)。

我希望常见情况 whereNULL == pProvider成为快速情况,即不采用分支。如何让 RVCT 4.0 生成这样的代码?

我尝试使用__builtin_expect如下:

if (__builtin_expect(NULL == pProvider, 1))
    return;

不幸的是,这对生成的代码没有影响。我使用__builtin_expect不正确吗?是否有另一种方法(希望没有内联汇编)?

4

4 回答 4

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,所以你必须自己写)

于 2011-03-22T22:01:09.600 回答
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;
}

GCC 4.5.2-O2生成以下代码(至少对于我的简单测试)以调用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)
于 2011-03-23T05:31:09.253 回答
0

您的分支优化将给您带来的收益很少。如果您执行以下操作,您可以获得更多收益:

#define log_some_stuff(pProvider, other_arg) \
        do {\
           if(pProvider != NULL) \
              real_log_some_stuff(pProvider, other_arg); \
         } \
        while(0)

这将做的是将NULL检查内联到所有调用代码中。这似乎是一种损失,但真正发生的是编译器可以避免函数调用的开销,包括推送寄存器、分支本身,以及通过简单的 NULL 检查使 r0-r3 和 lr 无效(你会无论如何都必须这样做)。总的来说,我敢打赌,这将远远超过通过提前退出一条指令所节省的单个周期。

于 2011-03-23T06:15:31.957 回答
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 有它。

于 2011-03-23T08:00:28.717 回答