在C++中使用跳转表实现切换语句
我正在努力提高解析器的速度。和开关箱,有时它是有用的,但我看到它仍然很慢。太好了!如果C++支持这个带有额外参数的特征地址检查点,那就太棒了!p> 简单的例子:在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
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++支持这个特性吗?它能极大地提高速度和性能吗?你可以用函数指针构建一个数组,并通过枚举对其进行索引你可以用函数指针构建一个数组,并通过枚举对其进行索引是的,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?我希望看到的是单人床,而不是双人床。