我试图了解跳转表及其在switch case语句之间的关系。
我被告知跳转表是编译器生成的O(1)结构,它使得查找值基本上与您可以获得的速度一样快。但是在某些情况下,Hashtable / Dictionary可能会更快。我还被告知,只有在开关案例包含ordered
个数据值时才能使用。
有人可以确认或否认这一点并解释跳转表是什么,它的重要性和使用字典或散列表的时间复杂性。感谢。
答案 0 :(得分:20)
跳转表是用于转移控制到另一个位置的抽象结构。转到,继续和中断是相似的,除了它们总是转移到特定位置而不是许多可能性。特别是,该控制流程与函数调用不同。 (维基百科关于branch tables的文章是相关的。)
switch语句是如何在C / C ++中编写跳转表。在这种常见情况下,仅提供有限形式(只能打开整数类型)以使实现更容易和更快。 (对于整数类型而言,如何有效地实现跳转表的研究比一般情况要多得多。)一个典型的例子是Duff's Device。
但是,通常不需要跳转表的全部功能,例如每个case都有break语句。这些“有限的跳转表”是一种不同的模式,它只利用跳转表经过充分研究的效率,并且在每个“动作”独立于其他动作时都很常见。
跳转表的实际实现采用不同的形式,主要是如何完成索引映射的键。该映射是“字典”和“哈希表”之类的术语,而这些技术可以独立于跳转表使用。说一些代码“使用跳转表”并不意味着你有O(1)查找。
编译器可以自由选择每个switch语句的查找方法,并且无法保证您将获得一个特定的实现;但是,应该考虑编译器选项,例如优化速度和优化尺寸。
您应该研究数据结构,以了解它们所施加的不同复杂性要求。简而言之,如果“字典”是指平衡的二叉树,则它是O(log n);哈希表取决于其哈希函数和冲突策略。在switch语句的特定情况下,由于编译器具有完整信息,因此它可以生成perfect hash function,这意味着O(1)查找。但是,不要仅仅考虑整体算法的复杂性而迷失方向:它隐藏了重要的因素。
答案 1 :(得分:4)
跳转表基本上是指向代码段的指针数组,用于处理switch语句中的各种情况。当您的案例密集时(例如,您对某个范围内的每个可能值都有一个案例),最有可能生成它。例如,给出如下声明:
switch (i) {
case 1: printf("case 1"); break;
case 2: printf("case 2"); break;
case 3: printf("case 3"); break;
}
它可以生成大致相当于这样的代码:
void case1() { printf("case 1"); }
void case2() { printf("case 2"); }
void case3() { printf("case 3"); }
typedef void (*pfunc)(void);
pfunc functions[3] = {case1, case2, case3};
if ((unsigned)i<3)
functions[i]();
这具有O(K)复杂性。典型的哈希表也具有大致O(K)预期的复杂度,尽管最坏的情况通常是O(N)。跳转表通常会更快,但通常只有在表格非常密集的情况下才会使用,而哈希表/字典即使在案例非常稀疏的情况下也能正常工作。
答案 2 :(得分:3)
假设您有一系列程序:
void fa() {
printf("a\n");
}
...
void fz() {
printf("it's z!\n");
}
typedef void (*F)();
F table[26]={fa,fb,...,fz};
假设您接受来自用户的输入字符(来自a-z)并运行fc:
char c;
switch(c) {
case 'a': fa();break;
case 'b': fb();break;
...
case 'z': fz();break;
default: exit(-1);
}
理想情况下,这将被替换为:
if (c<'a' || c>'z') exit(-1);
else (*table[c-'a'])();
当然,您可能会使表格更大,因此无需进行范围检查。
编译器会为任意代码执行此操作,而不仅仅是函数调用,并且可以通过存储要跳转到的地址(实际上是goto)来执行此操作。 C不直接支持任何类型的计算goto(索引到表或其他方式),但它的CPU指令非常简单。
答案 3 :(得分:2)
根据具体情况,编译switch语句可以采用多种形式。如果情况紧密相连,那就不用考虑了:使用跳转表。如果案例相距很远,请使用if(case == value)或使用map。或者编译器可以使用组合:跳转表的岛,如果检查跳转表范围确定。
答案 4 :(得分:1)
跳转表是一个简单的函数指针数组,你可以大致像这样画出跳转表:
int (*functions[10])(); /* Array of 10 Function Pointers */
根据我的理解,这与case语句一样使用:每个condition,case _,将是这个数组的索引,例如:
switch( a ) {
case 1: // (*functions[1])() // Call function containing actions in case of 1
...
case 2: // (*functions[2])() // Call function containing actions in case of 2
...
每个案例,转变为简单的函数[a]。这意味着访问函数[9]与访问函数[1]一样快。给你你提到的O(1)时间。
显然,如果你有案例1和案例4907,这不是一个好方法,你提到的哈希表/字典方法可能会发挥作用。
答案 5 :(得分:0)
进一步阐述Jerry's answer和其他人
假设:
int x=1;
switch (i) {
case 1: x=6; break;
case 2: x++;
// Fall through
case 3: x+=7; break;
}
你可以拥有以下内容:
int f1() {return 6;}
int f2() {return 1+f3();}
int f3() {return 8;}
编译器可以使用跳转表来索引{f1, f2, f3}
编译器可以在创建具有f1, f2, f3
设置x
的表格直接到6,9,8
但是如果你编写了这些函数,并推出了自己的跳转表,f1,f2,f3
可以在任何地方,但是编译器会知道将它们放在switch
附近,从而创建比你更好的代码位置
请注意,在许多情况下,编译器会生成一个警卫来检查i
是否在范围内(或处理default
),如果您确定它始终是其中一种情况,你可以跳过那个
有趣的是,对于少数情况,并且在不同的编译器标志(依赖于编译器)下,switch
不会使用表,而只会执行ifs,类似于:
if (i==1) x=f1();
else if (i==2) x=f2();
else if (i==3) x=f3();
或者它可以优化它(简单测试是一条指令):
x=(i==1) ? f1()
: (i==2) ? f2()
: (i==3) ? f3()
: x;
最好的建议是查看生成的程序集,以查看编译器对您的体系结构中的代码所执行的操作,如果有跳转表,则Linux / intel上的g ++将生成如下所示的内容
(注意我必须转到5个case
语句来强制跳转表,它使用ifs低于case
个语句数)
请注意,跳转表中会有小孔来执行default
int foo(int i)
{
int x=1;
switch (i) {
case 1: x=6; break;
case 2: x++;
// Fall through
case 3: x+=7; break;
case 4: x+=2; break;
case 5: x+=9; break;
}
return x;
}
将生成以下汇编代码( // comments is mine ):
cmp edi, 5 //make sure it is not over 5
ja .L2 //jump to default case
mov edi, edi
jmp [QWORD PTR .L4[0+rdi*8]] // use the jump table at label L4:
.L4:
.quad .L2 // if i=0, set x=1 (default)
.quad .L9 // f1() see below
.quad .L10 // f2() see below
.quad .L6 // f3() see below
.quad .L7 // f4() see below
.quad .L8 // f5() see below
.L10:
mov eax, 9 // x=9
ret
.L9:
mov eax, 6 // x=6
ret
.L8:
mov eax, 10 // x=10
ret
.L6:
mov eax, 8 // x=8
ret
.L7:
mov eax, 3 // x=3
ret
.L2:
mov eax, 1 // default, x was 1, noop is: x=1
ret