switch和跳跃表

对于swith大家都用过都知道在大于3的情况下switch的效率比较高,但是大家有没有想到过为什么。如果单纯的认为每次比较case的值想不想等在进行,那10万个case如果比较10万次那是不可能来看几个汇编代码《深入理解计算机》上提到了这个,所以我也花事件整理了一下,

/* -------------------- filename : ta.c --------------- */
int switch_test_first( int x )
{
        int res ;
        switch( x ){
                case 100 :
                        res = 1 ;
                        break ;
                case 102 :
                        res = 2 ;
                        break ;
                case 103 :
                        res = 3 ;
                        break ;
        }
        return res ;
}
然后,我们用 gcc 将它编译成汇编文件( 使用 -S 开关 )
gcc -S ta.c 
将得到如下的汇编文件( ta.s )
        .file   "ta.c"
        .text
.globl switch_test_first
        .type   switch_test_first,@function
switch_test_first:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        .file   "ta.c"
        .text
.globl switch_test_first
        .type   switch_test_first,@function
switch_test_first:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        movl    %eax, -8(%ebp)
        cmpl    $102, -8(%ebp)          // 1
        je      .L4                     // 2
        cmpl    $102, -8(%ebp)          // 3    
        jg      .L8                     // 4
        cmpl    $100, -8(%ebp)          // 5
        je      .L3                     // 6
        jmp     .L2                     // 7
.L8:
        cmpl    $103, -8(%ebp)
        je      .L5
        jmp     .L2
.L3:
        movl    $1, -4(%ebp)
        jmp     .L2
.L4:
        movl    $2, -4(%ebp)
        jmp     .L2
.L5:
        movl    $3, -4(%ebp)
.L2:
        movl    -4(%ebp), %eax
        leave
        ret
.Lfe1:

上面的汇编代码很明显case比较了3次类似 但是当case比较多的时候又是另一种情况

        .text
.globl switch_test_first
        .type   switch_test_first,@function
switch_test_first:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        subl    $100, %eax
        movl    %eax, -8(%ebp)
        cmpl    $6, -8(%ebp)
        ja      .L2
        movl    -8(%ebp), %edx
        movl    .L9(,%edx,4), %eax
        jmp     *%eax
        .section        .rodata
        .align 4
        .align 4
.L9:                             // A
        .long   .L3
        .long   .L2
        .long   .L4
        .long   .L5
        .long   .L6
        .long   .L7
        .long   .L8
        .text
.L3:                            // 1
        movl    $1, -4(%ebp)
        jmp     .L2
.L4:                            // 2 
        movl    $2, -4(%ebp)
        jmp     .L2
.L5:                            // 3
        movl    $3, -4(%ebp)
        jmp     .L2             // 4   
.L6:
        movl    $4, -4(%ebp)
        jmp     .L2             // 5
.L7:
        movl    $5, -4(%ebp)    // 6
        jmp     .L2
.L8:                            // 7
        movl    $6, -4(%ebp)
.L2:                            
        movl    -4(%ebp), %eax
        leave
        ret
.Lfe1:
        .size   switch_test_first,.Lfe1-switch_test_first
        .ident  "GCC: (GNU) 3.2.2 20030222 (Red Hat Linux 3.2.2-5)"

仔细比较一下这个最新的 new_ta.s 与前面的 ta.s,精华全在里面了!
首先 new_ta.s 比前面的 ta.s 多了一个 .L9 部分,而且它的 // 1 ~ // 7 中没有了前面
ta.s 文件中所存在的众多的 cmpl 与 jmp 语句,那么,现在这样的代码又是怎么实现 
switch 语句中的跳转的呢?我们来仔细分析一下它新多出来的 .L9 部份。
        .section        .rodata
        .align 4
        .align 4
.L9:
        .long   .L3
        .long   .L2
        .long   .L4
        .long   .L5
        .long   .L6
        .long   .L7
        .long   .L8
        .text
显而易见,.L9 部份是一个我们最常见的数据结构——表,它的每一项都是一个标号,而这个标号,恰恰是每个 case 语句的入口标号!
这很容易让我们想到,它很可能是用了一张表来存放所有的 case 语句的入口,然后,在
执行 switch 语句的时候就从这个表中直接检出相应的 case 语句的入口地址,然后跳转
到相应的 case 语句去执行,就像hash_table似的。具体是不是这样呢?我们看看进入
switch 部份的代码:

        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        movl    8(%ebp), %eax
        subl    $100, %eax
        movl    %eax, -8(%ebp)
        cmpl    $6, -8(%ebp)
        ja      .L2
        movl    -8(%ebp), %edx
        movl    .L9(,%edx,4), %eax // 1
        jmp     *%eax              // 2
果然如此!首先在 // 1 处根据%edp的值(其值相当于表的下标)在.L9的表中找到相应
case 语句的入口地址,并把这个地址存到%eax中,然后通过 // 2 (这是一个间接跳转
语句)转到%eax存放的地址中,也即相应的case语句处。
C编译器,果然聪明!

通过这个分析我们可以知道如下两点:
1. 当 case 语句少的时候,C编译器将其转成 if..else.. 类型进行处理,运用较多的
   cmp 与 jmp 语句 ,而当 case 语句较多的时候,C编译器会出成一个跳转表,而直
   接通过跳转表进行跳转,这让 switch 具有非常高的效律
2. 可以问答下面几个问题:
   1. 为什么 case 语句中需要的是整数类型而不能是其余的类型?
      这是因为,case 语句中的这个值是用来做跳转表的下标的,因此,当然必须是整数
   2. 为什么 case 语句在不加break的时候具有直通性?
      这是因为跳转是在进入 switch 是计算出的,而不是在case语句中计算出的,整个
      case 语句群就是一块完整而连续的代码,只是switch让其从不同的位置开始执行。

其实,上面基本上就是跳跃表的思想,每一个结点不单单只包含指向下一个结点的指针,可能包含很多个指向后续结点的指针,这样就可以跳过一些不必要的结点,从而加快查找、删除等操作。对于一个链表内每一个结点包含多少个指向后续元素的指针,这个过程是通过一个随机函数生成器得到,这样子就构成了一个跳跃表。这就是为什么论文“Skip Lists : A Probabilistic Alternative to Balanced Trees ”中有“概率”的原因了,就是通过随机生成一个结点中指向后续结点的指针数目。随机生成的跳跃表可能如下图3所示:

首先,应该要了解跳跃表的性质;

  1. 由很多层结构组成;
  2. 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
  3. 最底层的链表包含了所有的元素;
  4. 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
  5. 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;

可以看到,这里一共有4层,最上面就是最高层(Level 3),最下面的层就是最底层(Level 0),然后每一列中的链表节点中的值都是相同的,用指针来连接着。跳跃表的层数跟结构中最高节点的高度相同。理想情况下,跳跃表结构中第一层中存在所有的节点,第二层只有一半的节点,而且是均匀间隔,第三层则存在1/4的节点,并且是均匀间隔的,以此类推,这样理想的层数就是logN。

搜索

其基本原理就是从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。

//
// Created by wangyi on 18-1-27.
//

#ifndef UNTITLED1_SKIPLIST_H
#define UNTITLED1_SKIPLIST_H

#include <cstddef>
#include <cstdlib>
#include <cstdio>
const int Max_LEVEl=16;
typedef int KeyType;
typedef int ValueType;
class SkipList
{
public:
    struct Node;
    typedef Node* pNode;
    struct Node
    {
        KeyType key;
        ValueType value;
        Node **forword;
    };
    SkipList();
    Node* CreateNode(int level);
    bool Insert(const KeyType &key,const ValueType &value);
    bool Search(const KeyType &key,ValueType &value);
    bool Delete(const KeyType &key,const ValueType &value);
    static int RandmLevel();
private:

    pNode listhead;
    int level;
    int msize;
    pNode NIL_;
};
SkipList::SkipList()//初始化链表每个foword都指向最后一个
{
    NIL_=CreateNode(1);
    srand(time(NULL));
    level=0;
    NIL_->key=0x7fffffff;
    listhead=CreateNode(Max_LEVEl);
    for(int i=0;i<Max_LEVEl;++i)
    {
        listhead->forword[i]=NIL_;
    }
    msize=0;
}
SkipList::Node* SkipList::CreateNode(int level)
{
    pNode tmp=(pNode)malloc(sizeof(Node));
    tmp->forword=(pNode*)malloc(sizeof(pNode)*level);//
    if(tmp->forword==NULL)
    {
        exit(0);//分配失败
    }
    return tmp;
}
int SkipList::RandmLevel()
{
    return random()%17;
}
bool SkipList::Delete(const KeyType &key, const ValueType &value)
{
    pNode x=listhead;
    pNode pos=NULL;
    for(int i=level;i>=0;--i)
    {	
	printf("%d\n",x->forword[i]->key);    
        while(x->forword[i]->key<key)
        {
            x=x->forword[i];
        }
        if(x->forword[i]->key==key)
        {
            pos=x->forword[i];
            x->forword[i]=x->forword[i]->forword[i];
        }
    }
    if(pos==NULL)
    {
        return false;
    }
    delete pos;
    return true;

}
bool SkipList::Search(const KeyType &key,ValueType &value)
{
    pNode x=listhead;
    for(int i=level;i>=0;--i)
    {
        while(x->forword[i]->key<key)
        {
            x=x->forword[i];
        }
        if(x->forword[i]->key==key)
        {
            value=x->forword[i]->value;
            return true;
        }
    }
    return false;
}
bool SkipList::Insert(const int &key,const int &value)//记录每次的下转点方便后面使用这个东西如果随机的层数可能会大于这个层所以需要保存一下
{
    pNode x=listhead;
    pNode arr[Max_LEVEl];
    for(int i=level;i>=0;i--) //保存向下转时的指针地址一般都是key小于下一个key大于保存小于的地址
    {
	    printf("%d\n",level);
        while (x->forword[i]->key < key)
        {
            x = x->forword[i];
        }

        arr[i] = x;
    }//
    //最后一层
    if(x->forword[0]->key==key)
    {
        x->forword[0]->value=value;//更新
	return false;
    }
    else//插叙
    {
        int newlevel=RandmLevel();//获取随机层区更新
	printf("%d\n",newlevel);
        if(newlevel>level)
        {
            newlevel = ++level;
            arr[level]=listhead;
        }
        pNode newNode=CreateNode(newlevel);
        newNode->key=key;
  else//插叙
    {
        int newlevel=RandmLevel();//获取随机层区更新
        if(newlevel>level)
        {
            newlevel = ++level;
            arr[level]=listhead;
        }
        pNode newNode=CreateNode(newlevel);
        newNode->key=key;
        newNode->value=value;
        for(int i=newlevel;i>=0;--i)
        {
            newNode->forword[i]=arr[i]->forword[i];
            arr[i]->forword[i]=newNode;
        }
        ++msize;//所记录的节点数字加1;
    }
}}

参考这片文章http://blog.csdn.net/ict2014/article/details/17394259对代码进行讲解代码代码有错需要改

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值