快速排序的汇编级优化(windowds平台VS下dubug release模式对比 linux平台gcc的有无优化对比)

来源:互联网 发布:jenkins 构建php 编辑:程序博客网 时间:2024/06/07 06:17

bySA**406鲁可

前言

如果读者对基本的反汇编,压栈、出栈、参数传递不熟,由于此类文章网上汗牛充栋,本着不向互联网堆砌垃圾的态度(再让我写必定是垃圾,呵呵,因为注定没他们写得出彩),本篇博文不做介绍,可以参考

http://blog.csdn.net/zsy2020314/article/details/9429707

此篇博文只讨论快速排序汇编级的优化技术,对于代码级优化,诸如:单向扫描,双向扫描,选取中间值做划分,选取随机值做划分,不在本文讨论之列,可以参考下面两篇博文

http://wenku.baidu.com/view/4f1f6c360b4c2e3f57276304.html

http://blog.csdn.net/zuiaituantuan/article/details/5978009#t16,该篇博文最后已提到尾递归的代码级优化

程序选自编程珠玑源代码Column 11里最简单的快速排序

<span style="font-size:14px">/*Simplest version, Lomuto partitioning */void qsort1(int l, int u){           int i, m;    if (l >= u)        return;    m = l;    for (i = l+1; i <= u; i++)        if (x[i] < x[l])            swap(++m, i);    swap(l, m);    qsort1(l, m-1);    qsort1(m+1, u);}</span>

main函数中调用

qsort1(0,n-1);

VS下debug模式

把参数0 n-1压栈

debug模式下,qsort1调用过程:

debug模式下,先开辟存放局部变量区域:sub esp,0D8h,开辟了216B的栈空间,它还将寄存器ebxesiedi压栈,总空间216B+3*4B=228B

接下来  lea edi,[ebp-0D8h]

              mov ecx,36H36H十进制5454*4B=216BD8十进制216B

              mov eax,0CCCCCCCCh

              rep stos dword ptr es:[edi]

这四句汇编将ebp之下开辟的216B的栈空间初始化为CC字符,特殊调试字符,中断指令,防止栈被攻击,我们平时所见到的“烫烫”便由此而来。

其实,此举微软有吃力不讨好之嫌,因为黑客可以利用这点,发现自己是处于调试状态。

 

m存放在栈内局部变量区域

i也存放在栈内局部变量内存

以上调用qsort1(l,m-1)qsort1(m+1,u)都有将参数压栈过程,也发现swap开辟了新的栈帧,没有直接展开,都是正常的栈帧机制。

 

VS下release模式

release模式下调用qsort1之前的压栈:

两者都把参数0 n-1压栈,不同之处:debug用的是sub指令,release用的是寄存器自减指令dec,更快

没有开辟局部变量空间,edxlebxuecxieaxm

调用qsort1(l,m-1)有将参数压栈过程,release模式下主要省掉了局部变量的空间,用寄存器做了替代,但有将寄存器压栈的代价,编译器用了寄存器做暂存好处有二:一、速度快,二、省空间。

这里栈空间开销为两个参数8B+返回地址ret+压栈寄存器(旧的ebp ebx esiedi=28B,总的来说节省了不少空间。

      上面也可以看出调用swap没有开辟新的栈帧,内联直接展开。

      按如上分析,即使是release模式下调用一次qsort1最少也需要28B左右,但微软做了鬼斧神工的优化,release模式下调用后半段qsort1(m+1,u)时,根本没有压栈和call的过程,直接是设置新的参数,恢复堆栈平衡,提前作比较(指令预取技术),就跳到了 qsort1+10,微软这么一优化,实际上是沿用了父qsort1()的栈空间,减少了一半的栈空间消耗,并且还做到了调用qsort1(),第二个qsort1()实际上已经是尾递归了,这么分析理论上栈空间平均每次消耗就14B。考虑调用两个qsort的次数不同,实际栈消耗会在14B左右。

GCC下未优化版本

自己动手敲了个最简单的选取首元素作为基准的快速排序

#include<stdio.h>int Partition(int A[],int left,int right);int QuickSort(int A[],int left,int right);int test();int main(){    test();    return 0;}int test(){    int a5]={3,5,4,1,2};    int i;    QuickSort(a,0,4);    for(i =0;i<5;i++)        printf("%d ",a[i]);    printf(" ");}/*Partition步骤中哨兵选取的是第一个元素作为哨兵*/int Partition(int A[],int left,int q){    int pivot = A[left];    while(left<right)    {         while(left<right&&A[right]>=pivoit) –right;        A[left]=A[right];        while(left<right&&A[left]<=pivoit) ++left;        A[right]=A[left];    }    A[left]=pivot;    return left;}/*递归调用的QuickSort程序*/int QuickSort(int A[],int left,int right){    if(left<right)    {        int q = Partition(A,left,right);        QuickSort(A,left,q-1);        QuickSort(A,q+1,right);    }}

GCC下未优化的结果如下:

能看出明显的压栈、调用过程,并且调用QuickSort都是从函数头开始调用

GCC下优化版本

优化后的反汇编结果如下:

优化后反汇编已经看不到调用partition的过程,即使是调用两段QuickSort也不是从头调用,而是从中间某段就开始调用,但用gdb disas命令调试时结果如下:

 

发现优化后的目标代码反汇编出来的结果和无优化时基本一致,但这段优化过的反汇编代码我暂时还未能读懂,如果读者能看懂,望不吝赐教,如果不方便评论,请发邮件look390125133@126.com,我也只是在VS下偶然一次快排溢出时研究了下,经过计算每次快排居然只有14B的开销,所以才有此心得,入门都算不上。

后记

看优化后的反汇编功力,非一朝一夕能成,需长年不断的积累,而在这方面网上能给出具体代码分析的资料似乎不多,因为大部分开发者并没有关心编译器背后做的事。

参考

GCC编译优化指南

http://lamp.linux.gov.cn/Linux/optimize_guide.html

此篇博文有GCC编译优化技术的一些介绍,但没有具体代码可分析,倘若要化为自己的功力,尚需时日,并且优化应当适可而止为好,将精力留出来做一些其它事情会更有意义。

原创粉丝点击