【问题标题】:Static branch prediction on Arm9 with RVCT4.0使用 RVCT4.0 在 Arm9 上进行静态分支预测
【发布时间】:2011-07-20 21:01:57
【问题描述】:

我正在为 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?还有其他方法吗(希望没有内联汇编)?

【问题讨论】:

  • 这段代码没有意义。如果 pProvider 是第一个 arg 并且在没有取消引用的情况下检查 NULL,则编译器将不需要 ldr,因为 pProvider 已经在 r0 中。看起来您正在查看 Provider 类型的偏移量。
  • @Variable Length Coder,日志函数是内联的,不用担心参数。 ldr 指令正在从某些数据结构中获取 pProvider。如果函数没有内联,这将在调用之前发生。
  • 作为参考,RVCT 3.0 文档表明支持__builtin_expect

标签: c optimization embedded arm arm9


【解决方案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,所以你必须自己写)

【讨论】:

  • 我尝试重写日志记录代码,但 RVCT 生成相同的代码。用内联汇编重写可能是要走的路,但我不确定编译器是否会尊重这一点,因为这是一个块布局问题。无论如何感谢您的建议。
  • 是的,编译器优化可能有问题。不适用于 RVCT,但 gcc 允许您为程序集使用 __ volatile __(不带空格;但否则它只会使其变为粗体)关键字,以确保编译器不理会代码(基本上意味着它不会移动/删除代码)。
【解决方案2】:

如果您使用以下构造:

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)

【讨论】:

  • 遗憾的是,我们使用的是 RVCT,而不是 GCC。 RVCT 文档表明 __builtin_expect 应该可以工作,但它没有效果。不过,我会听取您关于可能/不太可能的宏的建议。
  • 即使使用 GCC,无论__builtin_expect() 如何,都会生成相同的代码,但我的测试相当简单。我对保留它的评论是为了以防它在更复杂的场景中对编译器有所帮助。我认为关键是保持内联log_some_stuff() 函数足够短,以便编译器使用条件操作码有效地使日志记录在常见情况下只有几个 NOP。如果内联函数太长,则 NOP 将花费与在分支后重新填充管道一样多或更多的周期,因此避免分支将没有任何好处。
【解决方案3】:

您的分支优化只会给您带来很少的好处。如果您执行以下操作,您可以获得更多:

#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 无效(你会无论如何都必须这样做)。总的来说,我敢打赌,这将远远超过通过提前退出一条指令所节省的单个周期。

【讨论】:

  • 使用宏而不是仅仅内联函数(他已经在做的)应该是相同的结果,并且您可以解决使用宏遇到的所有可怕问题。看不到优势
  • 我已验证该函数已被内联。我上面发布的指令实际上是在正常情况下为日志功能执行的仅有的三个指令。
【解决方案4】:

你可以使用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 是否有它。

【讨论】:

    猜你喜欢
    • 2013-09-04
    • 2015-12-24
    • 2020-06-01
    • 2015-10-17
    • 2012-02-12
    • 2014-04-25
    • 2014-03-03
    • 2011-09-28
    相关资源
    最近更新 更多