GCC 4.4:避免在 gcc 中对 switch/case 语句进行范围检查?

Posted

技术标签:

【中文标题】GCC 4.4:避免在 gcc 中对 switch/case 语句进行范围检查?【英文标题】:GCC 4.4: Avoid range check on switch/case statement in gcc? 【发布时间】:2011-03-16 01:51:56 【问题描述】:

This is only an issue on GCC versions prior to 4.4, this was fixed in GCC 4.5.

是否可以告诉编译器 switch 中使用的变量适合提供的 case 语句?特别是如果它是一个小范围并且生成了一个跳转表。

extern int a;
main()

        switch (a & 0x7)    // 0x7  == 111  values are 0-7
        case 0: f0(); break;
        case 1: f1(); break;
        case 2: f2(); break;
        case 3: f3(); break;
        case 4: f4(); break;
        case 5: f5(); break;
        case 6: f6(); break;
        case 7: f7(); break;
        

我尝试 xor'ing 到低位(例如),使用枚举,使用 gcc_unreachable() 无济于事。生成的代码总是检查变量是否在范围内,添加一个无意义的分支条件并移走跳转表计算代码。

注意:这是在解码器的最内层循环中,性能很重要。

看来我不是only one。

没有办法告诉 gcc 永远不会采用默认分支, 虽然它会省略默认分支,如果它可以证明 根据之前的条件检查,值永远不会超出范围。

那么,你如何帮助 gcc 证明变量适合并且在上面的示例中没有默认分支? (当然没有添加条件分支。)

更新

    这是在 OS X 10.6 Snow Leopard 上使用 GCC 4.2(默认来自 Xcode)。Linux 中的 GCC 4.4/4.3 没有发生这种情况(由 Nathon 和 Jens Gustedt 报告。)

    李>

    示例中的函数是为了便于阅读,认为这些函数是内联的或只是语句。在 x86 上进行函数调用很昂贵。

    此外,如注释中所述,该示例属于数据(大数据)循环。

    使用 gcc 4.2/OS X 生成的代码是:

    [...]
    andl    $7, %eax
    cmpl    $7, %eax
    ja  L11
    mov %eax, %eax
    leaq    L20(%rip), %rdx
    movslq  (%rdx,%rax,4),%rax
    addq    %rdx, %rax
    jmp *%rax
    .align 2,0x90
    L20:
    .long   L12-L20
    .long   L13-L20
    .long   L14-L20
    .long   L15-L20
    .long   L16-L20
    .long   L17-L20
    .long   L18-L20
    .long   L19-L20
    L19:
    [...]
    

    问题出在cmp $7, %eax;ja L11;

    好的,我将采用丑陋的解决方案,并为低于 4.4 的 gcc 版本添加一个特殊情况,使用不带开关的不同版本并使用 goto 和 gcc 的 &&label 扩展。

    static void *jtb[] =  &&c_1, &&c_2, &&c_3, &&c_4, &&c_5, &&c_6, &&c_7, &&c_8 ;
    [...]
    goto *jtb[a & 0x7];
    [...]
    while(0) 
    c_1:
    // something
    break;
    c_2:
    // something
    break;
    [...]
    
    

    注意标签数组是静态的,所以不是每次调用都计算。

【问题讨论】:

【参考方案1】:

也许您可以使用函数指针数组而不是开关?

#include <stdio.h>

typedef void (*func)(void);

static void f0(void)  printf("%s\n", __FUNCTION__); 
static void f1(void)  printf("%s\n", __FUNCTION__); 
static void f2(void)  printf("%s\n", __FUNCTION__); 
static void f3(void)  printf("%s\n", __FUNCTION__); 
static void f4(void)  printf("%s\n", __FUNCTION__); 
static void f5(void)  printf("%s\n", __FUNCTION__); 
static void f6(void)  printf("%s\n", __FUNCTION__); 
static void f7(void)  printf("%s\n", __FUNCTION__); 

int main(void)

    const func f[8] =  f0, f1, f2, f3, f4, f5, f6, f7 ;
    int i;

    for (i = 0; i < 8; ++i)
    
        f[i]();
    
    return 0;

【讨论】:

很酷,但这不会增加函数调用开销吗?但是,是的,您回答了这个问题,因为它没有分支。 (哎呀,现在有 4 个有效的 [答案],不知道 [其中一个] 到 [标记为已接受]) 编辑:我尝试强制内联但没有运气。另一种方法是 goto 并使用 gcc 的 &&label(我昨天尝试过但没有提及,因为我不想在代码中强制 GCC 依赖项。) (抱歉死帖...)只要您执行真正的函数调用,函数调用开销是给定的。如果您通过让 fN 返回 int 将其变成尾调用,则应该消除这种情况。【参考方案2】:

您是否尝试过将switch 变量声明为位域?

struct Container 
  uint16_t a:3;
  uint16_t unused:13;
;

struct Container cont;

cont.a = 5;  /* assign some value */
switch( cont.a ) 
...

希望这行得通!

【讨论】:

有趣。但没有运气,它仍然与 7 和“ja L5”相比。我正在使用 gcc 4.2 BTW(忘了提!)但是 gcc bugzilla 上的那个人报告说它发生在 gcc 4.5 中(尽管它可能不是 x86。)【参考方案3】:

我没有尝试,但我不确定gcc_unreachable 是否与__builtin_unreachable 做同样的事情。谷歌搜索这两者,gcc_unreachable 似乎被设计为用于开发 GCC 本身的断言工具,可能包含分支预测提示,而__builtin_unreachable 使程序立即未定义——这听起来像是删除了基本块,就是你想要的。

http://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-g_t_005f_005fbuiltin_005funreachable-3075

【讨论】:

是的,这里证明gcc_unreachable 是一个函数而不是无效代码的抽象表示:old.nabble.com/…【参考方案4】:

我尝试编译一些简单且可与 -O5 和 -fno-inline 相媲美的东西(我的 f0-f7 函数很简单),它生成了这个:


 8048420:   55                      push   %ebp ;; function preamble
 8048421:   89 e5                   mov    %esp,%ebp ;; Yeah, yeah, it's a function.
 8048423:   83 ec 04                sub    $0x4,%esp ;; do stuff with the stack
 8048426:   8b 45 08                mov    0x8(%ebp),%eax ;; x86 sucks, we get it
 8048429:   83 e0 07                and    $0x7,%eax ;; Do the (a & 0x7)
 804842c:   ff 24 85 a0 85 04 08    jmp    *0x80485a0(,%eax,4) ;; Jump table!
 8048433:   90                      nop
 8048434:   8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
 8048438:   8d 45 08                lea    0x8(%ebp),%eax
 804843b:   89 04 24                mov    %eax,(%esp)
 804843e:   e8 bd ff ff ff          call   8048400 
 8048443:   8b 45 08                mov    0x8(%ebp),%eax
 8048446:   c9                      leave  

您是否尝试过优化级别?

【讨论】:

是的,-O3 和 -O5 的代码完全相同,正如我在使用 gcc 4.2 的另一条评论中所述,但 bugzilla 条目说它也发生在 gcc 4.5 上。 嗯,我的是 gcc (Debian 4.4.4-6) 4.4.4。 没有 -O5 (gcc.gnu.org/onlinedocs/gcc-4.4.4/gcc/Optimize-Options.html),所以它与 -O3 的作用相同并不奇怪 ;-) 刚刚在 Gentoo 上用 4.5 试了一下,结果和你一样!啊。尽管如此,这是一个开源项目,所以不能要求用户拥有最新的 gcc 版本。现在我很难将答案分配为正确的人:-/【参考方案5】:

也许只是在第一个或最后一个案例中使用default 标签?

【讨论】:

我忘了说我也试过这个。这只是使条件跳转包括第一个或最后一个案例,仍然分支。似乎 gcc 无法判断该值在 0-7 范围内。 @aleccolocco:太糟糕了。你没有告诉你有什么版本的 gcc,也许你只是有一个坏的?我想您也尝试了所有优化标志? @aleccolocco:抱歉,我上次发表评论后才阅读编译器的描述。我在我的机器上进行了实验(64 位,gcc 4.4.3)我得到了一个完美的跳转表,唯一的警告是 main 缺少 return。我还将在我的手机上尝试使用 gcc 4.2 的 ARM 在带有 gcc 4.2 的 ARM 上,它也会生成一个跳转表,但无法优化 4.4.3 所做的边界检查。 andcmp 这两条指令确实是互相接替的,所以这不是很优化。顺便说一句,当我在switch 之后添加return 语句时,我观察到生成的代码完全不同。 i686 上的 gcc 4.4.3:跳转表,无边界检查。 i686 上的 gcc 4.0.1:跳转表,绑定检查。所以我的猜测是版本有所不同,但不幸的是我没有在 4.5 的机器上进行测试。【参考方案6】:

从错过的编译器优化的角度来看,这个问题当然很有趣,这对我们来说似乎很明显,我确实花了很多时间试图提出一个简单的解决方案,主要是出于个人的好奇。

也就是说,我不得不承认我非常怀疑这条额外的指令是否会在实践中产生可衡量的性能差异,尤其是在新的 Mac 上。如果您有大量数据,您将受到 I/O 限制,并且一条指令永远不会成为您的瓶颈。如果您的数据量很小,那么您需要重复执行 lot lot lot 的计算,然后一条指令就会成为瓶颈。

您会发布一些代码来表明确实存在性能差异吗?或者描述您使用的代码和数据?

【讨论】:

是的。如果分支不太可能,预测器将对其进行标记。但它会消耗2个周期。我正在研究一种高性能压缩算法和实现。压缩的问题之一是由于压缩的性质导致的不可预测性。解压缩的下一个块是文字还是匹配? (摘源)多长时间了?它是否跨越缓存行或页面边界? (严重的性能损失。)解压时将为每个“原子”处理此循环。性能很重要:Google 有他们的超级机密压缩算法 Zippy。 (数据库,通讯,一切) 最初发布时,我认为错误/问题在最新版本的 gcc 中已被破坏(因为 4.5 的 gcc bugzilla 条目已链接。)我的错。上面的@Jens Gustedt 证实了这一点。然后我在不同的操作系统上测试了 4.4/4.5。我将编辑问题以反映这一点。谢谢! (并对有效的批判性思维投赞成票!) 我同意性能很重要。我在问该 cmpl 指令是否具有可衡量的性能影响,如果是这样,在什么实际情况下(因为我想不出。)这是“我如何破解特定版本”的问题之间的区别gcc 只是为了好奇”和一个关于合法代码优化的问题。 是的。目前测试数据是***文本的随机块,仍然需要完成代码的其他部分。但这是最里面最重要的解码循环,对于仅 1MB 的典型明文,它将运行约 180k 次(来自测试)。

以上是关于GCC 4.4:避免在 gcc 中对 switch/case 语句进行范围检查?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Mac OS X 10.8 / Xcode 4.4 上使用/安装 gcc

我如何告诉 gcc 在 switch/case 语句上警告(或失败)而不中断?

在 switch 块中使用 QVariant::Type 的用户类型的 GCC 警告

使用AVR-GCC的switch语句的汇编代码

ARM GCC 中对 posix_memalign 的未定义引用

排查GCC 4.4.X版本优化switch-enum的BUG