• C++的高效从何而来


    前一段时间,实验室的一哥们突然跑过来跟我说,“我自己写了个C的快速排序,排了一个10000000个int的数组,貌似比C库中是qsort算法要快,咋回事?C++的STL中快排(quick sort)算法的效率如何?”。

    听他这么一说,我就立即做了个实验,写了如下代码:

    #include <iostream>
    #include <algorithm>
    #include <time.h>
    
    using namespace std;
    
    #define MAX_LEN 10000000
    
    int arri[MAX_LEN];
    
    int compare(const void* i, const void* j)
    {
      return (*(int*)i - *(int*)j);
    }
    
    int main()
    {
      for (int i = 0; i < MAX_LEN; i++) {
        arri[i] = rand() % MAX_LEN;
      }
    
      ::clock_t start, finish;
    
      //STL sort
      start = ::clock();
      sort(arri, arri + MAX_LEN);
      finish = ::clock();
    
      cout << "STL sort:\t" << finish - start << "ms" << endl;
    
      for (int i = 0; i < MAX_LEN; i++) {
        arri[i] = rand() % MAX_LEN;
      }
    
      //C qsort
      start = ::clock();
      qsort(arri, MAX_LEN, sizeof(arri[0]), compare);
      finish = ::clock();
    
      cout << "C qsort:\t\t" << finish - start << "ms" << endl;
    
      return 0;
    }

    我机器上装的是CodeBlocks(10.05)+MinGW(4.7.2)的环境。

    首先,在debug模式下,CodeBlocks显示出当前的编译器命令:

    mingw32-g++.exe -Wall -fexceptions  -g  -std=c++0x    -c D:\Workspaces\CodeBlocks\TestSortQ\main.cpp -o obj\Debug\main.o
    mingw32-g++.exe  -o bin\Debug\TestSortQ.exe obj\Debug\main.o

    运行结果:

    image

    运行结果让我大吃一惊,STL不可能这么慢啊!后来仔细一想,这是debug模式,没有加任何优化,加上优化看看什么结果:

    mingw32-g++.exe -Wall -fexceptions  -O2  -std=c++0x    -c D:\Workspaces\CodeBlocks\TestSortQ\main.cpp -o obj\Release\main.o
    mingw32-g++.exe  -o bin\Release\TestSortQ.exe obj\Release\main.o   -s

    运行结果:

    image

    果然,O2选项一加上,STL sort瞬间完成了逆袭,运行时间优化了75%,而C qsort优化前后变化不是很明显,大概减少了10%。

    问题来了,为什么C++标准库的快排的优化效果如此明显,而C库的快排优化不是很明显呢?

    答案是inline

    我们知道,STL是泛型编程的杰出成果,里面的容器、迭代器、算法几乎都是通过泛型实现的,使得STL的通用性很强。泛型编程的一个负面效果就是破坏了接口与实现的分离,即头文件中声明,源文件中实现,源文件单独编译成库,用户只需要拿到头文件和库就可以使用了,看不到具体实现,这就是所谓的ABI,也是C的传统做法。有人会问,为什么不能做到接口与实现的分离,因为泛型编程中的函数和类,在没有接受一个模版参数之前,是没办法实例化的,只有当用户给定了模版参数的时候,编译器才会去实例化一个具体的类或函数。

    如,C++ STL中的快速排序算法定义:

    template< class RandomIt >
    void sort( RandomIt first, RandomIt last );
    只有RandomIt这个类型真正确定了,编译器才会去实例化这个方法。在上面的代码中,我这么写:
    sort(arri, arri + MAX_LEN);

    编译器通过自动类型推导,知道了RandomIt其实是一个int*,于是产生这个函数:

    sort(int*, int*);

    具体实现中的RandomIt已经都替换成相应的int*,一个完整的函数就产生了。

    关键的问题出现了!编译器在实例化一个函数(类也一样)的时候,它必须知道具体的实现代码,才能够产生完整的函数。这也是为什么如果大家自己写模版的时候,.h文件和.cpp文件的关系变得十分奇怪的原因,一般做饭是在.h文件末尾,#include xx.cpp,其中xx.cpp中实现了.h文件中的函数声明,或者干脆直接在.h文件中写实现。否则,如果按照一般的.h和.cpp的关系,编译器会报错,说找不到函数的实现。写过模版的程序猿应该都知道这个。

    事实上,C++标准委员会为了解决这个问题,曾经引入了export关键字,来试图解决这个问题,但很少有编译器实现了(估计是实现难度较大,且增加了复杂度,得不偿失),所以这个关键字后来基本上费了。

    大家或许会问,你是不是走题了,刚开始不是讨论C++的效率问题吗?怎么说了半天泛型和模版的事情了?

    答案是,真是由于泛型的这个“副作用”,使得编译器可以做更多的优化!

    既然编译器知道具体的实现,那么inline是编译器可以在优化上大显身手的一个手段,sort函数中需要一个compare函数(在C++中还可以通过函数对象或者操作符重载实现)来知道如何比较两个元素的大小,sort函数每次比较的时候,都会调用这个函数。对于一个10000000个元素的数组,一共会调用多少次这个compare函数是可想而知的(具体数目可以算出来),而一次函数调用的开销比较大,如栈的分配等等,这就很大程度上限制了C库中的qsort的威力,因为qsort的实现是在编译在库中的,它所调用的函数就没法inline到qsort函数里面去。但是STL是可以做到的,所以它的优化效果非常明显。

    另外一个STL sort效率高的原因,在于算法的实现,不仅仅是快速排序算法,估计这也是为什么名字叫sort而不叫qsort的原因吧。在SGI STL(GNU所使用的STL)的实现中,sort函数一共采用了三种排序算法,分别是quick sort,heap sort和insert sort。使用策略如下:

    1、函数主体为quick sort,但是在递归调用的时候,加上了一个参数记录迭代层数,如果迭代次数超过一定数目,转而采用heap sort。原因是如果迭代次数过多,很可能意味着quick sort落入了最坏情况(O(n2)),而heap sort的最坏情况依然是O(nlogn)。

    2、当数组划分到很小的一段时,采用insert sort。原因是对于小数据量采用quick sort有些不划算(quick sort适合处理大量数据),因为quick sort本身是递归的,递归就是一次函数调用,开销较大。而insert sort在较小数据量的情况下,表现很好。

    具体算法可参考SGI STL和侯捷的《STL源码剖析》。

    说了这么一大堆,其实想表达的一点就是,C++为了提高效率,可以说无所不用其极,无论是STL算法实现,还是编译器的优化(语言本身为了编译器能做优化也下了很多功夫),都体现了C++的三大设计思想(或者叫做约束,可参考孟岩《关于C++复杂性的零碎思考》)之一:最高性能。

    回到那位哥们的问题,为什么他的快排效率比C库的要高?我不否认他的水平,但是我感觉最大的原因还是,自己写的函数,编译器可以将compare的功能内敛到函数里面去,所以效率比C库的qsort效率要高。

    关于C++的高效,我还想继续写一些文章,这篇博客且当作一个开始吧。

    Top
    收藏
    关注
    评论
  • 相关阅读:
    “回调地址”全攻略 java程序员
    超棒的纯Javascript实现的文件上传功能 Fine uploader java程序员
    帮助你操作数字和处理数字格式的javascript类库 Numeral.js java程序员
    正则表达式 java程序员
    一款非常棒的CSS 3D下拉式菜单实现Makisu java程序员
    Google的全新在线地图API演示网站 More than a map java程序员
    Android中的布局属性 java程序员
    整理一系列优秀的Android开发源码 java程序员
    Lambda表达式select()和where()的区别
    按公式产生随机数、java中的重载、递归、有关计算机计算的问题
  • 原文地址:https://www.cnblogs.com/codemood/p/2787691.html
Copyright © 2020-2023  润新知