第一部分
不客气地说,CSDN论坛有一股不太好的风气,那就是喜欢空对空。常常就常识性的问题争个不可开交,而真正有价值的帖子却鲜有人问津。这样就很难留住真正有技术积累的且乐于贡献于社区的工程师。而且,由于基本的问题被一遍又一遍地问,总给人一种在低水平徘徊的感觉。道理上讲,还有一种可能是有太多的新人不断加入到C++之中,从而是C++的平均水平被稀释。我情愿是这样。同时,我真心希望,这一代C++工程师不是通过CSDN的C++版来学习C++的,至少不仅仅是。
书归正传。今天,偶然看到了了这个帖子《从号称自己C++好的人说自己C语言很差说开去》[url=http://topic.csdn.net/u/20091011/05/492fe17d-63b8-4050-a677-3b536908d8a7.html][/url],不出所料,其热闹程度非同一般。本来类似的问题也容易引起大家的共鸣和争论,我们可以从中抽提重一点有价值的东西。但是通篇看下来,有价值的评论没有反响,明显错误的却引来热捧。今天,索性花点时间,不揣冒昧,就其中谬种流传给出一个剖析。这种工作其实是吃力不讨好的,在专业工程师眼中是等而下之甚至不入流。那我为什么做呢?
1. 我对自己有一个承诺,有贡献于社区,因为我从社区获取良多。所以不敢偷懒
2. 专家一般不会在这种问题上浪费时间。我不是专家,尽管时间还是宝贵,但是如果它能有用,我就觉得值得
3. 期望能引起思考,进而早日上层次(没有写错!)
1. C++是用来解决什么问题的?
支持或者维护过稍微大一点的项目就会明白,理解现有的代码是最困难的事情。这个困难不是在于你熟悉不熟悉某个库(专用库往往是最容易理解的),而是理解其业务逻辑。说白了,就是类似反汇编当初编码人员的想法。这里,程序或者代码本身的组织带来的复杂性可能已经超过了业务逻辑的复杂性。而且,这种复杂性不是线性增长的!复杂性的来源是一个有趣也是开放的话题,这里仅仅列出一部分,并且看看C++是怎么进步来控制这些复杂性的:
a. 单一作用域内过多可见的名字。过多的名字不仅仅导致名字冲突的几率急剧增加,而且使得该作用域内任何的操作可能波及到范围指数变大。这是我们一直强调尽量不使用全局名字的原因。C提供了结构这样的操作,把多个关联的名字绑定到一个名字;C++除了通过使用访问控制来强化名字使用之外,还提供了名字空间的构造,可以把不是直接关联,但是逻辑相关的名字隐藏起来。这也是为什么C++允许就近声明。
b. 细节完全可见。C++使用访问控制还有一个好处就是隐藏细节。人们往往难以经受走捷径的诱惑而太多暴露是细节很容易让一个程序员在不完全理解的情况下使用了他不应该使用的东西。不要指望通过完善文档来避免这样的事情,因为一来程序员写的文档从来不够更新,二来程序员更相信自己的直觉。这就是我们强调“自文档”的含义,尽量使你的代码通俗到不用写注释也可以完整表达你的含义。这就要求我们使用合适的名字,这也是为什么熟悉const关键字成为C++入门的必须课,也可以解释为什么还要引入专门的mutable关键字。一个常识是代码是写给程序员的,而不是编译器;
c. 相同功能的不同非标准的实现。要想理解非标准的实现,就要深入到其中细节。这对于理解代码简直就是梦魇。更糟糕的是,许多以自己实现字符串类,容器类为骄傲的程序员的实现根本达不到标准的高度,更不要说超出。这里的常识是(如果你不是轮胎厂商就)不要重新发明轮子。这里可以解释为什么C++在语言核心的改进非常谨慎,而在库的支持上不断拓展而精益求精。其他现代语言以丰富的库支持而自豪,自称为C++程序员的人却抱残守缺,以不用任何标准库为荣,真是怪事哪都有,这里特别多。较新C++教学资料,如C++ Primer, Accelerate C++以及Programming Principles and Practice Using C++都完整的OO以及C++标准库为基础。忘记谭浩强先生的C++入门书吧。
d. 资源管理。资源是使用之前需要申请,使用后必须释放的东西。资源由于其稀缺,所以就显得非常重要。最简单的资源就是内存。C程序以及大部分不好的C++程序中,内存问题一直都是占据主要。C++提供了自动调用构造函数,析构函数以及基于值语义的可重载的对象复制机制,进而使得资源管理问题前所未有的简单。这个技术简称RAII,是学习C++登堂入室的关键。熟练使用这个看似简单的技术,就再也不会遭遇资源泄漏,资源重复释放,资源释放后使用等等一系列问题。
e. 错误处理。如果这个问题没有引起你的注意,那么你就没有真正写过工业强度的代码,但就是要求可以7*24运行,可以用于诸如生命保障系统控制的代码,尽管巨大部分的代码都不需要这样的强度。错误处理之所以复杂在于我们在出错的时候要尽可能回复到先前的状态,比如一个多线程服务程序,当一个新的请求因为资源有限而不能启动线程提供服务时,其他的正在运行的服务线程不应该收到任何的影响。C用来克服这些问题的手段有goto这样的局部跳转以及long jump这样的全局跳转。经验证明这些构造都不好使用(副作用大,难以组合等)。大家都知道,C++提供了异常。配合上述的资源管理,二者真实双剑合壁,谁与争锋。那些提起异常就考虑性能的可能一直都不明白,异常构造究竟是用来干什么的!
f. 风格和组织。C和C++都是自由风格代码,所有差别不大。但是我们有合适的编码标准。使用一致的编码标准,可以强化代码阅读人员的阅读习惯,减少歧义,提高精神欢愉程度而提升工作效率。这个C和C++打个平手。
g. 问题分解。复杂的问题必须首先分解之,然后各个击破。这也是团队合作的基础和前提。做这个目前所知的最好的工具就是OO。C++提供了对OO的完整支持,这就使得C++这个领域得心应手,左右逢源。GoF在Design Pattern中说,选择支持面向对象的语言的原因之一就是不想论述诸如继承,运行时多态这样的模式,尽管这样的模式在C的实现中非常普遍。C++把C中太常用的模式标准化了。
h. 静态类型检查。强类型之所以有用,是因为我们需要编译器帮助我们检查可能在实现引入的bug。C也是强类型的,但是支持的不够(想象全能的void*)。C++通过引入类型安全的库支持以及细化了类型查遍而强化了强类型检查,同时,是用有效的泛型手段,在加强了静态类型检查的同时还不需要额外的代码。
i. 有效的封装可以大大降低问题的复杂性。人人都知道,Windows平台上CreateFile是一个重要的函数,但是谁能在不查看文档的情绪下写好这个API的调用?那么istream呢?这个例子形象地说明了封装的极其重要而且C++善于与此。从系统API和标准库流差距十万八千里,其中你可以发挥余地的地方很多,比如创建NT sparse file就必须使用系统API,但是如果你每次都需要查文档,那么那还不是合格的程序员,更不要说是合格的C++程序员了。所以这里就显示出了封装的重要性。我评价一个C++程序是不是成熟的标准之一就是有没有自己的C++封装库。封装不仅仅积累类的知识,还有效减少了名字的数量。一个系统API往往需要多个参数,使用C++的缺省参数机制以及设计专用类,可以有效降低程序员的记忆负担。而且,除非重构了代码,你都不需要做重复的测试!
这样的条目还可以列出一些。基本上,任何一个C++相对C的新特性,都可以找到对应的工业最佳实践。
2。C++和性能问题。
C++程序员在更新的系统语言之前有时候不够自信,原因是C++太复杂。但是他们还有一根救命稻草,那就是性能。可惜的是,C++从来不是靠着宣称自己性能如何而获得目前的地位的。下面的叙述属于常识。对性能问题有一般理解的都可以直接跳过不读。这是为C++新手准备的,只要是为了打破性能迷思,重归学习C++的康庄大道。
a. 常识之一:性能是设计出来的,不是实现出来的。这句话的意思不是说糟糕的实现也能实现优良的性能,而是说,再好的实现,包括语言的选择,导致的性能改善都不如设计时对性能的考虑。现实的情况是,真正性能攸关的部分,可能早就在设计之初做过评估并且有了原型实现。实现时选择语言的机会非常有限。所以最后的结论就是C++在项目中被选中的绝大部分原因并不是C++的性能有多好。
b. 常识之二:未成熟的优化是罪恶之源。这句话的意思表述的和上一个有点相似,但是是从另一个侧面。那就是,除非做过性能瓶颈的测试(profiling),永远不要直观猜测性能的瓶颈,否则你可能花大量时间优化了不需要优化的地方。真正的性能瓶颈一般只存在与非常有限的几个分散地方,程序员对此估计的不准确性是人所共知的。
c. 常识之三:对于真正的性能问题,数据结构和算法的选择是关键。这需要多性能场景做数据流分析才能确定使用什么算法或者动态调整算法。C++非常关注性能问题,所以有基于树的map,也有机遇散列的map;有支持前端后端快速插入的容器,也有可以再中间快速插入的容器,还有可以根据迭代器类型自动选择二分查找还是顺序查找的自适应的标准库。这些,保证了一般C++代码的性能不会低于同等质量的任何其他代码。
d. 常识之四:在性能成为为题之前,它不是问题。这个有点绕。其基本含义是,对于绝大部分的C++实现者,也就是我们一般所说的C++程序员或者工程师,性能可以是最后一个考虑的因子。在此之前,很多更重要的事情需要关注,如编码风格,函数封装,基本类的设计以及C++基本构造的有效和合理使用。
e. 常识之五:C++不靠性能取胜,学习使用C++也并不复杂!这可以让C++工程师在C#和java程序员前挺直胸膛。C++解决的问题是偏向于底层的,复杂的系统及的问题,它的成功是靠着有效的复杂性控制机制以及与系统平台的天然兼容。而且,它正变得越来越易用!
3. C++和C
C++和C血脉相连,不可分割。二者不是竞争关系,而是相辅相成的,这种C和C++标准的相互借鉴就可以看出来。C++不是唯一的从C派生出来的现代多模式语言,但是C++是唯一成功的。下面就所谓的C和C++的复杂性做一简单讨论。希望可以给那些正在学些这些的朋友们一点帮助。
从C++的角度而言,C没有什么复杂性,一切都简单透明。C的复杂性来源于其基本特性的组合,比如数组,指针,函数指针的组合。在稍微复杂一点的应用中,它们的组合不可避免。另一方面,由于C处理的问题大部分在系统编程领域,系统编程有好多特性的约定,也导致C奇怪的使用。这应该属于域知识,并不是C的一部分。好多自动化问题的解决方案都是以C作为中间代码,依笔者有限的经验,这个可以说是相当的容易。
C中最复杂的毫无疑问是指针。但是复杂的并不是指针本身,而是指针可以做的工作太多太多,以及C语言中对指针操作的任意支持。一般来说,《C专家编程》是任何C背景程序员的必读数目。另一方面,阅读以下C规范,理解什么是标准明确定义的行为,这样就不至于局限于编译器的实现。当然使用多编译器是最好的,可以理解C的实现之差别。关注C语言构造背后的东西(rationale)而不是语言细节,并且熟悉C标准库。
C++,给人的第一影响就是复杂,复杂,复杂。但是,对于满汉全席,您会嫌盘子多吗?C++之所以又复杂的语言构造部分原因是它要解决的问题域极其宽广,部分原因是历史造成了。好消息是C++正在变的易于使用而且C++入门书籍也愈来愈好;不幸的是C++还没有完全达到人因工程要求的好的程度(易于写正确的代码,而不易于犯错)。我们学习C++的目的是要使用它,就像买个电视是为了看CCTV,而不是研究电视的工作原理一样。
4. 总结。
够长的了。上文中提到并且批评了一些错误的理解和思想,希望那个不至于误会为针对个人。作为一个使用C++的程序员,我的大部分日常代码都是使用perl写的,包括生成C++代码的时候。当有人责难为什么C++0x要引入regex的时候,我为此欢呼,因为它确实有用而,实现良好,而且与perl全兼容:)
本文的主要目的不是抛砖引玉,而是作为期望作为一个可以代言那部分没有发言的C++程序员,提供可以让C++新生力量少走弯路的一点指南。其中除了错误,其他所有都来于这个虚拟的社区。
————————————————
版权声明:本文为CSDN博主「bfzhao」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/bfzhao/article/details/4673087
第二部分
在上一部分中,我花了大量的精力强烈暗示使得C++如此成功的并不是由于所谓性能上的优势,而是其规范并升华了C的最佳实践。现在我要用同样的精力来证明C++在性能上的专门考虑和设计以及实践中的优越表现。C++根植与C的肥沃土壤,天然具有C所拥有的直接与物理硬件交互的优势。更要提到的是,每一个C++新特性的引入,无不是经过了详细的性能权衡。毫不奇怪,C++在绝大多数的情况下可以取得与C比肩的性能,并且有所超过。
性能的最终瓶颈必然取决于硬件能力。这样,优化性能的途径不外乎两点:
1. 充分发挥硬件的优势。
2. 不做无用功。
在第一点上,C和C++不分伯仲。原因是,编译器后端使用的往往一个完整支持的CPU集合的大部分公共指令,这就意味着某种可以用于特定CPU的特别优化指令根本就不可能出现在编译后的二进制代码中。这同时也就说明了为什么需要内联的汇编,而C和C++都完美支持这个特性。另一方面,C和C++编译器一般使用同样的后端,这样更模糊了C和C++的界限。同样有效的C代码,使用C++编译器和C编译器产生的最终结果基本完全一致,甚至更好,后面有详述。
第二点看起来非常简单,但是由于做无用功的可能性成千上万,想做到不做无用功也就比初看起来的要困难的多。我们先简单分析一下这是为什么。第一,现代基于模块的编程实践非常强调封装,这基本上都是以一定的性能损失为代价的。对于编译器来说,那就是,你的代码不可能超出编译器和其基本库的实现限制;第二,选择合适的算法有时候很难,这决定了实现的性能表现。本质上,就是使用更好的避免的做无用功的方法。使用简单的排序算法说明。冒泡算法之所有低效,是因为尽管我们通过比较知道a>b,b>c,但是我们还需要比较a和c的关系,其实这完全是浪费。快速排序则不同,它使用一个值来吧整个排序集合分为两部分,大于它的和小与它的。这样,这两个集合中的任何元素都不必在进行比较,这就是不做无用功。仔细思考一下其他的常用算法,你就可以很清晰地认识到都是这样。这就是我们一直强调算法是性能的关键的原因,因为标准算法都是经过详细设计的,实践证明正确的,做无用功最少的代码抽象。第三,无论从何种尺度,避免重复都要比预期的要困难。重复可能表现为:数据重复(同样程序状态被保存在多个位置,它们之间需要更新和同步),执行重复(由于诸如缓存频繁失败而导致的低性能代码被重复调用),结构重复(同样的代码被重复执行而具有相同的副作用)等,这仅仅是运行态的一般情况。实现的重复则会导致的执行体体积庞大,引用冗余,在导致性能问题的同时引出维护的灾难。
其实这些都说明,在相同的层面上,性能其实和你选择工具语言没有什么直接的关系。C和C++无疑是在相同的层面上的。我们已经知道,C++提供了现代的构造,满足超大规模的设计和实现的基本要求。但是,C++是怎么在提升代码可管理性的基础上,可以保证与C基本一致(差别小于5%)的性能的呢?
1. 使用(更)强的类型。C++和C都是强类型的语言,但是C++做的更好。强类型带来的好处是,类型判断和检查可以在编译期执行,而不是运行时。这就意味着目标代码的体积更小,逻辑更简单。
2. 需要时再计算(延迟求值)。这是最常用的常规性能优化方法。程序的一致执行会话中,可能某些代码段并不会被执行到,那么这些代码所需要的数据就没有必要产生或者计算。mutable和const关键字提供了基本的支持。
3. 需要时才声明对象。这样可以避免当条件不满足时构造和销毁对象的开销,同时保证的最小的对象作用域。
4. 避免没必要的对象复制。返回对象的函数调用一般都会产生一个临时对象,当函数调用完毕之后,该临时对象就会被销毁。这虽然完全符合C++ 的对象语义,但是是低效的。一般的C++编译器都会执行返回值优化来消除这个额外的对象构造。从C++语言来说,这样的临时变量其实可以绑定到一个常量引用,在该常量引用失效之前,这个零时对象一直存在。然而,仍然存在着一些场景,会不可避免地产生中间对象的而导致的低效,C++0x通过引入右值引用来试图消除这个最后的可能性。
5. 基于模板的标准程序库和算法。这是C++中最令人叫绝的地方。它是执行效率和易用性的完美折中。和C基于值语义类似,所用的标准容器都保存一份数据或者数据指针的拷贝。模板在避免了大量的代码重复的同时,通过完全媲美手写算法的性能,这就是C++语言。
C++在性能问题上被人诟病,除了明显的无知,还有一些深层次的原因。那就是对C++特性的滥用。这里讨论几个最常听到的arguments:
a. C++的虚函数机制。可能你常常看到有人煞有介事地评论调用一个虚函数会导致一直额外的查表操作,这个是如何导致了他的代码的性能低下。事实是:除非他的代码根本不需要运行时多态而滥用了虚拟机制,否则C++编译器的实现基本上可以说是最直接且高效的,比如使用散列来索引函数的地址。恰当的比喻是,你要切牛肉,非要使用关老爷的青龙偃月,还要抱怨刀锋划了案板。
b. 异常机制。异常恐怕是C++里最没有被广泛使用的最好的特性了。由于异常要求非常不同的程序观念,有一些公司甚至如洪水猛兽般避之不及。不幸的是,C++标准库也使用了异常,那就意味着C++标准库最好也就不要碰了。异常导致的问题主要是代码执行管理和软件架构中错误处理策略的更本性变革,但是从来不是因为其性能问题。是的,异常的抛出和捕获会导致低性能。但是真正需要异常发挥作用的地方可能只有程序执行逻辑的1%。形象的比喻是,如果过年吃一顿好的也被认为是浪费,那就是不辨是非了。
c. 对象的自动构造,析构和复制。复制我们不说,因为C也支持数据结构(struct)的直接赋值。对象的自动构造和析构之所以必要,因为在某些情况下,我们需要。当我们不需要的时候,它们完全可以是没有的。如果可以理解C中要求变量声明后必须赋初值的最佳实践,也就可以明白C++怎么把这样的最佳时间制度化了。
该是那就老话,性能是设计出来的,不是实现出来的。有关C++性能的讨论可以休矣!
————————————————
版权声明:本文为CSDN博主「bfzhao」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/bfzhao/article/details/4700653