在C++中使用跳转表实现切换语句

在C++中使用跳转表实现切换语句,c++,performance,switch-statement,memory-address,C++,Performance,Switch Statement,Memory Address,我正在努力提高解析器的速度。和开关箱,有时它是有用的,但我看到它仍然很慢。太好了!如果C++支持这个带有额外参数的特征地址检查点,那就太棒了!p> 简单的例子: enum Transport //MOTORBIKE = 1, CAR = 2, ... SHIP = 10 Transport foo = //unknown switch(foo) { case MOTORBIKE : /*do something*/ break; case CAR : /*do so

我正在努力提高解析器的速度。和开关箱,有时它是有用的,但我看到它仍然很慢。太好了!如果C++支持这个带有额外参数的特征地址检查点,那就太棒了!p> 简单的例子:

enum Transport //MOTORBIKE = 1, CAR = 2,  ...  SHIP = 10
Transport foo = //unknown

    switch(foo)
{
    case MOTORBIKE : /*do something*/ break;
    case CAR : /*do something*/ break;
    //////////////////////////
    case SHIP : /*do something*/ break;
}
如果变量foo是SHIP,则程序至少必须重新检查该值达十次!->还是很慢

如果C++支持检查点:

它不生成任何跳转表,然后按值检查。也许它不适合默认情况。唯一的事情是StaskySwitk+CARE-StRiguSwite+Sead可能有不同的地址,因此如果C++将它们作为真实地址来评估,那么进程将失败。因此,在编译时,编译器只需将它们转换为实际地址


< C++支持这个特性吗?它能极大地提高速度和性能吗?

你可以用函数指针构建一个数组,并通过枚举对其进行索引

你可以用函数指针构建一个数组,并通过枚举对其进行索引

是的,C/C++确实支持此功能,而且它在。。。标准开关。我不知道你从哪里知道开关会检查每个值,但你错了。是的,我听说一些编译器可以为相当大的情况生成更好的代码许多变体,比如可能几百个,但我不认为它是你的。 例如,以下代码由gcc编译,无需任何优化:

enum E { One, Two, Three, Four, Five };

void func( E e )
{
    int res;
    switch( e ) {
        case One : res = 10; break;
        case Two : res = 20; break;
        case Three : res = 30; break;
        case Four : res = 40; break;
        case Five : res = 50; break;
    }
}
生成以下内容:

_Z4func1E:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -20(%rbp)
    movl    -20(%rbp), %eax
    cmpl    $4, %eax
    ja  .L1
    movl    %eax, %eax
    movq    .L8(,%rax,8), %rax
    jmp *%rax
    .section    .rodata
    .align 8
    .align 4
.L8:
    .quad   .L3
    .quad   .L4
    .quad   .L5
    .quad   .L6
    .quad   .L7
    .text
.L3:
    movl    $10, -4(%rbp)
    jmp .L1
.L4:
    movl    $20, -4(%rbp)
    jmp .L1
.L5:
    movl    $30, -4(%rbp)
    jmp .L1
.L6:
    movl    $40, -4(%rbp)
    jmp .L1
.L7:
    movl    $50, -4(%rbp)
    nop
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:

正如您所看到的,它只是跳转到特定的位置,而不检查每个值。

是的,C/C++确实支持此功能,并且它在。。。标准开关。我不知道你从哪里知道开关会检查每个值,但你错了。是的,我听说一些编译器可以为相当大的情况生成更好的代码许多变体,比如可能几百个,但我不认为它是你的。 例如,以下代码由gcc编译,无需任何优化:

enum E { One, Two, Three, Four, Five };

void func( E e )
{
    int res;
    switch( e ) {
        case One : res = 10; break;
        case Two : res = 20; break;
        case Three : res = 30; break;
        case Four : res = 40; break;
        case Five : res = 50; break;
    }
}
生成以下内容:

_Z4func1E:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -20(%rbp)
    movl    -20(%rbp), %eax
    cmpl    $4, %eax
    ja  .L1
    movl    %eax, %eax
    movq    .L8(,%rax,8), %rax
    jmp *%rax
    .section    .rodata
    .align 8
    .align 4
.L8:
    .quad   .L3
    .quad   .L4
    .quad   .L5
    .quad   .L6
    .quad   .L7
    .text
.L3:
    movl    $10, -4(%rbp)
    jmp .L1
.L4:
    movl    $20, -4(%rbp)
    jmp .L1
.L5:
    movl    $30, -4(%rbp)
    jmp .L1
.L6:
    movl    $40, -4(%rbp)
    jmp .L1
.L7:
    movl    $50, -4(%rbp)
    nop
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:

正如您所看到的,它只是跳转到特定的位置,而不检查每个值。

您所说的就是跳转表。跳转表通常是一个相对地址数组,可以在其中传输程序执行控制。下面是一个如何实现的示例:

#include <ctime>
#include <cstdlib>
#include <cstdio>

int main()
{
    static constexpr void* jump_table[] =
    {
        &&print_0, &&print_1, &&print_2,
        &&print_3, &&print_4, &&print_5
    };

    std::srand(std::time(nullptr));
    int v = std::rand();

    if (v < 0 || v > 5)
        goto out;

    goto *jump_table[v];

print_0:
    std::printf("zero\n");
    goto out;
print_1:
    std::printf("one\n");
    goto out;
print_2:
    std::printf("two\n");
    goto out;
print_3:
    std::printf("three\n");
    goto out;
print_4:
    std::printf("four\n");
    goto out;
print_5:
    std::printf("five\n");
    goto out;
out:
    return EXIT_SUCCESS;
}
然而,我严重怀疑两件事。第一个疑问是使用跳转表会使程序更快。间接跳转的成本相对较高,而且硬件的预测能力较差。如果您只有三个值,那么最好使用if-then-else语句简单地比较它们。对于许多稀疏值,例如1100、250、500等,最好进行二进制搜索,而不是放大表的大小。无论哪种情况,在转换语句时,这都只是冰山一角。所以,除非您知道所有的细节,并且知道编译器在哪里对您的特定情况做了错误的事情,否则甚至不要费心尝试将开关更改为其他东西-您永远不会比编译器聪明,只会使您的程序变慢

第二个疑问是切换实际上是解析器的瓶颈。很可能不是。因此,为了节省大量宝贵的时间,请首先尝试分析代码,以确定程序最慢的部分是什么。通常情况下,其步骤如下:

分析并找到瓶颈。 找出这是一个瓶颈的原因,并提出如何提高代码速度的合理想法。 尝试改进代码。 转到步骤1。 这个循环没有出口。优化是你可以用一生去做的事情。在某些情况下,您必须假设程序足够快,并且没有瓶颈:


此外,我还写了一篇更全面的分析,详细介绍了编译器是如何实现switch语句的,以及何时、何时不尝试超越switch语句。请找到这篇文章。

您所说的是跳转表。跳转表通常是一个相对地址数组,可以在其中传输程序执行控制。下面是一个如何实现的示例:

#include <ctime>
#include <cstdlib>
#include <cstdio>

int main()
{
    static constexpr void* jump_table[] =
    {
        &&print_0, &&print_1, &&print_2,
        &&print_3, &&print_4, &&print_5
    };

    std::srand(std::time(nullptr));
    int v = std::rand();

    if (v < 0 || v > 5)
        goto out;

    goto *jump_table[v];

print_0:
    std::printf("zero\n");
    goto out;
print_1:
    std::printf("one\n");
    goto out;
print_2:
    std::printf("two\n");
    goto out;
print_3:
    std::printf("three\n");
    goto out;
print_4:
    std::printf("four\n");
    goto out;
print_5:
    std::printf("five\n");
    goto out;
out:
    return EXIT_SUCCESS;
}
然而,我严重怀疑两件事。第一个疑问是使用跳转表会使程序更快。间接跳转的成本相对较高,而且硬件的预测能力较差。如果您只有三个值,那么最好使用if-then-else语句简单地比较它们。对于许多稀疏值,例如1100、250、500等,最好进行二进制搜索,而不是放大表的大小。无论哪种情况,在转换语句时,这都只是冰山一角。所以,除非您知道所有的细节,并且知道编译器在哪里为您的特定情况做了错误的事情,否则甚至不要费心尝试将开关更改为其他东西——您将永远不会成功 utsmart会启动编译器,只会使程序变慢

第二个疑问是切换实际上是解析器的瓶颈。很可能不是。因此,为了节省大量宝贵的时间,请首先尝试分析代码,以确定程序最慢的部分是什么。通常情况下,其步骤如下:

分析并找到瓶颈。 找出这是一个瓶颈的原因,并提出如何提高代码速度的合理想法。 尝试改进代码。 转到步骤1。 这个循环没有出口。优化是你可以用一生去做的事情。在某些情况下,您必须假设程序足够快,并且没有瓶颈:


此外,我还写了一篇更全面的分析,详细介绍了编译器是如何实现switch语句的,以及何时、何时不尝试超越switch语句。请找到这篇文章。

我非常怀疑是开关让你慢了下来。但您当然可以尝试用数组、映射或无序的_映射替换它,将值转换为函数指针。@Jon这肯定比switch慢。很难打败编译器生成的开关的速度。@斯拉瓦:编译器编写代码,和你一样。实际上,switch语句相当于magicpixie dust,与map或unordered_map等完全动态的容器相比。编译器可以对输入数据做出许多假设,可以内联函数调用,并且可以跨开关的各个条目折叠代码的冗余部分。如果使用函数指针的容器,这些都不会发生。我非常怀疑是开关让你慢下来了。但您当然可以尝试用数组、映射或无序的_映射替换它,将值转换为函数指针。@Jon这肯定比switch慢。很难打败编译器生成的开关的速度。@斯拉瓦:编译器编写代码,和你一样。实际上,switch语句相当于magicpixie dust,与map或unordered_map等完全动态的容器相比。编译器可以对输入数据做出许多假设,可以内联函数调用,并且可以跨开关的各个条目折叠代码的冗余部分。如果它使用函数指针的容器,就不会发生这些情况。-1:你从哪里知道switch不会检查每个值?当然不是从标准中。因此,您在编译某些特定代码时,查看了某些特定编译器使用某些特定设置生成的程序集,这就是它的工作原理?有足够的信息值得商榷。如果你不了解某件事,最好是学习,而不是留下毫无意义的评论。”VladLazarenko:问题是Slava过于泛化和宣传标准行为,因为ISO C++标准行为C/C++支持这个事实-实际上,编译器没有。此外,答案并没有给OP提供任何东西,与你的相比,我投了更高的票。最后,他对我所知道或理解的东西做了更多的假设,宣称这些评论毫无意义等等,这对我没有帮助。如果你需要一个开关,你应该写一个开关。如果编译器以一种执行性能良好的方式实现转换的方式不好,则应该考虑不同的编译器。这个标准给了编译器足够的自由去做这件事,他们也应该这样做。-1:你从哪里知道switch不会检查每个值?当然不是从标准中。因此,您在编译某些特定代码时,查看了某些特定编译器使用某些特定设置生成的程序集,这就是它的工作原理?有足够的信息值得商榷。如果你不了解某件事,最好是学习,而不是留下毫无意义的评论。”VladLazarenko:问题是Slava过于泛化和宣传标准行为,因为ISO C++标准行为C/C++支持这个事实-实际上,编译器没有。此外,答案并没有给OP提供任何东西,与你的相比,我投了更高的票。最后,他对我所知道或理解的东西做了更多的假设,宣称这些评论毫无意义等等,这对我没有帮助。如果你需要一个开关,你应该写一个开关。如果编译器以一种执行性能良好的方式实现转换的方式不好,则应该考虑不同的编译器。该标准为编译器提供了正确执行此操作的自由度,应该期望他们这样做。调用函数的速度很慢,尤其是代码较少的函数。我必须多次检查值。编辑:而且,我必须处理我的许多局部变量…@xersi你真的分析过这些吗?是的,calli
ng函数有一些开销,但您的函数几乎不需要做任何事情,这才是真正重要的。@us2012您应该在这样的注释之前尝试分析。是,functon比switch慢。若您关心编译器生成代码的速度,那个么调用函数确实很重要,而内联函数则起作用purpose@Slava取决于很多因素,比如对象的比较运算符的速度,如果该运算符速度较慢,在映射中查找函数指针要比尝试平均打开开关情况便宜得多。当然,函数调用可能会有开销,但我想说的是:在分析正在处理的代码以证明它之前,不要假设它们是问题所在。对象的比较运算符?您真的了解开关在C/C++中的工作原理吗?调用函数很慢,尤其是代码较少的函数。我必须多次检查值。编辑:而且,我必须处理我的许多局部变量…@xersi你真的分析过这些吗?是的,调用函数会有一些开销,但您的函数几乎不需要做任何事情,这才是真正重要的。@us2012您应该在这样的注释之前尝试分析。是,functon比switch慢。若您关心编译器生成代码的速度,那个么调用函数确实很重要,而内联函数则起作用purpose@Slava取决于很多因素,比如对象的比较运算符的速度,如果该运算符速度较慢,在映射中查找函数指针要比尝试平均打开开关情况便宜得多。当然,函数调用可能会有开销,但我想说的是:在分析正在处理的代码以证明它之前,不要假设它们是问题所在。对象的比较运算符?你真的了解switch在C/C++中是如何工作的吗?@Arnaud:谢谢你:我想v=rand一定是一个v=rand%6;否则程序不会打印任何东西。请原谅我的无知。。。什么是&&in&&print\u 0?我希望看到的是单&,而不是双。@Arnaud:谢谢:我想v=rand一定是v=rand%6;否则程序不会打印任何东西。请原谅我的无知。。。什么是&&in&&print\u 0?我希望看到的是单人床,而不是双人床。