身边的人一般说到这个问题,总会马上第一个回应我,C语言的速度更快。但实际上,这个问题并不是可以这样单纯的来回答的。下面就我个人的一些体会以及看书时了解的内容,对这个问题做一个粗浅的整理和总结。如有疏漏,欢迎指出。
一、什么是C,什么是C++
说起来,我觉得这是一个非常难以回答的问题。因为我们在这样说的时候,已经暗含了默认常规的C代码不属于C++的意思。但由于C++对C的兼容,实际上C语言实现的功能也是C++能实现的功能。我们平时更多要进行对比的,是在解决某一个问题时,C语言编写者编写程序常规的思路和方法与C++程序编写者编写程序的常规的思路和方法写出的程序在效能上的对比。从某种意义上来说,优秀的C语言程序员写出的程序和优秀的C++程序员写出的程序基本上不会有太大的效率上的差异的。但考虑到真正优秀的程序员的稀缺性,尤其是庞大复杂的C++语言导致优秀的C++程序员更是少见,所以我还是决定就这个问题进行一个粗浅的探究。
二、哪些时候C++比C更快
在写这部分的时候,我脑中总是会不停地回响起很多人的声音:“C肯定比C++快啊”。因此,我针对这个问题进行了一些搜索,试图找到C++比C运行更快的案例,结果是我找到了下面这样三个具体的实验案例。
第一个实验,字符串连接实验
当做字符串连接的操作时,一个常规的C程序员,首先会想到的事分配足够空间的字符数组,然后利用strcat函数进行字符串连接(当然,高手若使用更特殊的方法来达到非常高效的实现的情况不做考虑。因为针对不同的情境,需要进行不同的特定优化,这样实现需要付出比较高的代码优化设计的代价。)而一个常规的C++程序员则会选择使用string类,并利用string类的拼接操作。
int main()
{ int i; char s[10001]; string str,str2,str3; clock_t t; t=clock(); s[0]=0; for (i=0;i<10000;i++) strcat(s,"a"); printf("strcat used %ld clock ticksn",(clock()-t));t=clock(); str=""; for (i=0;i<10000;i++) str=str+"a"; printf("string used %ld clock ticksn",(clock()-t));t=clock(); str2=""; for (i=0;i<10000;i++) str2+="a"; printf("string2 used %ld clock ticksn",(clock()-t));t=clock(); str3.reserve(10000); str3=""; for (i=0;i<10000;i++) str3+="a"; printf("string3 used %ld clock ticksn",(clock()-t));
}
上面4部分代码在我的电脑上输出的结果依次是40,8,1,0 (windows平台g++ 4.8.2)。str的拼接是一个新手程序员的写法,str2的拼接是一个比较常规的写法,而str3则是比较优化的写法(对于C语言的实验,我暂时没有想到比较简单的优化方法,想到的只有自己写拼接函数,并且维护字符数组的有效长度信息,实现起来相比下面的优化实在有些麻烦)。str=str+"a"的实现机制是对加号左边的字符串复制生成一个临时string变量,然后将加号右边的append进去,最后再复制给等号左边的string变量。str2+="a"的实现机制则是直接将右边的string append到左边的string变量中。相较之下,后者的开销要小非常多。而str3.reserve(10000)操作,避免了string动态增长时候的反复的内存分配,因此会有更高的效率。
下面说说为什么C语言实现的字符串连接会慢这么多。主要还是C语言字符串结构的特点造成的。C语言的字符串的结束要以''来作为标记。当我们执行strcat的时候,首先要找到字符串的结尾'',这是一个O(n)的操作,然后在其后添加要连接的字符串(所以strlen要慎用,它也是一个O(n)的操作)。而C++的string类是保存有字符串的长度的信息的,因此找字符串的结尾是一个O(1)的操作。因此要想优化C语言的字符串连接,就需要额外维护一个长度信息,并重新写字符串连接函数(貌似没有现成的可以利用了),其花费的精力是比较多的。
最后要澄清的一点是,有人说,C语言的我使用s[i]=‘a'进行赋值就好了啊。这里我之所以循环10000次并且每次都连接相同的’a'操作,主要原因是只做一次操作的时间太短了啊,利用系统常规的函数是根本无法统计出时间的。重复10000次将时间放大之后,可以看出两种字符串连接(长字符串,短字符串差距会很小)的方式在运行时间上还是差距比较大的,虽然他们运行一次的时间都非常非常的短。
第二个实验,排序实验
当我们要对一个数组进行排序的时候,我们一般首先想到的是调用库的排序函数,只有个别的时候我们才会自己去写排序函数。下面就qsort函数和std::sort函数进行一个简单的对比。
int comp(const void*a,const void*b)
{return *(int*)a-*(int*)b;
}
bool comp2(const int a,const int b)
{return a>b;
}
int main()
{clock_t t;srand(time(NULL));int n=100000;int array1[n],array2[n];for(int i=0;i<n;i++)array2[i]=array1[i]=rand();t=clock();qsort(array1,n,sizeof(int),comp);printf("qsort used %ld clock ticksn",(clock()-t));t=clock();std::sort(&array2[0],&array2[n-1],comp2);printf("std::sort used %ld clock ticksn",(clock()-t));
}
运行的结果是qsort:18,std::sort:13(windows平台g++ 4.8.2)。在运行的时候最开始使用的是debug版本,结果sort是比qsort慢的,其原因是编译器并没有对代码进行优化,后面会提及编译器对提高C++代码效率的巨大作用。主要原因我觉得还是std::sort函数优化了排序算法而造成的结果。不过,其实C++内联的优化是更重要的原因。
下面是吴神的说法:
sort配合functor使用,效率比qsort会提高很多,其根本原因是,sort使用functor时两个元素之间的比较通过内联函数,而qsort每次两两元素之间的比较通过函数指针来调用相应的比较函数,内联函数和普通函数调用开销不言而喻。因此再使用sort时一定要配合使用functor,不要使用函数指针传递比较函数(有的编译器也能将其优化与使用functor一样的效率,但是个人觉得如果需要重写比较函数,建议使用函数子的方式重写)。coding加以说明:
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <ctime>#define SIZE 10000
using namespace std;int mycomp(const void* _a,const void* _b){return *((int*)_a) - *((int*)_b);
}bool myless(const int _a, const int _b){return _a < _b;
}struct LessFunctor : public binary_function<int, int, bool>{bool operator()(const int& _a, const int &_b){return _a < _b;}
};int main(int argc, char **argv){int array1[SIZE], array2[SIZE], array3[SIZE];srand(time(NULL));for(int i = 0; i < SIZE; i++)array1[i] = array2[i] = array3[i] = rand() % SIZE;clock_t begin = clock();qsort(array1, SIZE, sizeof(int), mycomp);clock_t end = clock();printf("qsort tooks %ld clock ticks(%lf seconds)n", end - begin, (double)(end-begin)/CLOCKS_PER_SEC);begin = clock();sort(array2, array2+SIZE, myless);end = clock();printf("sort using function pointer tooks %ld clock ticks(%lf seconds)n", end - begin, (double)(end-begin)/CLOCKS_PER_SEC);begin = clock();sort(array3, array3+SIZE, LessFunctor());end = clock();printf("sort using functor tooks %ld clock ticks(%lf seconds)n", end - begin, (double)(end-begin)/CLOCKS_PER_SEC);return 0;
}
DEBUG版本:
g++ sort.cpp -o sort -Wall
运行结果:
qsort tooks 1447 clock ticks(0.001447 seconds)
sort using function pointer tooks 1692 clock ticks(0.001692 seconds)
sort using functor tooks 1658 clock ticks(0.001658 seconds)
O3优化版本:
g++ sort.cpp -o sort -Wall -O3
运行结果:
qsort tooks 1225 clock ticks(0.001225 seconds)
sort using function pointer tooks 779 clock ticks(0.000779 seconds)
sort using functor tooks 679 clock ticks(0.000679 seconds)
第三个实验,标准流输入实验
对于程序员来说,输入和输出是必不可少的操作。C语言程序员使用scanf函数来实现输入,而C++程序员一般会使用cin来实现输入。那么这两者相较之下速度如何呢?经常做OJ的人一般会马上给出答案,scanf会远快于cin。但事实真的是这样么?下面我就此做一个实验。
int main()
{clock_t t;int num;srand(0);ofstream file("data");int size=10000000;for(int i=0;i<size;i++){file<<rand()<<" ";if((i+1)%20==0) file<<endl;}freopen("data","r",stdin);t=clock();for(int i=0;i<size;i++)cin>>num;printf("cin used %ld clock ticksn",clock()-t);fclose(stdin);freopen("data","r",stdin);t=clock();for(int i=0;i<size;i++)scanf("%d",&num);printf("scanf used %ld clock ticksn",clock()-t);fclose(stdin);freopen("data","r",stdin);ios::sync_with_stdio(false);t=clock();for(int i=0;i<size;i++)cin>>num;printf("cin without sync used %ld clock ticksn",clock()-t);fclose(stdin);
}
输出的结果为cin:17869;scanf:12608;cin without sync:3989(windows平台g++ 4.8.2)。从中我们可以看出,对于单个数据的输入,如果正确使用了cin,其速度是远远快于scanf函数的。 ios::sync_with_stdio(false)这句是关掉cin的默认同步功能。在默认情况下,cin是为了保证兼容与stdin保持同步的,这样我们混用cin和scanf函数就不会出现混乱的情况。但维持这个同步是有着巨大的开销的。当我们没有混用时,关掉这个同步功能,就会使得代码速度得到很大的提升,会比scanf快上很多。
另外,对于多个参数的时候,由于scanf是一次函数调用,而每一个>>是一次函数调用,因此多个参数的时候,scanf可能会比cin更快。我进行了同时输入4个数据的情况,其结果是scanf:8819和cin:2295 ,并没有出现我预期的情况。
在此处我又进行了两次实验,一次是格式化输入的时候数据用空格间隔,一次是格式化输入的时候每个数据之间使用换行间隔。通过对比一次输入4个数据和一次输入两个数据的情况,发现使用空格间隔消耗的时间要明显大于使用换行间隔的时间,这说明了在对格式化字符处理上的开销也是比较大的。另外,在不同的机器上进行对比,发现在不同的机器上,同样的代码,关闭同步的cin和scanf进行比较,很多时候会出现cin耗时略大于scanf的情况,少数时候(本机),出现cin比scanf快很多的情况。说明不同编译器对其优化产生的差异是非常大的。
下面就调试模式下的汇编代码对其进行一个简单的分析。
159 scanf("%d%d%d%d", &num1, &num2, &num3, &num4);
0x4017bd <+0x0161> lea -0x3c(%ebp),%eax
0x4017c0 <+0x0164> mov %eax,0x10(%esp)
0x4017c4 <+0x0168> lea -0x38(%ebp),%eax
0x4017c7 <+0x016b> mov %eax,0xc(%esp)
0x4017cb <+0x016f> lea -0x34(%ebp),%eax
0x4017ce <+0x0172> mov %eax,0x8(%esp)
0x4017d2 <+0x0176> lea -0x30(%ebp),%eax
0x4017d5 <+0x0179> mov %eax,0x4(%esp)
0x4017d9 <+0x017d> movl $0x410091,(%esp)
0x4017e0 <+0x0184> call 0x401600 <scanf(char const*, ...)>
这是scanf函数产生的汇编代码,我们可以注意到,几个参数的传递都是采用了压栈的方式。
188 cin >> num1 >> num2 >> num3 >> num4;
0x40194c <+0x02f0> lea -0x30(%ebp),%eax
0x40194f <+0x02f3> mov %eax,(%esp)
0x401952 <+0x02f6> mov $0x6fcba0e0,%ecx
0x401957 <+0x02fb> call 0x401a84 <_ZNSirsERi>
0x40195c <+0x0300> sub $0x4,%esp
0x40195f <+0x0303> lea -0x34(%ebp),%edx
0x401962 <+0x0306> mov %edx,(%esp)
0x401965 <+0x0309> mov %eax,%ecx
0x401967 <+0x030b> call 0x401a84 <_ZNSirsERi>
0x40196c <+0x0310> sub $0x4,%esp
0x40196f <+0x0313> lea -0x38(%ebp),%edx
0x401972 <+0x0316> mov %edx,(%esp)
0x401975 <+0x0319> mov %eax,%ecx
0x401977 <+0x031b> call 0x401a84 <_ZNSirsERi>
0x40197c <+0x0320> sub $0x4,%esp
0x40197f <+0x0323> lea -0x3c(%ebp),%edx
0x401982 <+0x0326> mov %edx,(%esp)
0x401985 <+0x0329> mov %eax,%ecx
0x401987 <+0x032b> call 0x401a84 <_ZNSirsERi>
0x40198c <+0x0330> sub $0x4,%esp
我们再看cin产生的汇编代码,可以清晰的看出,每一次>>都进行了一次函数调用。并且在传递参数的时候默认用寄存器ecx传递了this指针。
总之,可以得出的结论就是在合理的使用cin、cout的情况下,它们的效率不但不低,有时候反而可能会更高。
在这里顺便提一嘴"n"和std::endl的差别。前者只是单纯的换行,而后者除了换行,还有刷新缓冲区的功能(即把缓冲区内的内容输出),因此后者在效率上会慢很多。在不需要刷新缓冲区的场景下,一定要使用前者,尤其是在做OJ的时候。
顺便强调一下,由于编译器的差异,以上的三个实验用不同的编译器做出来的结果未必会相同,上述结论只在使用了对代码进行良好优化的编译器下成立。
其他C++比C快的理由:(主要摘自《游戏之旅——我的编程感悟》一书,主要说明了同样采用面向过程的设计方法,C编译器产生的代码效率会比C++编译器低。这部分我未进行验证,由于此书出版于2006年,我不确定现在的编译器是否有新的变化。此处我挑选了我认同的几个部分进行了精简和摘抄)
1.全局变量
C++在实现全局变量时一般会使用单件来实现,表面上看会多一次间接性。但现代CPU在对待数据时,把数据集中有助于提高数据高速缓存的使用效率。直接访问全局变量会产生一个32位的指针,处理速度要慢于用一个this指针加一个短偏移量。
2.函数调用的堆栈处理
C语言中为了支持不确定参数个数的函数调用,强迫所有函数都调用函数本身的代码来把压入堆栈的参数出栈,这比让函数自身弹参数出栈要稍微低效一点。
而大多数的C++编译器可以让不同的函数使用不同的调用方式,默认情况不再使用那种略微低效的调用方式。
3.函数调用时候的参数传递
C语言传递参数主要是压入堆栈。有不少C编译器支持寄存器传参,但没有标准。
C++里类的函数调用会默认传递this指针,在准标准中,这个指针是使用寄存器传递的。(这里破坏了面向过程设计的大前提,还是使用了面向对象来说明效率)
4.代码生成
C++编译器有编译时推导的能力,让很多工作可以在编译时完成,有利于生成更佳的代码。(代价是编译器要消耗更多的资源,代码占用的空间更大,这对于资源紧张的嵌入式系统不利)
5.异常
C使用setjmp/longjmp来实现异常状态处理,而C++内建异常,让编译器产生许多额外的代码,实践代码空间换时间的策略,提高运行时间效率。
6.inline
C原本不支持inline,在C99才纳入标准。C++编译器则把inline技术发挥的淋漓尽致,使得很多看似复杂的C++程序可以飞快运行。
7.标准库差异
(此处解释了qsort 和std::sort的差异) qsort要求编写一个比较函数,而std::sort使用模板技术,让比较函数内联,消除函数调用消耗。
不过需要注意的一点是,这里提到的C++比C快的情况,在单个操作下差距都是很小的。而下面的拖慢C++的情况,往往都会拖慢非常多的速度。这也就是一个C++程序往往会比C程序慢的原因。
三、拖慢C++速度的情况
1.类层次过细,增加了过多的调用消耗
2.滥用异常。异常的处理会造成代码膨胀,并且会影响代码速度。
3.虚函数、虚继承。主要是查虚表影响运行速度。
由于我对C++了解的浅薄,也许还有更多可以说明的内容,欢迎大家补充。
四、嵌入式为什么用C更普遍
C++相对于C来说,在很多地方是有很大优势的,但在嵌入式系统的开发上,很少有人会使用C++,这里面的原因是什么呢?
首先,上面的内容主要说明了一点,要想用C++写出比C快的程序,需要程序员非常了解C++,而恰巧C++又极其庞大而复杂,使得这成为一个几乎不可能完成的任务。而做C语言高效代码开发相应的门槛就要低上很多。
其次,C++采用了大量的使用代码空间换时间的操作,使得C++编译的程序会比C的更庞大,这对于存储资源紧张的嵌入式系统代价较为高昂。
另外,嵌入式系统主要由C编写,而且其C的相关资源也多一些,便于学习,可移植性也更好。
而且有些平台没有C++编译器,C++编译器所消耗的大量资源对于嵌入式系统来说也可能负担过重,交叉编译又会使工作变得复杂。
五、总结
总的来说,其实探讨这样一个问题是一件挺没意义的事情。不过出于好奇,还是搜索整理了一些资料,并亲自做了一些实验来进行验证。
硬说要得到一个结论的话,就是不同的语言有不同的适用场合。而决定一个程序的运行效率的,不是用什么样的语言来编写,而是编写这个程序的程序员的水平如何。至少,在写完这篇博客之后,我再也不会说出C语言比C++快这样的话了,听到这样的话我也可以一笑而过。决定哪个更快的,还是取决于编写的人和实际的应用场景,不是么?