C++可算是一种声名在外的编程语言了。这个名声有好有坏。从好的方面讲,C++性能非常好,哪个编程语言性能好的话总忍不住要跟C++来单挑一下。从坏的方面讲,它是臭名昭著的复杂、难学、难用。
不管说C++是好还是坏,不可否认的是,C++仍然是一门非常流行且非常具有活力的语言。继沉寂了十多年后发布语言标准的第二版——C++11——之后,C++以每三年一版的频度发布着新的语言标准,每一版都在基本保留向后兼容性的同时提供着改进和新功能。
虽然在语言领域,也有Rust这样的新语言在向C++发起挑战,但是,不可否认的是,C++仍然是面向性能的领域里的编程语言王者。我甚至不认为C++在性能方面次于C——在极致追求速度时,C++可以比C更强,而C相比C++的主要优点是更加简单:不管是学习、使用,还是产生的二进制代码的体积上。
今天,我们就来大略讨论一下,C++是如何做到高性能的。
跟C语言一样,C++提供非常底层的数据操作能力,为开发者提供了灵活性。跟“高级”语言一样,C++提供了强大的抽象能力(可以说超越了大部分语言)。而且,相比C,C++要安全得多。在语言诞生的初期就是如此,现在就更不用说了。
C++的类型系统比C更加严格,因此虽然一直有C++是C的超集的说法,这个说法严格来说从来就没成立过。最近(2023年)碰到过一个程序崩溃的案例,简化来讲,就是开发者使用了一个char的二维数组(charnames[MAX_NAMES][MAX_NAME_LEN]),然后把它传给了一个接收char**参数的函数……这代码当然是错的,但C编译器虽然给了个告警,但编译还是没有失败。如果这是C++代码的话,那编译器就会直接报告错误,不给通过了。
而第二点,零开销抽象,对于C++的性能至关重要。我们有很多的抽象机制,同时,使用这些抽象机制并不会带来额外的开销。在某些情况下,使用这些机制,反而有“负开销”——“使用者”可以非常安全地使用这门语言,即可获得极高的性能。同时,C++还给予了“定制者”根据自己的需求来写出更贴近使用场景的库的能力,可以进一步方便“使用者”。
当然,定制对程序员的技能有非常高的要求。初学C++的更需要掌握C++的标准库的使用——用好标准库,就能获得非常不错的性能。正如高德纳大神的名言的完整版:
就在同一篇论文的同一页上,高德纳还写下了:
在成熟的学科里,对于12%的提升,如果易于获取的话,那绝不会被认为是微不足道;我相信,在软件工程里,相同的观点也会占上风。
而C++已经提供相当多的机制,可以允许我们很容易地获取高性能,在很多场景下远远超过高德纳所说的12%。
我经常举的一个例子是C++标准库的sort和C标准库的qsort:在关闭优化时,我在某一测试场景下得到了1:2.5的性能差异,C++似乎要慢不少;但一旦打开-O2(允许内联)时,两者的性能差异突变成3.5:1,C++的性能比C高出了好几倍!这就是所谓的“负开销”了。C++的代码比C的更简单、更直观,性能还更高。原因自然就是C++的函数对象和模板机制允许编译器更好地进行内联,从而产生更加高性能的代码。
任何情况下学习C++,第一需要了解的就是析构函数和RAII(resourceacquisitionisinitialization)惯用法。对,虽然C++诞生时名字是“带类的C”,但类和面向对象并不等同,对面向对象编程的支持并不是C++的最重要特性。C++的自定义类型的最特别之处不在多态,而在对其行为的定制上——最重要的就是对象销毁时应该做些什么。析构函数和析构函数带来的RAII惯用法,是C++里最重要的特性,也是用C++进行资源管理的关键。
重载是另外一个非常重要的C++特性。除了你不用在名字上区分process_char、process_string、process_int带来的方便性外,它对泛型编程也很重要,还对现代C++的一个基本特性“移动语义”非常重要。刨除语法上的细节,本质上来说,移动语义就是让程序员可以方便地区分会继续使用的对象和以后不再使用的对象,允许对后者使用构造函数和赋值运算符的重载来“窃取”其中的资源。对于一个普通的vector,拷贝的开销是O(n)或更高(如果vector成员是容器或其他具有高拷贝开销的对象),但移动开销通常(是,只是通常;不过通常你也不会遇到这种例外的特殊情况)是O(1),常数复杂度。这就是我们在C++里高效传递对象的一种常见方式了。
C++标准库里最常用的组件恐怕就是string和各种容器了。它们都对移动进行了优化。当然,除了这个基本的性能点外,容器都有各自的特殊性能点,比如不同情况下的插入性能差异。这些都是需要学习的地方。
前面我们已经提到过模板,而string和容器也都是模板,行为可以通过模板参数来进行定制,并允许高效的内联优化。模板当然是C++里比较复杂的一个地方,但基本的使用则相当简单:vector就是一个放int的vector,用起来跟一个普通的类没有区别——只是模板创建者的工作简单多了,不需要手工为不同的类型创建不同的类。
用好C++、在项目中获得令人满意的性能当然不止上面这一些。最基本的,我们还需要了解标准库算法,并合适地使用并发和并行来充分利用硬件。在本文中我们暂且就不展开了。
当我们用熟了C++之后,慢慢地,我们就会不再满足于C++标准库这一“制式武器”。我们会寻找适合自己的第三方库,甚至自己造轮子来满足项目的特定需求。此时,我们就需要进一步了解C++的高级特性。我们需要了解模板的进一步细节,尤其是特化。我们需要了解SFINAE和模板元编程。我们需要了解constexpr和它带来更方便的编译期编程。C++的使用者也许可以暂时不关心这些问题,但定制者,或者说项目里的框架搭建者和工具提供者,必须去了解C++的这些高级特性,为你的项目提供扎实的基础。
这种情况下,最合理的选择是使用某种intrusive_list,侵入式的链表,不需要在每次插入或删除时进行内存管理。C++标准库没有提供这个功能。你可以使用Boost里提供的容器,或者自己写一个新的。对于这个例子,Boost多半就足够好了。但总可能出现一些现成库解决不了的问题的,这时候,利用C++的高级特性来自己造轮子就是一件非常自然的事。我们可以做到既有合适的定制,同时用法又跟已有的容器相似,没有额外的学习成本。
或者,也许你希望使用分配器来创建一个容器内存池,来提供对内存的使用效率。这在C++里也是非常容易完成的,只要你了解合适的定制机制。根据洋葱原则,你可以不管这些定制点,直接用C++,这样最简单;也可以把标准库“切开”,以自己最喜欢的方式来拼接定制使用——当然,这种做法确实跟切洋葱一样,很容易就会哭鼻子的。但它确实能帮助你获得最高的可能性能
课程介绍
课程收获
名企好评
吴咏炜老师的《C++性能优化高端培训》课程是Boolan技术赋能培训的品牌课程,在华为、博世西门子、银科、大疆等很多著名企业内训都获得高度认可,得到参训学员一致好评。
该课程将于本周六(1月20日)正十开课,现在扫描下方二维码,都有机会领取C++之父的《C++白皮书中文版》