如何高效的使用switch(2015/1/30)

在TWAG性能测试中,使用perf命令发现ui_generic_encapsulate  函数占用的CPU高达8.3%。这个函数实现非常简单,只是包含了一个比较大的switch语句。本文从研究switch的汇编代码出发,提出了两个降低ui_generic_encapsulate  CPU占用率的建议。

1 switch的实现

通过分析switch.c的汇编代码发现,首先找出case 的最大值,如果x比最大值要大,则直接跳转到default。否则,生成一个数组,这个数组存储了每个case对应的跳转地址,数组索引是case的值。目的跳转地址存储在(数组地址 + 4 * x ),所以jmp  (数组地址 + 4 * x )直接跳转到了目的case。

代码1:switch.c

int func(int x)
{
    int y = 9;
    switch(x)
    {
    case 3:
        y+=3;
        break;
    case 5:
        y+=5;
        break;
    case 4:
        y+=4;
        break;
    case 2:
        y+=2;
        break;
    case 1:
        y+=1;
        break;
    default:
        y+=10;
    }
    return y;
}

代码2:switch.c对应的汇编代码,编译命令是gcc -s -m32 switch.c

        pushl   %ebp
        movl    %esp, %ebp
        subl    $16, %esp
        movl    $9, -4(%ebp)    #y初始化
#8(%ebp)表示x,如果x比最大值(5)还大,则直接跳转到default即.L2
        cmpl    $5, 8(%ebp)
        ja      .L2
#计算跳转地址在数组中的偏移,并赋值给寄存器eax
        movl    8(%ebp), %eax
        sall    $2, %eax
#数组地址(.L8)加上偏移(eax)存储目的跳转地址,并把这个地址赋值给eax
        movl    .L8(%eax), %eax
#跳转到指定的地址
        jmp     *%eax
        .section        .rodata
        .align 4
        .align 4
#生成一个整数数组,存储了所有可能跳转的地址,索引是case值
.L8:
        .long   .L2
        .long   .L3
        .long   .L4
        .long   .L5
        .long   .L6
        .long   .L7
        .text
.L5: #case 3
        addl    $3, -4(%ebp)
        jmp     .L9
.L7: #case 5
        addl    $5, -4(%ebp)
        jmp     .L9
.L6: #case 4
        addl    $4, -4(%ebp)
        jmp     .L9
.L4: #case 2
        addl    $2, -4(%ebp)
        jmp     .L9
.L3: #case 1
        addl    $1, -4(%ebp)
        jmp     .L9
.L2: #case default
        addl    $10, -4(%ebp)
.L9:
        movl    -4(%ebp), %eax  #return y
        leave
        ret
        .size   func, .-func

2 优化

众所周知,CPU会把待执行的指令预取到缓存。CPU对分支进行预测,如果预测失败,并且跳转的分支没有进入预取缓存,CPU需要清空预取缓存,然后从内存读取指令到缓存。由于CPU读取内存的开销非常大,所以分支预测失败对CPU杀伤力非常大。

在编写性能要求非常高的代码时,要尽量避免使用分支。If/switch/for/while都包含了分支。如果实在需要,合理的安排分支的顺序。另外还可以借助likely/unlikely提示CPU如何进行分支预测。

有两个技巧可以提高switch语句的性能。

1)  合理安排分支的顺序

原则是分支可能性越大,其位置越靠后。这样带来的好处是跳转到指定case后,减少了再次跳转后指令不在预取缓存中的可能性。

2)  按照一定顺序定义case的值

原则是分支的可能性越大,其case值越小。这样可以降低访问数组(用来存储跳转地址)时发生缓存不命中的概率。


所有按照上面的原则,可以这样优化函数ui_generic_encapsulate。

1) 调整分支顺序

ui_generic_encapsulate
{
    …
case UIP_TUNNEL_GRE:
case UIP_TUNNEL_EOIP:
case UIP_TUNNEL_GTP:
case 	case UIP_TUNNEL_ESP_OUT:
case UIP_TUNNEL_MAC:
default:
}

2) 重新定义case值

typedef enum
{
	UIP_TUNNEL_MAC,
	UIP_TUNNEL_ESP_OUT,
	UIP_TUNNEL_ESP_IN,
	UIP_TUNNEL_GTP,
	UIP_TUNNEL_GRE,
	UIP_TUNNEL_EOIP,	
	UIP_TUNNEL_IPinIP,
	UIP_TUNNEL_L2TP,
	UIP_TUNNEL_AH,
	UIP_TUNNEL_PPPOE,
	UIP_TUNNEL_MAC,
	UIP_TUNNEL_NON,
} UIP_tunnel_type_t;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章