我真的很惊讶我找不到这个问题已经问过了。我想知道 switch 语句占用多少代码空间,以及使用 const 查找表是否会更有效地满足我的需求。
typedef struct container{
type1 a;
type2 b;
type3 c;
}container;
static container d;
//option A
void foo(int num)
{
void* x;
switch (num)
{
case 1:
x = &d->a;
break;
case 2:
x = &d->b;
break;
case 3:
x = &d->c;
break;
default:
x = NULL;
break;
}
// do something with x
}
// option B
const void* lookup_table[] = {
d.a,
d.b,
d.c,
NULL
};
void foo(int num)
{
void* x = lookup_table[num];
// do something with x
}
switch 语句将如何分解为汇编,它在代码空间中会大多少?是否值得使用查找表而不是使用 switch 语句?
如果您可以将开关重写为查找表的简单查找,这可能是最好的解决方案,尤其是在可能的索引密集的情况下,因为它也可能更具可读性。(如果可能的索引不密集,您可以浪费空间或使用更复杂的查找技术:两级表、哈希表、对排序列表的二叉搜索。这些可能比 switch 语句更好,但可读性较差。但是,一个好的编译器会努力提高效率,其中一些会生成与您完全相同的代码。
但在通常情况下,您不仅需要查找值,switch 语句几乎可以肯定更好。一个好的编译器会将 switch 语句编译成上述策略之一,并且根据目标平台的详细信息,它可能比您更了解最佳解决方案。
特别是,由于调用函数的开销,将 switch 语句转换为函数指针的索引查找,然后通过函数指针调用可能比 switch 语句慢得多。使用 switch 语句,编译器可能会生成一个分支表,其中的查找代码将与您手工构建的代码非常相似,但在查找之后执行的是简单的分支而不是函数调用。
这个问题没有确切的含义。优化编译器(通常(至少一次编译整个函数(通常,整个翻译单元(。
阅读 R.Sayle 关于编译开关的论文。您将了解到有几种竞争策略(跳跃表,平衡树,条件移动,哈希跳跃表等(,并且其中几种可以组合在一起。
相信您的优化编译器会做出足够好的选择来编译您的开关代码。对于 GCC,如果您想查看生成的汇编程序内部,请使用gcc -Wall -O2 -march=native
进行编译,也许可以添加-fverbose-asm -S
(和/或用 -O3
替换-O2
(。还了解gcc -flto -O3
等...
当然,出于基准测试和生产代码的目的,您应该始终要求编译器进行优化。
请注意,作为一个扩展(也被Clang/LLVM接受...(GCC 将标签作为值(带有间接goto
s(。有了它们,您可以强制使用跳转表,或者使用一些线程代码。这并不总是使您的代码更快(例如,因为分支预测(。
看待帖子的不同方式:
void foo(int num) { void* x; switch (num)...
很好地应对了范围外1,2,3
num
。
当num
超出0,1,2,3
范围时,void foo(int num) { void* x = lookup_table[num];
具有未定义的行为。
有些人可能会说num
范围不是问题。 但这在帖子中没有说明。 代码维护也是如此 - 许多未陈述的,暗示的,有时是错误的假设条件。
是否值得使用查找表而不是使用 switch 语句?
为了维护的价值,我会选择switch()
.
正如其他人已经说过的那样,现代优化编译器将尝试自己选择一个好的策略来将开关编译成更有效的代码。Hans Wennborg在2015年LLVM开发者大会上发表了关于最近开关降低改进的演讲,简要介绍了这个主题。
因此,最好让编译器完成其工作,并决定最易读的解决方案,而不是您认为最有效的解决方案。
如果要查看 Clang 为交换机文件生成的代码,可以使用 -S
或 -S -emit-llvm
。