Fork me on GitHub
C++的高效从何而来2

C++的高效从何而来(二)

之前就写过一篇博客《C++的高效从何而来》,分析C++中效率问题。最近在Herb Sutter(C++标准委员会的chair)的GotW中看到了这篇文章GotW #2: Temporary Objects (5/10),主要是讲C++中临时对象的问题,文章给出了一段代码,问读者有多少处地方产生了不必要的临时对象。代码如下:

复制代码
string find_addr( list<employee> emps, string name ) {
    for( auto i = begin(emps); i != end(emps); i++ ) {
        if( *i == name ) {
            return i->addr;
        }
    }
    return "";
}
复制代码

这段代码的作用是在emps这个list中寻找名字为name的那个employee。具体答案我们在这边不做太多讨论,有兴趣的可以自己想想。

我看到这个函数以后,第一反应是:这样的实现是不是最好的?

我们知道C++里面要完成这样一个查找有很多方式,我自己想到了下面着一些(我在代码中用的是vector):

1、最直观的for循环

复制代码
// original for loop
string find_addr_01(const vector<Employee>& emps, const string& name)
{
    auto emps_end = end(emps);
    for (auto iter = begin(emps); iter != emps_end; ++iter) {
        if (iter->name_ == name) {
            return iter->addr_;
        }
    }
    const static string empty_string;
    return empty_string;
}
复制代码

这段代码其实是我自己对Herb Sutter那段代码的一个改进版,消除了不必要的临时变量。

2、C++11中引入了range-based for循环,用法和Java中的类似

复制代码
// Range-based for loop(since C++11)
string find_addr_02(const vector<Employee>& emps, const string& name)
{
    for (const auto& employee : emps) {
        if (employee.name_ == name) {
            return employee.addr_;
        }
    }
    const static string empty_string;
    return empty_string;
}
复制代码

这段代码相对于find_addr_01没有什么新东西,语法简单而已。

3、STL算法find_if和lambda表达式

复制代码
// find_if with lambda expression
string find_addr_03(const vector<Employee>& emps, const string& name){
    auto& iter = find_if(begin(emps), end(emps), [&](const Employee& employee) -> bool {
        return employee.name_ == name;
    });
    if (iter != end(emps))
        return iter->addr_;
    const static string empty_string;
    return empty_string;
}
复制代码

这段代码将find_if算法和lambda表达式结合起来,比较直观和优雅,符合Modern C++的风格。

4、find_if的手动实现版

了解STL的人应该知道find_if的内部实现,其实很简单:

template <class InputIterator, class Predicate> 
InputIterator find_if(InputIterator first, InputIterator last, Predicate pred) {
    while(first != last && !pred(*first)) ++first;
    return first;
}

我自己也可以手动实现,还省了一次函数调用:

复制代码
// original for loop without if inside while
string find_addr_04(const vector<Employee>& emps, const string& name)
{
    auto iter = begin(emps);
    auto emps_end = end(emps);
    while (iter != emps_end && iter->name_ != name) ++iter;
    if (iter != emps_end)
        return iter->addr_;
    const static string empty_string;
    return empty_string;
}
复制代码

这段代码和find_addr_01中的差别在于,把循环中的if语句的判断提到循环中来了。

5、独具C++特色的function object

复制代码
// function object
struct EmpCompare {
    EmpCompare(const string& name) : name_(name)
    {}
    bool operator()(const Employee& emp)
    {
        return emp.name_ == name_;
    }
    string name_;
};

// find_if with function object
string find_addr_05(const vector<Employee>& emps, const string& name){
    auto& iter = find_if(begin(emps), end(emps), EmpCompare(name));
    if (iter != end(emps))
        return iter->addr_;
    const static string empty_string;
    return empty_string;
}
复制代码

这段代码和find_addr_03很像,只不过把find_if中的Predicate从lambda表达式换成了function object而已。

6、最后一个是find_if和bind的综合

复制代码
bool emp_compare(const Employee& emp, const string& name)
{
    return emp.name_ == name;
}

// find_if with bind and function pointer
string find_addr_06(const vector<Employee>& emps, const string& name){
    auto& iter = find_if(begin(emps), end(emps), bind(emp_compare, placeholders::_1, name));
    if (iter != end(emps))
        return iter->addr_;
    const static string empty_string;
    return empty_string;
}
复制代码

bind是C++11新加入的adapter,作用相当于bind1st和bind2nd,但用途更广,支持绑定多个,placeholders::_1是一个占位符,表示待接受的参数。

不知道大家感觉上面这6个函数,效率谁高谁低?

废话少说,写测试代码,首先是一些辅助函数(这段代码的风格是模仿Milo Yip的):

复制代码
#define COUNT 10000        // loop count
#define EMPS  10000        // emps size

#define TIME(X) { \
    LARGE_INTEGER start, stop, freq; \
    QueryPerformanceCounter(&start); \
    {X;} \
    QueryPerformanceCounter(&stop); \
    QueryPerformanceFrequency(&freq); \
    double duration = (double)(stop.QuadPart - start.QuadPart) / (double)(freq.QuadPart); \
    cout << setw(10) << fixed << duration << " " << #X << endl; \
}
struct Employee {
    Employee(const string& name, const string& addr)
        : name_(name), addr_(addr)
    {}
    string name_;
    string addr_;
};

typedef string (*find_addr_func)(const vector<Employee>&, const string&);
void test_average(const vector<Employee>& emps,  find_addr_func find_addr)
{
    for (int i = 0; i < COUNT; i++) {
        string addr = find_addr(emps, "name5000");
        assert(addr == "addr5000");
    }
}
复制代码

COUNT是循环次数,EMPS是员工的数目,TIME宏用来记录运行时间(较精确,仅在windows下有效,linux下可以用不是特别精确的clock_t来实现),test_average函数接受一个vector和一个find_addr_func类型的函数指针(通过typedef定义),分别去寻找名字为"name5000”的employee。

main函数如下:

复制代码
int main()
{
    vector<Employee> emps;
    emps.reserve(EMPS);
    for (int i = 0; i < EMPS; ++i) {
        stringstream ssname, ssaddr;
        ssname << "name" << i;
        ssaddr << "addr" << i;
        emps.push_back(Employee(ssname.str(), ssaddr.str()));
    }
    //srand(0);
    //random_shuffle(begin(emps), end(emps));

    TIME(test_average(emps, find_addr_01));
    TIME(test_average(emps, find_addr_02));
    TIME(test_average(emps, find_addr_03));
    TIME(test_average(emps, find_addr_04));
    TIME(test_average(emps, find_addr_05));
    TIME(test_average(emps, find_addr_06));

    return 0;
}
复制代码

首先把vector初始化好,预留10000个空间,构造出“name0,addr0”到“name9999,addr9999”的Employee对象。大家可能看出来了,在调用find_addr_xx的时候,每次都是寻找中间的name5000这个employee。大家也可以通过random_shuffle来将vector中的对象随机打乱,不过这样对我们测试的影响不大。

我的机器配置是i5-2400+8G,在VS2010 Update2中的运行结果:

Debug(/Od):

image

Release(/O2):

image

大家可能会问,是我眼睛看花了吗?怎么相差几十倍啊!

这也是我没有搞懂的一个问题,VC到底在Debug模式下做了什么东西,怎么会这么慢?

在没有优化之前,find_if+lambda表达式,以及find_if+function object是效率最高的,但是优化以后就不一定了,所以我决定看看其它编译器的结果。

使用MinGW4.8,Debug模式下(不开优化):

image

Release模式下(-O2):

image

MinGW给出的结果就比较靠谱了,优化效果也很明显,并且优化前后效率关系基本保持一致,比较利于我们的分析。

首先,find_if+bind效率最低,原因是adapter从某种程度上阻止了内联(MinGW中没有加优化,时间几乎是其它的4倍),但编译器优化似乎做得也不错,最后也没有慢太多;

其次,VC下手写的for循环总体比find_if要慢一些,但find_addr_04的效率在VC中是挺高的,说明是while循环中的if语句起了影响,使得CPU的分支预测更准确,提出来之后效率提高很明显;

第三,VC下find_addr_04比find_if+lambda和find_if+function object要快,因为lambda表达式和function object都多了一个function object的构造和析构,MinGW中这一点体现不出来,不是很清楚原因,但是相差也不大;

第四,MinGW秒杀VC,我真想不通微软在自己的操作系统上的编译器为什么还不如Linux上GCC移植过来的编译器产生的代码?

结论:C++中实现循环的方式很多,之间的效率差别有各种原因(主要是内联和分支预测),但现在的C++编译器的优化能力已经是非常强悍了,效率上的差别已经不是那么明显了。反而最大的区别是体现在代码风格上,我个人比较喜欢find_if+lambda表达式,它是高效(lambda表达式容易内联)和直观(STL中的算法实现即高效又直观,从名字上就知道用途,不用自己写for循环,很不直观)的完美结合。不知道大家更喜欢哪种?

收藏
关注
评论
 
分类: C++

托管代码与非托管代码的执行效率比较

 

[转]托管代码与非托管代码的执行效率比较
值得一看

一、首先回答一个问题:托管代码(.net)比非托管代码(vc++)慢吗?

如果你用上面这个问题去问每一个人,基本上每个人都会回答,肯定会慢! 那么他们说的是正确的吗? 不,并不正确。 问题在于,绝大多数人认为.Net只是一个基于运行库的框架,就像Java或者VB,或者他们甚至以为.Net使用像Java一样的虚拟机系统。 他们并沒考虑到程序本身,沒考虑到程序是用来干什么,也没有考虑到访问网络或者磁盘的速度因素。简单来说,就是他们根本没有思考!

.NET并不像那种运行库(VB或者Java)。 它是一个经过精心构思的,并且微软在其身上下了极大功夫的框架,以保证它的良好运行。 在这篇文章 我将给大家展示一些将需要大量运算的代码,并且将他编译成托管以及非托管代码。 然后我将测量这两个库分别的表现。 你将看到, 并不会因为这是.net程序就自动要比c++程序慢,事实是,在某些情况,托管代码甚至比非托管代码更快。

基本上每个人都知道的是,所有.Net语言都将被编译成为一个叫做IL汇编的中间语言。但是计算机是如何执行这个中间代码的,却是很多人不知道,甚至理解错误了的。
JIT是.NET程序运行的重要部件之一,全称是即时编译器。我刚才说的误解,就是很多人(绝对不是少数,问了很多c++程序员,10个有9个这种想法)都以为JIT其实就是跟Java VM差不多的东西,是一个Interpreter,在运行时读取IL汇编代码,然后模拟成x86代码(也就是俗称的虚拟机)。但是事实上,.NET使用的是更为高级的技术。 .Net程序被加载入内存以后,当某段IL代码被第一次运行的时候,JIT编译器就会将这段IL代码,全部编译成本地代码,然后再执行。这也就是为什么.NET程序第一次运行都启动很慢的原因! 随.NET库,微软还附带了一个工具,可以事先将.NET程序所有的IL代码都编译成本地代码并保存在缓存区中,这样一来,这个程序就跟c++编译的一模一样了,没有任何区别,运行时也可以脱离JIT了(这里不要混淆了,这里不是说可以脱离.NET库,而是说不需要在进行即时编译这个过程了)。所以,请不要将.NET和Java混为一谈,两个的运行效率根本不是一个等级的!

 

二、实验比较

作为测试算法,我们选中了FFT(Fast Fourier Transform),这是一个将跟时间有关系的数据(例如音乐)转换成他应有的频率信息的算法。
这个算法有很多种,如果你用Google搜索会发现很多,这里我选中了Real Discrete Fourier Transform, 因为他比较简单明了,比较好修改。 我将其复制了4份,分别用于测试托管的C++, C++/CLI,C#。

非托管的代码我只是将其函数名称改成了fourier,并且加入了__declspec(dllexport)用来导出。
托管的代码改动的要稍多些:

    * 方法参数改成了托管的Array, 并且使用Array::Length来代替额外的长度参数
    * 涉及到三角函数的地方都改为使用Math类下的方法
    * 算法被作为一个公开类的静态成员导出

然后我把托管的c++代码转换成了c#,只做了极小的变化(大多数是语法上以及申明上的改动)
最后,我又将托管的c++代码转换成了C++/CLI

然后我们将所有版本都分别编译几个不同的版本:未优化版,空间优化,速度优化.

结果:
我将这些程序分别在两台电脑上进行了测试,一台是装了.net 2.0的 XPSP2,处理器是PIII 850, 512MB内存。 另外一台是Vista build 5321,处理器是2GHz 移动PIV,1G内存,每次测试我都是取100次算法运算的平均值,结果单位是毫秒,以下是PIII电脑的运行结果:
                        沒优化              进行了空间优化  进行了速度优化
Unmanaged     92.88 ± 0.09  88.23 ± 0.09  68.48 ± 0.03
Managed C++  72.89 ± 0.03  72.26 ± 0.04  71.35 ± 0.06
C++/CLI           73.00 ± 0.05  72.32 ± 0.03  71.44 ± 0.04
C# Managed    72.21 ± 0.04  69.97 ± 0.08

PIV电脑的结果:
                        沒优化          进行了空间优化  进行了速度优化
Unmanaged     45.2 ± 0.1  30.04 ± 0.04  23.06 ± 0.04
Managed C++  23.5 ± 0.1  23.17 ± 0.08  23.36 ± 0.07
C++/CLI           23.5 ± 0.1  23.11 ± 0.07  23.80 ± 0.05
C# Managed    23.7 ± 0.1  22.78 ± 0.03

可以看出,非托管代码在不同的优化方案上存在很大的效率差异,PIII上不优化比优化慢35%,在PIV上也是。 在这个简陋的统计上表明,不管是哪种优化方案,管理代码在运行效率上并没有太大区别,编译器和连接器并没有影响到运行效率太多, 我在后面会说更多关于这方面的信息。
奇怪的是,在Vista下,管理代码进行空间优化甚至比进行速度优化速度更快!

C#的结果跟托管的C++比起来,并没有太大区别,但是可以看到, 优化过的c#代码比优化过的托管C++代码要稍快些。

现在来比较以下托管代码和非托管代码的结果。 在不优化的情况下,托管代码远远快于非托管代码,这个差距在优化空间后被稍微缩短了点,只有在进行速度优化后,非托管代码才比托管代码稍稍快上一点。非托管代码和C#代码的差别只有3%左右,不过,c#代码仍然比c++的更快! 

 

三、机制实质

.NET的编译器(在这个情况下是托管的C++代码) 可以看成是与非托管C++编译器的Parser引擎是等价的。编译器将生成类,方法等的表,然后进行了一系列的高等级优化。 .NET真正的非托管编译器其实是JIT(即时编译器):这才是程序真正转换成低等級的x86代码的地方..NET编译器和JIT编译的组合,其实跟非托管C++编译器等价的,唯一的不同是,.Net被分成了两个部分.事实上,JIT在运行托管代码时,对.NET程序针对客户电脑进行了优化,而不是像非托管代码那样是在程序员电脑上进行的优化。结果表明,托管C++代码和c#代码的优化设置带来的影响非常小。 显而易见的是,C#代码至少是跟C++代码同样高效

记住!在.NET中没有任何一个部分是自动就必C++代码慢的,运行效率完全取决于程序员。任何一个告诉你托管代码比非托管代码慢的人,都是没有考虑到.NET运行机制的人,简单的说,就是对.NET一窍不通!

 

个人认为C++的编译(了解c++编译原理的都知道)在生成中间代码或汇编代码过程中,可能涉及到优化处理。优化有两种:一种优化仅涉及代码本身,主要是删除公共表达式、循环优化、代码外提、无用 代码赋值等。另一种优化设计具体的计算机硬件,比如,如何根据机器硬件执行指令的特点对指令进行调整优化,减少目标代码长度,提高执行效率。这是非托管代码,这样带来的一个不好的地方就是对于不同很多硬件的用户端,你要达到最好优化效率必须在不同编译环境编译不同版本

但是对于c#的来说,JIT编译则会自动根据硬件的环境去优化编译IL中间代码(其实就是汇编代码)。

链表逆序(反转)

注明,本博客均假设链表没有单独的头结点。

网上博客:http://blog.csdn.net/niuer09/article/details/5961004  一种有有单独头结点的一种实现,带有头结点的实现起来要简单一些的。

单向链表的逆序或是反转是经常会遇到的一个面试题,也是一个非常基础的问题,我自己的面试中就已经遇到的两次。

比如一个链表是这样的: 1->2->3->4->5 通过反转后成为5->4->3->2->1.

最容易想到的方法,其实也可说是最简单的办法就是遍历一遍链表,利用一个辅助指针,存储遍历过程中当前指针指向的下一个元素,然后将当前节点元素的指针反转后,利用已经存储的指针往后面继续遍历。

分析:

  1). 若链表为空,则直接返回;

  2). 设置三个前后相邻的指针pre,粗人,next, 将pre所指向的节点作为cur指向节点的后继;

  3). 重复2)直到cur为空

  4). 调整链表头和链表尾

源代码如下:

   1: void reverse(linka** head) {
   2:     if(head ==NULL || *head ==NULL)
   3:         return;
   4:     linka *phead = *head;
   5:     linka *pre=phead;
   6:     linka *cur=phead->next;
   7:     linka *next =NULL;
   8:     while(cur)
   9:     {
  10:         next = cur->next;
  11:         cur->next = pre;
  12:         pre = cur;
  13:         cur = next;
  14:     }
  15:     phead->next = NULL;
  16:     phead = pre;
  17:  
  18:     *head = phead;
  19: }

 

还有一种利用递归的方法。这种方法的基本思想是在反转当前节点之前先调用递归函数反转后续节点。不过这个方法有一个缺点,可能使得就是在反转后的最后一个结点会形成一个环,所以必须想办法将链表第一节点的next置为NULL.算法的源代码如下:

   1: //使用递归
   2: linka* reverse1(linka*p,linka** head)
   3: {
   4:     if(p == NULL || p->next == NULL)
   5:     {
   6:         *head=p;
   7:         return p;
   8:     }else    if ( p == *head)
   9:     {
  10:         linka* tmp = p->next ;
  11:         p->next =NULL;
  12:         linka* tmp1 = reverse1(tmp,head);
  13:         tmp1->next = p;
  14:         return p;            
  15:     }else
  16:     {
  17:         linka* tmp = reverse1(p->next,head);
  18:         tmp->next = p;
  19:         return p;
  20:     }
  21: } 

 

两个算法因为都需要改变链表头,我们传递的都是链表头的指针,即双重指针。 如果完全采用c++,可以使用引用替代。

还有另一种思路,就是利用C++语言的容器栈,只是空间复杂度和事件复杂度都较大,源码如下:

   1: linka* reverse2(linka** head)
   2: {
   3:     if(head ==NULL || *head ==NULL)
   4:         return *head;
   5:  
   6:     stack<linka*>  stk_link;
   7:  
   8:     linka *phead = *head;
   9:     linka *cur= NULL;
  10:     while(phead != NULL)
  11:     {        
  12:         cur = phead->next;
  13:         stk_link.push(phead);
  14:         phead =cur;
  15:     }
  16:     
  17:     *head = stk_link.top();
  18:     stk_link.pop();
  19:     phead =*head;
  20:     
  21:     while (stk_link.empty() == false)
  22:     {
  23:         cur = stk_link.top();
  24:         phead->next = cur;
  25:         phead= cur ;
  26:         stk_link.pop();
  27:     }    
  28:     phead ->next =NULL;
  29:     return *head;
  30: }

如果有这种思路,当然可以另外一种方法,其空间复杂度为O(1),就是新建一个节点作为临时头结点。循环,一一般简历链表的方式重新建立链表,可以很容易的反向实现, 与第一种在实现上应该是一样的吧。

本程序所有源代码(VS2010编译通过):

   1: // code-summary.cpp : 定义控制台应用程序的入口点。
   2:  
   3: /**************************************************************************
   4:     * Copyright (c) 2013,  All rights reserved.
   5:     * 文件名称    : code-summary.cpp
   6:     * 文件标识    :
   7:     * 摘    要    : 
   8:     * 
   9:     * 当前版本    : Ver 1.0
  10:     * 作者        : 徐冬冬
  11:     * 完成日期    : 2013/05/10
  12:     *
  13:     * 取代版本    : 
  14:     * 原作者    :
  15:     * 完成日期    :  
  16:     * 开放版权  : GNU General Public License GPLv3
  17: *************************************************************************/
  18: #include "stdafx.h"
  19:  
  20: #include <iostream>
  21: #include <random>
  22: #include <stack>
  23: using namespace std;
  24: //链表逆序
  25:  
  26:  
  27:  
  28: struct linka {
  29:     int data;
  30:     linka* next;
  31: };
  32: void print(linka *head);
  33: typedef linka link;
  34:  
  35: void init(link **head)
  36: {
  37:     int i= 0;
  38:     link *phead = *head;
  39:     link *cur = NULL;
  40:     for (i= 0 ;i< 10;i++)
  41:     {
  42:         cur = new link ;
  43:         cur ->data  =rand();
  44:         cur->next = phead;
  45:         phead =cur ;
  46:     }
  47:     *head = phead;
  48:     return ;
  49: }
  50:  
  51: void print(link *head)
  52: {
  53:     int i=0;
  54:     link *cur=head;
  55:     for ( ; cur != NULL ; cur= cur->next)
  56:     {
  57:         cout<< cur ->data <<'\t' ;
  58:     }
  59:     cout<<endl;
  60: }
  61:  
  62: void reverse(linka** head) {
  63:     if(head ==NULL || *head ==NULL)
  64:         return;
  65:     linka *phead = *head;
  66:     linka *pre=phead;
  67:     linka *cur=phead->next;
  68:     linka *next =NULL;
  69:     while(cur)
  70:     {
  71:         next = cur->next;
  72:         cur->next = pre;
  73:         pre = cur;
  74:         cur = next;
  75:     }
  76:     phead->next = NULL;
  77:     phead = pre;
  78:  
  79:     *head = phead;
  80: }
  81: //使用递归
  82: linka* reverse1(linka*p,linka** head)
  83: {
  84:     if(p == NULL || p->next == NULL)
  85:     {
  86:         *head=p;
  87:         return p;
  88:     }else    if ( p == *head)
  89:     {
  90:         linka* tmp = p->next ;
  91:         p->next =NULL;
  92:         linka* tmp1 = reverse1(tmp,head);
  93:         tmp1->next = p;
  94:         return p;            
  95:     }else
  96:     {
  97:         linka* tmp = reverse1(p->next,head);
  98:         tmp->next = p;
  99:         return p;
 100:     }
 101: } 
 102:  
 103: linka* reverse2(linka** head)
 104: {
 105:     if(head ==NULL || *head ==NULL)
 106:         return *head;
 107:  
 108:     stack<linka*>  stk_link;
 109:  
 110:     linka *phead = *head;
 111:     linka *cur= NULL;
 112:     while(phead != NULL)
 113:     {        
 114:         cur = phead->next;
 115:         stk_link.push(phead);
 116:         phead =cur;
 117:     }
 118:     
 119:     *head = stk_link.top();
 120:     stk_link.pop();
 121:     phead =*head;
 122:     
 123:     while (stk_link.empty() == false)
 124:     {
 125:         cur = stk_link.top();
 126:         phead->next = cur;
 127:         phead= cur ;
 128:         stk_link.pop();
 129:     }    
 130:     phead ->next =NULL;
 131:     return *head;
 132: }
 133:  
 134: int _tmain(int argc, _TCHAR* argv[])
 135: {
 136:     linka *head =NULL ;
 137:     init(&head);
 138:     print(head);
 139:     reverse(&head);
 140:     print(head);
 141:  
 142:     reverse1(head,&head);
 143:     print(head);
 144:  
 145:     reverse2(&head);
 146:     print(head);
 147:  
 148:     system("pause");
 149:     return 0;
 150: }
 151:  
雨,静静的飘扬; 心,慢慢的行走; 程序人生,人生迈进。
 
分类: 算法分析
posted on 2013-05-11 22:20  HackerVirus  阅读(330)  评论(0)    收藏  举报