跳表切换案例问题

时间:2009-12-03 04:33:01

标签: c++ c hashtable switch-statement

我试图了解跳转表及其在switch case语句之间的关系。

我被告知跳转表是编译器生成的O(1)结构,它使得查找值基本上与您可以获得的速度一样快。但是在某些情况下,Hashtable / Dictionary可能会更快。我还被告知,只有在开关案例包含ordered个数据值时才能使用。

有人可以确认或否认这一点并解释跳转表是什么,它的重要性和使用字典或散列表的时间复杂性。感谢。

6 个答案:

答案 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