├── .gitignore ├── 01-Introduction └── 01-简介.ipynb ├── 02-ChoosingTheOptimalPlatform └── 02-选择最优平台.ipynb ├── 03-FindingTheBiggestTimeConsumers └── 03-找到最大的时间消费者.ipynb ├── 04-PerformanceAndUsability └── 04-性能与易用性.ipynb ├── 05-ChoosingTheOptimalAlgorithm └── 05-选择最优算法.ipynb ├── 06-DevelopmentProcess └── 06-开发流程.ipynb ├── 07-TheEfficiencyOfDifferentC++Constructs ├── 7-不同的C++构建体的效率.ipynb ├── 7.01-不同种类的变量存储.ipynb ├── 7.02-整型变量和操作符.ipynb ├── 7.03-浮点变量和操作符.ipynb ├── 7.04-7.05.ipynb ├── 7.06-指针和引用.ipynb ├── 7.07-7.09.ipynb ├── 7.10-数组.ipynb ├── 7.11-类型转换.ipynb ├── 7.12-分支和switch语句.ipynb ├── 7.13-循环.ipynb ├── 7.14-7.18-函数相关.ipynb ├── 7.19-7.25-结构与类相关.ipynb ├── 7.26-7.29.ipynb ├── 7.30-模板.ipynb ├── 7.31-线程.ipynb ├── 7.32-异常和错误处理.ipynb └── 7.33-7.36.ipynb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .ipynb_checkpoints 3 | -------------------------------------------------------------------------------- /01-Introduction/01-简介.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 简介\n", 8 | "该手册是写给高级程序员,以及那些想让软件运行得更快的开发者们。手册假定读者具备良好的C++编程知识,并对编译器的工作方式具有一定的了解。C++语言被选择来作为手册的基础,原因在稍后(原文第8页)给出。\n", 9 | "\n", 10 | "手册主要基于作者本人对编译器和微处理器方面的研究工作。给出的建议是基于Intel, AMD and VIA的微处理器的x86系列,包括64位版本。x86处理器广泛应用于indows, Linux, BSD and Mac OS X操作系统。当然这些操作系统也可以基于其它的处理器。这里很多建议也适用于其它平台,其它编程语言。" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "本手册是五大技术手册系列的第一本。分别是:\n", 18 | "1. Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms.\n", 19 | "- Optimizing subroutines in assembly language: An optimization guide for x86 platforms.\n", 20 | "- The microarchitecture of Intel, AMD and VIA CPUs: An optimization guide for assembly programmers and compiler makers.\n", 21 | "- Instruction tables: Lists of instruction latencies, throughputs and micro-operation breakdowns for Intel, AMD and VIA CPUs.\n", 22 | "- Calling conventions for different C++ compilers and operating systems.\n", 23 | "\n", 24 | "这些手册的最终版本可以在www.agner.org/optimize 访问到。版权条款见168页。" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "如果只需要使用高级语言来写软件,只需要阅读本手册。其它手册是为那些更进一步深入技术细节的人提供。这些技术包括\n", 32 | "- 指令时序\n", 33 | "- 汇编语言编程\n", 34 | "- 编译器技术\n", 35 | "- 微处理器之微架构\n", 36 | "\n", 37 | "如要再进一步的优化,对CPU密集型代码,可以使用汇编语言进行优化。后续的手册里有描述。\n", 38 | "\n", 39 | "请注意我的优化手册数千人使用。单单我一个人肯定没有时间回答所有人的问题。请不要向我提问程序设计问题,否则你不会收到回复的。建议初学者们先到其它地方寻求信息,获得较好的程序设计经验之后,再来尝试此手册中的技巧。如果你不能在相关的书籍和手册中找到答案,互联网上有很多论坛可以得到编程问题的解答。\n", 40 | "\n", 41 | "在此我想要感谢给我提供对本手册的纠错和建议的许多人。我也一直很欢迎读者提供此类信息。" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## 1.1 优化的代价\n", 49 | "\n", 50 | "大学里的计算机编程课程一般强调这些问题的重要性:\n", 51 | "- 数据结构和面向对象编程\n", 52 | "- 模块化\n", 53 | "- 重用性\n", 54 | "- 软件开发流程的系统化\n", 55 | "\n", 56 | "这些需求往往和优化软件的速度和大小的需求相冲突。\n", 57 | "\n", 58 | "今天,软件老师常常建议,函数或者方法不该超过太多行。然而,数十年前,建议刚刚相反:如果代码段只被调用一次,不要创建独立的子程序。这种软件编写风格转变的原因是:软件项目已经变得日趋庞大和复杂,关注的焦点切换到了软件开发的代价上,并且计算机也越来越强大。\n", 59 | "\n", 60 | "在选择编程语言和接口框架是,人们首选关注的是结构化的软件开发,程序运行效率不是那么受关注了。然而,这常常对最终用户是不利的。他们需要投资购入更强悍的计算机,才能跟上日渐臃肿的软件的需求,而且往往任然为难以接受的响应时间感到沮丧,有时哪怕是运行简单的任务。\n", 61 | "\n", 62 | "有时候,必须要对先进的软件开发原则作出折中,以使软件包更快,更小。本手册讨论如何在这些考虑之间做出合理的平衡。手册将\n", 63 | "- 讨论如何识别出程序中最为关键的部分,并隔离出来,再集中精力优化。\n", 64 | "- 讨论如何克服如下原始的编程风格的风险:不自动检查数组越界,无效指针,等等。\n", 65 | "- 讨论以执行时间来衡量,哪些高级的编程结构是昂贵的,哪些是低成本的\n" 66 | ] 67 | } 68 | ], 69 | "metadata": { 70 | "kernelspec": { 71 | "display_name": "C++17", 72 | "language": "C++", 73 | "name": "cling-cpp17" 74 | }, 75 | "language_info": { 76 | "codemirror_mode": "c++", 77 | "file_extension": ".c++", 78 | "mimetype": "text/x-c++src", 79 | "name": "c++" 80 | } 81 | }, 82 | "nbformat": 4, 83 | "nbformat_minor": 2 84 | } 85 | -------------------------------------------------------------------------------- /02-ChoosingTheOptimalPlatform/02-选择最优平台.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 2 选择最优平台\n", 8 | "\n", 9 | "## 2.1 硬件平台的选择\n", 10 | "\n", 11 | "硬件平台的选择已经变得不如以前重要了。RISK 和 CISC 处理器之间的差异,PC 和 大型机之间的差异,简单处理器和向量处理器之间的差异,都越来越变得模糊。采用 CISC 指令集的标准 PC 处理器也包含了 RISC 核心,向量处理指令集,多核心。处理速度已经超越了昨天的大型机。\n", 12 | "\n", 13 | "当今,选择硬件平台更多要考虑的是:价格,兼容性,第二来源,以及是否有良好的开发工具,而较少考虑处理器能力。多台 PC 组网可能更便宜,也更有效,相对于一台大型机。具有大规模并行矢量处理能力的大型超级计算机在科学计算领域仍然占有一席之地,但对于对于大多数用途来说,标准PC处理器是优选的,因为它们具有优越的性价比。\n", 14 | "\n", 15 | "从技术角度来看,标准PC处理器的CISC指令集(称为x86)并不是最优的。这个指令集是为了向后兼容,可以追溯到1980年左右。在那是RAM内存和磁盘空间都是稀缺资源。然而,CISC指令集比它的声誉要好。CPU缓存是有限的资源,指令代码的紧凑性使今天的缓存操作更加高效。在代码缓存至关重要的情况下,CISC指令集实际上可能比RISC更好。x86指令集最糟糕的问题是寄存器太少。在x86指令集的64位扩展中,此问题得到了缓解,寄存器数量增加一倍。\n", 16 | "\n", 17 | "不推荐在重要应用程序中采用依赖网络资源的瘦客户机,因为网络资源的响应时间难以控制,不能得到保证。\n", 18 | "\n", 19 | "小型手持设备越来越流行,并且越来越多地用于以前需要PC的电子邮件和网页浏览等用途。同样,我们看到越来越多的嵌入式微控制器的设备和机器。我没有就哪些平台和操作系统对这些应用程序最有效提出具体建议。但重要的是,要认识到这些设备通常比PC具有更少的内存和计算能力。因此,节省资源在这些系统上的使用比在PC平台上节省更为重要。但是,通过优化的软件设计,即使在这种小型设备上,也可以为许多应用获得良好的性能,如第162页所述。\n", 20 | "\n", 21 | "本手册基于采用Intel,AMD或VIA处理器的标准PC平台,以及以32位或64位模式运行的Windows,Linux,BSD或Mac操作系统。这里给出的许多建议也可能适用于其他平台,但这些示例仅在PC平台上进行了测试。" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "### 图形加速器\n", 29 | "\n", 30 | "平台的选择显然受到有关任务要求的影响。例如,重型图形应用程序最好在具有图形协处理器或图形加速卡的平台上实现。有些系统还有一个专用物理处理器,用于计算电脑游戏或动画中物体的物理运动。\n", 31 | "\n", 32 | "在某些情况下,可以将图形加速卡上处理器的高处理能力用于其他目的,而不是在屏幕上呈现图形。但是,这样的应用程序是高度依赖于系统的。因此,如果可移植性很重要,则不推荐使用。本手册不包括图形处理器相关内容。" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "### 可编程逻辑器件\n", 40 | "\n", 41 | "可编程逻辑器件是一种可以用硬件定义语言来编程的芯片。语言有 VHDL 和 Verilog。通用器件是CPLD和FPGA。\n", 42 | "软件开发语言(如C++),和硬件定义语言的差异在于:\n", 43 | "- 软件编程语言:定义了顺序指令的算法\n", 44 | "- 硬件定义语言:定义了硬件电路,包括数字基本模块,如门,触发器,多路复用器,算术单元等,以及连接它们的线路\n", 45 | "\n", 46 | "硬件定义语言本质上是并行的,因为它定义了电路连接,而不是操作序列。\n", 47 | "\n", 48 | "复杂的数字操作通常可以在可编程逻辑器件中比在微处理器中更快地执行,因为硬件可以针对特定用途进行排布。在FPGA中实现微处理器也是有可能的,这就是所谓的软处理器。这种软处理器比专用微处理器慢得多,因此不具有优势。但是在某些情况下,使用软处理器处理关键核心应用程序特定指令,这种解决方案中,在同一芯片中以硬件定义语言编码,可能是一种非常有效的解决方案。更强大的解决方案是将专用微处理器内核与FPGA集成在同一芯片中。这种混合解决方案现在用于某些嵌入式系统。\n", 49 | "\n", 50 | "我猜想,类似的解决方案有一天可能在PC处理器中实现。此类应用程序将能结合应用,使用硬件定义语言,定义专用指令。除了代码缓存和数据缓存之外,这种处理器还会为硬件定义代码提供额外的缓存。" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## 2.2 选择微处理器\n", 58 | "\n", 59 | "由于竞争激烈,微处理器竞争品牌的基准性能非常接近。具有多个内核的处理器更有优势,使得应用程序可以多线程并行运行。具有低功耗的小型轻量级处理器实际上非常强大,可能足以满足计算不大密集的应用需求。\n", 60 | "\n", 61 | "一些系统具有图形处理单元,或者在显卡上,或集成在CPU芯片中。这些单元可以用作协处理器来处理一些重要的图形计算。在某些情况下,可以将图形处理单元的计算能力用于图形处理之外的其他目的。一些系统还有一个物理处理单元,用于计算电脑游戏中物体的移动。这样的协处理器也可以用于其他目的。协处理器的使用超出了本手册的范围。\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## 2.3 选择操作系统\n", 69 | "\n", 70 | "x86系列中的所有新型微处理器都可以运行在16位,32位和64位模式下。\n", 71 | "\n", 72 | "在旧的操作系统DOS和Windows 3.x中使用16位模式。如果程序或数据的大小超过64k字节,这些系统使用内存分段。这是非常低效的。现代微处理器没有针对16位模式进行优化,某些操作系统不能向前兼容16位程序。除了小型嵌入式系统外,不建议开发16位程序。\n", 73 | "\n", 74 | "今天(2013年),32位和64位操作系统都很常见,系统之间的性能没有太大差异。64位软件没有大量的市场推广,但可以肯定的是,64位系统将在未来占统治地位。\n", 75 | "\n", 76 | "对于有很多函数调用的CPU密集型应用程序,64位系统可以将性能提高5-10%。如果瓶颈在别处,那么在32位和64位系统之间的性能没有区别。使用大量内存的应用程序将受益于64位系统的更大地址空间。软件开发人员可以为吃内存的软件,选择生成两种版本。为了与现有系统兼容而使用32位版本,为了获得最佳性能,使用64位版本。\n", 77 | "\n", 78 | "Windows和Linux操作系统为32位软件提供几乎相同的性能,因为两个操作系统使用相同的函数调用约定。几乎所有与软件优化相关的方方面面,FreeBSD和Open BSD都与Linux相同。这里所说的关于Linux的所有内容也适用于BSD系统。\n", 79 | "\n", 80 | "基于英特尔的Mac OS X操作系统是基于BSD的,但编译器默认使用与位置无关的代码和延迟绑定,这使其效率较低。可以使用下面方法提高性能:\n", 81 | "- 通过使用静态链接\n", 82 | "- 不使用位置无关的代码(选项-fno-pic)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "64位系统与32位系统相比有如下优点:\n", 90 | "\n", 91 | "- 寄存器的数量增加了一倍。 这使得可以将中间数据和局部变量存储在寄存器中而不是内存中。\n", 92 | "- 函数参数在寄存器中而不是在堆栈中传输。这使得函数调用更有效率。\n", 93 | "- 整数寄存器的大小扩展到64位。这只是可以利用64位整数的应用程序的一个优势。\n", 94 | "- 大内存块的分配和释放更高效。\n", 95 | "- 所有64位CPU和操作系统都支持SSE2指令集。\n", 96 | "- 64位指令集支持数据的自相关寻址。这使得与位置无关的代码更有效率。\n", 97 | "\n", 98 | "与32位系统相比,64位系统具有以下缺点:\n", 99 | "- 指针,引用和堆栈条目使用64位而不是32位。这使数据缓存效率降低。\n", 100 | "- 如果内存映像基址不能保证小于2^31,访问静态或全局数组需要一些额外的指令,用于64位模式下的地址计算。这种额外的开销在64位Windows和Mac程序中可见,但在Linux中很少见。\n", 101 | "- 在代码和数据的组合大小可能超过2GB的大内存模型中,地址计算更复杂。尽管这种大容量内存模型几乎很少使用。\n", 102 | "- 某些指令在64位模式下比在32位模式下长一个字节。\n", 103 | "- 一些64位编译器不如32位的相应版本。" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "一般来说,你可以期望64位程序的运行速度比32位程序快一点,\n", 111 | "- 如果有很多函数调用\n", 112 | "- 如果有大量内存块的分配\n", 113 | "- 或者程序可以利用64位整数计算。\n", 114 | "\n", 115 | "如果程序使用超过2千兆字节的数据,则必须使用64位系统。\n", 116 | "\n", 117 | "在64位模式下运行时,不同的操作系统有所差异,这是因为函数调用约定的不同。\n", 118 | "- 64位Windows只允许在寄存器中传输四个函数参数,而64位Linux,BSD和Mac允许在寄存器中传输多达十四个参数(6个整数和8个浮点)。\n", 119 | "- 还有一些其他细节使64位Linux中的函数调用效率高于64位Windows(请参阅第50页和手册5:“针对不同C ++编译器和操作系统的调用约定”)。\n", 120 | "\n", 121 | "在64位Linux中,函数调用多的应用程序的运行速度可能比在64位Windows中稍快。64位Windows的缺点可以通过下面方法缓解:\n", 122 | "- 关键函数内联或静态\n", 123 | "- 使用可以完成整个程序优化的编译器" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "## 2.4 选择编程语言\n", 131 | "\n", 132 | "在开始一个新的软件项目之前,决定哪种编程语言最适合手头的项目,这非常重要。低级语言对于优化执行速度或程序大小非常有用,而高级语言则适用于开发清晰,结构良好的代码,以及快速简便地开发用户界面和网络资源,数据库等的接口。\n", 133 | "\n", 134 | "最终应用程序的效率取决于编程语言的实现方式。当代码被编译并分发为二进制可执行代码时,获得最高效率。 C ++,Pascal和Fortran的大多数实现都基于编译器。\n", 135 | "\n", 136 | "一些其它编程语言都通过解释来实现。程序代码按原样分发,并在运行时逐行解释。例子包括JavaScript,PHP,ASP和UNIX shell脚本。解释代码的效率非常低,因为循环里的主体代码,会在每次迭代中被一次又一次地解释。\n", 137 | "\n", 138 | "一些实现使用即时编译(JIT)。 程序代码按原样分发和存储,并在执行时进行编译。 Perl就是一个例子。\n", 139 | "\n", 140 | "几种现代编程语言使用中间代码(字节代码)。 源代码被编译为中间代码,然后被分发。中间代码不能直接被执行,必须经过第二步解释或编译才能运行。 Java的某些实现是基于解释器的,该解释器通过模拟所谓的Java虚拟机来解释中间代码。最好的Java机器使用JIT来编译最常用的中间代码。 C#,托管C++和Microsoft .NET框架中的其他语言都基于中间代码的即时编译技术。\n", 141 | "\n", 142 | "使用中间代码的原因是平台无关,并且紧凑。使用中间代码的最大缺点是用户必须安装大型运行时框架来解释或编译中间代码。这个框架通常比代码本身使用更多的资源。\n", 143 | "\n", 144 | "中间代码的另一个缺点是它增加了一个额外的抽象层次,这使得详细的优化更加困难。另一方面,即时编译器可以专门针对正在运行的CPU进行优化,而在预编译的代码中进行针对CPU特定的优化则更为复杂。\n", 145 | "\n", 146 | "编程语言及其实现的历史揭示了一个曲折的过程,过程中充满了效率,平台独立性和易开发性的冲突考虑。 例如,第一台个人电脑有一个Basic的解释器。Basic的编译器很快就问世了,因为Basic的解释版本太慢了。今天,Basic的最流行版本是Visual Basic .NET,它使用中间代码和即时编译来实现。一些Pascal的早期实现就使用了中间代码,类似于今天Java中所使用的。 但是,当真正的编译器问世后,这种语言才真正的开始大大受欢迎。" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "从讨论中我们可以清楚的看到,编程语言的选择是基于效率,可移植性和开发时间之间的折中。当效率很重要时,解释型语言就不再被考虑了。基于中间代码和即时编译的语言,在可移植性和开发的简易性比速度更重要时,可能是一种可行的折衷方案。这包括诸如C#,Visual Basic .NET和Java等语言。但是,这些语言有一个非常大的运行时框架,每次程序运行时都必须加载它。加载框架和编译程序花费的时间通常远远多于执行程序所花费的时间,运行时框架在运行时,可能比程序本身使用更多的资源。使用这种框架的程序有时对于按下按钮或移动鼠标这样的简单任务来说,响应时间无法接受。 速度至关重要时,绝对应该避免使用.NET框架。\n", 154 | "\n", 155 | "毫无疑问,使用完全编译的代码可以获得最快的执行速度。编译语言包括C,C++,D,Pascal,Fortran和其他一些不太知名的语言。我偏好C++的原因有几个。\n", 156 | "- 一些非常好的编译器和优化函数库支持C++。\n", 157 | "- C++是一种高级高级语言,具有很多其他语言中很少见的高级功能。但是C++语言还包含低级C语言作为子集,可以进行低级别的优化。\n", 158 | "- 大多数C++编译器能够生成汇编语言输出,这对于检查编译器如何优化一段代码非常有用。\n", 159 | "- 此外,当需要最高级别的优化时,大多数C++编译器允许类似汇编的内部函数,内联汇编或轻松链接到汇编语言模块。\n", 160 | "- C++语言是可移植的,因为所有主要平台都存在C++编译器。\n", 161 | "\n", 162 | "Pascal具有C++的许多优点,但不是多功能的。Fortran也非常有效,但语法非常老套。\n", 163 | "\n", 164 | "由于强大的开发工具的可用性,C++开发非常高效。一种流行的开发工具是Microsoft Visual Studio。这个工具可以生成两种不同的C++实现代码,直接编译的二进制执行代码,和.NET框架公共语言运行时中间代码。 显然,当速度很重要时,直接编译的版本是首选。\n", 165 | "\n", 166 | "C++的一个重要缺点与安全性有关。没有检查数组边界违规,整数溢出和无效指针。没有这样的检查使得代码执行速度比其他有此类检查的语言更快。但程序员有责任在程序逻辑可能包含类错误的情况下,明确检查这些错误。第15页提供了一些指导原则。\n", 167 | "\n", 168 | "当性能优化更重要时,C++肯定是首选的编程语言。与其他编程语言相比,性能的提高可能相当大。当性能对最终用户很重要时,性能的提高导致的开发时间可能会稍微增加,也变得合理。\n", 169 | "\n", 170 | "有些情况下,基于其他原因需要基于中间代码的高级框架,但部分代码仍需要仔细优化。在这种情况下,混合实施可能是一个可行的解决方案。代码中最关键的部分可以用编译的C++或汇编语言来实现,其余的代码(包括用户界面等)可以在高级框架中实现。代码的优化部分可能被编译为动态链接库(DLL),由代码的其余部分调用。 这不是一个最佳解决方案,因为高级框架仍然消耗大量资源,并且两种代码之间的转换会带来额外的开销,从而消耗CPU时间。但是,如果代码中时间关键的部分可以完全包含在DLL中,那么这种解决方案仍然可以大大提高性能。\n", 171 | "\n", 172 | "另一个值得考虑的选择是D语言。D具有Java和C#的许多特性,并且避免了C++的许多缺点。并且,D 被编译为二进制代码,并且可以和 C 或 C++ 代码连接在一起。 用于D的编译器和IDE还不如C++编译器那么完善。" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "## 2.5 选择编译器\n", 180 | "\n", 181 | "有几种不同的C++编译器可供选择。很难预测,对于某段特定的代码,哪个编译器会做优化得更好。每个编译器都有一些非常智能的功能实现,其他的东西很愚蠢。下面提到了一些常见的编译器。" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "### Microsoft Visual Studio\n", 189 | "\n", 190 | "这是一个非常用户友好的编译器,具有许多功能,但也非常昂贵。有限的“特快”版本是免费提供的。\n", 191 | "- Visual Studio可以为.NET框架以及直接编译的代码生成代码。(编译没有公共语言运行库CLR,生成二进制代码)。\n", 192 | "- 支持32位和64位Windows。 \n", 193 | "- 集成开发环境(IDE)支持多种编程语言,分析和调试。\n", 194 | "- Microsoft平台软件开发工具包(SDK或PSDK)中免费提供C++编译器的命令行版本。\n", 195 | "- 支持用于多核处理的OpenMP指令。\n", 196 | "- Visual Studio优化合理,但它不是最好的优化器。\n", 197 | "\n", 198 | "### Borland/CodeGear/Embarcadero C++ builder\n", 199 | "\n", 200 | "- 具有与Microsoft编译器具有许多相同功能的IDE。\n", 201 | "- 仅支持32位Windows。-\n", 202 | "- 不支持SSE和更高版本的指令集。\n", 203 | "- 不像Microsoft,Intel,Gnu和PathScale编译器那样优化。\n", 204 | "\n", 205 | "### Intel C++ compiler (parallel composer)\n", 206 | "\n", 207 | "- 该编译器没有自己的IDE。\n", 208 | " - 在为Windows进行编译时,作为Microsoft Visual Studio的一个插件\n", 209 | " - 针对Linux进行编译时,作为Eclipse编译的插件\n", 210 | " - 从命令行或make实用程序调用时,它也可以用作独立编译器。\n", 211 | "- 它支持32位和64位Windows,32位和64位Linux以及基于Intel的Mac OS和Itanium系统。\n", 212 | "\n", 213 | "英特尔编译器支持向量内在函数,自动向量化(参见第110页),OpenMP以及将代码自动并行化到多个线程中。\n", 214 | "编译器支持CPU调度,为不同的CPU制作多个代码版本。(有关如何在非Intel处理器上工作,请参阅第133页)。\n", 215 | "它对所有平台上的内联汇编以。在Windows和Linux中使用相同的内联汇编语法成为可能,都有出色的支持。编译器附带一些可用的最佳优化数学函数库。\n", 216 | "\n", 217 | "英特尔编译器最主要的缺点是,在AMD和威盛处理器上,编译后的代码可能以低速运行,或者根本不运行。绕过所谓的CPU调度程序,可以避免这个问题,该调度程序检查代码是否在Intel CPU上运行。(详情请参阅第133页)。\n", 218 | "\n", 219 | "鉴于下面的情况,英特尔编译器是一个理想选择:\n", 220 | "- 代码从编译器的优化功能中获益\n", 221 | "- 代码需要移植到不同的操作系统\n", 222 | "\n", 223 | "### Gnu\n", 224 | "\n", 225 | "这是可用的最优化的编译器之一,尽管用户不太友好。\n", 226 | "- 它是免费且开源的。\n", 227 | "- 它有大多数Linux,BSD和Mac OS X(32位和64位)发行版。\n", 228 | "- 支持OpenMP和自动并行化。\n", 229 | "- 支持矢量内在函数和自动矢量化(参见第110页)。\n", 230 | "- Gnu函数库尚未完全优化。\n", 231 | "- 支持AMD和英特尔矢量数学库。\n", 232 | "- Gnu C++编译器适用于许多平台,包括32位和64位Linux,BSD,Windows和Mac。\n", 233 | "\n", 234 | "Gnu编译器是所有类Unix平台的非常好的选择。\n", 235 | "\n", 236 | "### Clang\n", 237 | "\n", 238 | "Clang编译器基于LLVM,是一种新型编译器,它在许多方面与Gnu编译器相似,并且与Gnu高度兼容。\n", 239 | "预计将取代Mac平台上的Gnu编译器,同时也支持Linux和Windows平台。 Clang编译器是所有平台不错的选择。\n", 240 | "\n", 241 | "### PathScale\n", 242 | "是用于32位和64位Linux的C++编译器。有许多很好的优化选项。 支持并行处理,OpenMP和自动矢量化。\n", 243 | "可以在代码中插入优化提示作为编译指示(pragmas)以告知编译器,例如,某部分代码执行是否常被执行。\n", 244 | "优化非常好。如果不能接受英特尔编译器偏向英特尔CPU,此编译器对Linux平台来说是一个不错的选择。\n", 245 | "\n", 246 | "### PGI\n", 247 | "\n", 248 | "适用于32位和64位Windows,Linux和Mac的C++编译器。支持并行处理,OpenMP和自动矢量化。优化合理。向量内有函数性能非常差。\n", 249 | "\n", 250 | "### Digital Mars\n", 251 | "这是32位Windows的廉价编译器,自带IDE。优化做得不是很好。\n", 252 | "\n", 253 | "### Open Watcom\n", 254 | "另一个用于32位Windows的开源编译器。 默认情况下,不符合标准函数调用约定。优化合理。\n", 255 | "\n", 256 | "### Codeplay VectorC\n", 257 | "32位Windows的商业编译器。 集成到Microsoft Visual Studio IDE中。 自2004年以来未更新。可以做自动矢量化。 优化适中。 支持三种不同的目标文件格式。" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "### 评论\n", 265 | "\n", 266 | "所有这些编译器都可以使用没有IDE的命令行版本。\n", 267 | "对于商业编译器,有免费试用版提供。\n", 268 | "\n", 269 | "通常可以在Linux平台上混合链接来自不同编译器的目标文件,在某些情况下,在Windows平台上也可以混用。\n", 270 | "用于Windows的Microsoft和Intel编译器在目标文件上完全兼容,并且Digital Mars编译器与这些编译器大多兼容。\n", 271 | "CodeGear,Codeplay和Watcom编译器在目标文件上与其他编译器不兼容。\n", 272 | "\n", 273 | "对于开发良好性能的代码,我的建议是\n", 274 | "- 对Unix应用程序使用Gnu,Clang,Intel或PathScale编译器,\n", 275 | "- 对Windows应用程序使用Gnu,Clang,Intel或Microsoft编译器。\n", 276 | "\n", 277 | "在某些情况下,编译器的选择可能需要考虑:\n", 278 | "- 与旧代码兼容的要求\n", 279 | "- IDE的特定偏好\n", 280 | "- 调试设施\n", 281 | "- 简易GUI开发\n", 282 | "- 数据库集成\n", 283 | "- Web应用程序集成\n", 284 | "- 混合语言编程等等\n", 285 | "\n", 286 | "在所选编译器不提供最佳优化的情况下,使用不同编译器s生成最关键的模块可能会很有用。\n", 287 | "如果还需要链接必要的库文件,英特尔和PathScale编译器生成的目标文件,在大多数情况下可以链接到使用Microsoft或Gnu编译器制作的项目中,而不会出现任何问题。\n", 288 | "将Borland编译器与其他编译器或函数库结合起来更加困难。 这些函数必须具有`extern \"C\"`声明,并且目标文件需要转换为OMF格式。\n", 289 | "另一个方案,使用最好的编译器创建一个DLL,并使用另一个编译器构建的项目调用它。" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "## 2.6 选择函数库\n", 297 | "\n", 298 | "一些应用程序将大部分执行时间花费在执行库函数上。耗时的库函数通常属于以下集中情况:\n", 299 | "\n", 300 | "- 文件I/O\n", 301 | "- 图形和声音处理\n", 302 | "- 内存和字符串操作\n", 303 | "- 数学函数\n", 304 | "- 加密,解密,数据压缩\n", 305 | "\n", 306 | "大多数编译器都包含用于上述目的的标准库。不幸的是,标准库并不总是完全优化。\n", 307 | "\n", 308 | "库函数通常是许多用户在许多不同应用程序中使用的小块代码。\n", 309 | "因此,比优化应用程序特定的代码更值得花费更多的精力来优化库函数。\n", 310 | "最佳功能库一般经过高度优化,使用汇编语言,能自动CPU调度(参见第125页)到最新的扩展指令集。\n", 311 | "如果性能分析(请参见第16页)显示某个特定应用程序在库函数中占用了大量CPU时间,或者不用通过性能分析,肉眼可见,则可以通过使用不同的函数库来显著地提高性能。\n", 312 | "如果应用程序在库函数中使用大部分时间,那么可能不需要优化其他任何内容,只需要寻找最高效的库,并优化减少对该库函数的调用。\n", 313 | "建议尝试不同的库,看看哪一个最好。" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "下面讨论一些常用函数库。也包含许多用于特殊用途的库。\n", 321 | "\n", 322 | "### Microsoft\n", 323 | "微软编译器自带。一些功能优化得很好,另一些功能则不是很好。支持32位和64位Windows。\n", 324 | "\n", 325 | "### Borland / CodeGear / Embarcadero\n", 326 | "Borland C++ builder附带。未针对SSE2及更高版本的指令集进行优化。仅支持32位Windows。\n", 327 | "\n", 328 | "### Gnu\n", 329 | "附带于Gnu编译器。没有像编译器本身那样优化。64位版本比32位版本更好。\n", 330 | "Gnu编译器通常插入内置函数代码(built-in code),而不是最常见的内存和字符串指令。\n", 331 | "内置的代码不是最优化的。 使用选项`-fno-builtin`来获取相应的库版本。 Gnu库支持32位和64位Linux和BSD。Windows版本目前不是最新版本。\n", 332 | "\n", 333 | "### Mac\n", 334 | "Gnu编译器附带的给Mac OS X(Darwin)库,是Xnu项目的一部分。\n", 335 | "一些最重要的功能函数包含在名为commpage中的操作系统内核中。\n", 336 | "这些函数针对Intel Core和更高版本的Intel处理器进行了高度优化。\n", 337 | "AMD处理器和更早的Intel处理器完全不受支持。\n", 338 | "只能在Mac平台上运行。\n", 339 | "\n", 340 | "### Intel\n", 341 | "英特尔编译器包含标准函数库。还有几种专用库,如\n", 342 | "- “英特尔数学核心库” Intel Math Kernel Library\"\n", 343 | "- “综合性能基元” \"Integrated Performance Primitives\"\n", 344 | "\n", 345 | "这些功能库针对大型数据集进行了高度优化。但是,英特尔图书馆在AMD和威盛处理器上并不总是很好。\n", 346 | "有关解释和可能的解决方法,请参阅第133页。\n", 347 | "支持所有x86和x86-64平台。\n", 348 | "\n", 349 | "### AMD\n", 350 | "AMD数学核心库包含优化的数学函数。它也适用于英特尔处理器。性能差于英特尔库。支持32位和64位Windows和Linux。\n", 351 | "\n", 352 | "\n", 353 | "### Asmlib\n", 354 | "我自己的函数库用于演示目的。 可从 [www.agner.org/optimize/asmlib.zip](www.agner.org/optimize/asmlib.zip) 获取。\n", 355 | "目前包括内存和字符串函数的优化版本以及其他一些难以在别处找到的功能。\n", 356 | "在最新的处理器上运行时比其他大多数库更快。支持所有x86和x86-64平台。\n" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": {}, 362 | "source": [ 363 | "### 函数库对比\n", 364 | "\n", 365 | "测试 | 处理器 | Microsoftt | CodeGear | Intel | Mac | Gnu 32-bit | Gnu 32-bit -fno-builtin | Gnu 64-bit -fno-builtin | Asmlib\n", 366 | " --- | --- | --- | --- | --- | --- | --- | --- | --- | ---\n", 367 | "memcpy 16kB aligned operands | Intel Core 2 | 0.12 | 0.18 | 0.12 | 0.11 | 0.18 | 0.18 | 0.18 | 0.11\n", 368 | "memcpy 16kB unaligned op. | Intel Core 2 | 0.63 | 0.75 | 0.18 | 0.11 | 1.21 | 0.57 | 0.44 | 0.12\n", 369 | "memcpy 16kB aligned operands | AMD Opteron K8|0.24 | 0.25 | 0.24 | n.a. | 1.00 | 0.25 | 0.28 | 0.22\n", 370 | "memcpy 16kB unaligned op. | AMD Opteron K8|0.38 | 0.44 | 0.40 | n.a. | 1.00 | 0.35 | 0.29 | 0.28\n", 371 | "strlen 128 bytes | IntelCore 2 |0.77 | 0.89 | 0.40 | 0.30 | 4.5 | 0.82 | 0.59 | 0.27\n", 372 | "strlen 128 bytes | AMD Opteron K8|1.09 | 1.25 | 1.61 | n.a. | 2.23 | 0.95 | 0.6 | 1.19\n", 373 | "\n", 374 | "表2.1. 不同函数库性能对比\n", 375 | "\n", 376 | "表格中的数字是每个字节数据的核心时钟周期数(数字越小意味着性能越好)。对齐的操作数意味着源和目标都具有可被16整除的地址。\n", 377 | "\n", 378 | "被测库版本 (不是最新):\n", 379 | "- Microsoft Visual studio 2008, v. 9.0\n", 380 | "- CodeGear Borland bcc, v. 5.5\n", 381 | "- Mac: Darwin8 g++ v 4.0.1.\n", 382 | "- Gnu: Glibc v. 2.7, 2.8.\n", 383 | "- Asmlib: v. 2.00.\n", 384 | "- Intel C++ compiler, v. 10.1.020. 函数`_intel_fast_memcpy` 和 `__intel_new_strlen` 来自库 `libircmt.lib`. 函数名未被标准记录。" 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "## 2.7 选择UI框架\n", 392 | "\n", 393 | "典型软件项目中的大多数代码都会有用户界面。\n", 394 | "相比于程序的实质性任务,计算密集程度不高的应用程序很可能会在用户界面上花费更多的CPU时间。\n", 395 | "\n", 396 | "应用程序员很少从头开始编写自己的图形用户界面。\n", 397 | "从头写的话,不仅浪费了程序员的时间,而且对最终用户也不方便。\n", 398 | "出于可用性原因,菜单,按钮,对话框等应尽可能标准化。\n", 399 | "程序员可以利用来自于如下所列的标准用户界面元素:\n", 400 | "- 操作系统附带的,\n", 401 | "- 编译器和开发工具附带的库。\n", 402 | "\n", 403 | "Windows和C++的流行用户界面库是Microsoft基础类(MFC)。\n", 404 | "一个竞争产品是Borland现在已经停用的对象Windows库(OWL)。\n", 405 | "还有几种图形界面框架可用于Linux系统。\n", 406 | "用户界面库可以作为运行时DLL或静态库链接。\n", 407 | "运行时DLL比静态库需要更多的内存资源,除非多个应用程序同时使用相同的DLL。\n", 408 | "\n", 409 | "用户界面库可能比应用程序本身更大,并需要更多时间来加载。\n", 410 | "轻量级的选择是Windows模板库(WTL)。\n", 411 | "WTL应用程序通常比MFC应用程序更快,更紧凑。\n", 412 | "WTL应用程序的开发时间可能会因文档不足和缺乏先进的开发工具而更高。\n", 413 | "\n", 414 | "最简单的用户界面是抛弃图形用户界面,使用控制台模式。\n", 415 | "控制台模式程序的输入通常在命令行或输入文件中指定。输出到控制台或输出文件。\n", 416 | "控制台模式程序快速,紧凑且易于开发。\n", 417 | "很容易移植到不同的平台,因为它不依赖于系统特定的图形界面调用。\n", 418 | "可用性可能很差,因为它缺乏图形用户界面的解释性很好的图形菜单。\n", 419 | "控制台模式程序对于从其他应用程序(如make实用程序)进行调用很有用。\n", 420 | "\n", 421 | "结论是用户界面框架的选择必须是开发时间,可用性,程序紧凑性和执行时间之间的妥协。 没有通用的解决方案最适合所有应用。\n" 422 | ] 423 | }, 424 | { 425 | "cell_type": "markdown", 426 | "metadata": {}, 427 | "source": [ 428 | "## 2.8 克服C++语言缺陷\n", 429 | "\n", 430 | "虽然C++在优化方面有许多优点,但它也有一些缺点,导致开发人员选择其他编程语言。本节讨论为了性能优化而选择C++时,如何克服这些缺点。\n", 431 | "\n", 432 | "### 可移植性\n", 433 | "\n", 434 | "C++完全可移植,因为其语法在所有主要平台上完全标准化和被支持。\n", 435 | "但是,C++也是一种允许直接访问硬件接口和系统调用的语言。这些当然是和具体系统特定相关的。\n", 436 | "为了便于平台之间的移植,建议将用户界面和代码的其他系统特定相关部分放在单独的模块中,\n", 437 | "并将代码的特定于任务的部分放置在另一个模块中,这部分代码应该与系统无关。\n", 438 | "\n", 439 | "整数大小和其他硬件相关细节取决于硬件平台和操作系统。 详情请参阅第29页。\n", 440 | "\n", 441 | "### Development time\n", 442 | "\n", 443 | "一些开发人员认为某些编程语言和开发工具比其他开发工具更快使用。\n", 444 | "虽然其中的一些差异仅仅是一个习惯问题,但某些开发工具确实拥有强大的功能,可以自动完成大部分琐碎的编程工作。\n", 445 | "C++项目的开发时间和可维护性可以通过持续一致的模块化和可重用的类来提高。" 446 | ] 447 | }, 448 | { 449 | "cell_type": "markdown", 450 | "metadata": {}, 451 | "source": [ 452 | "\n", 453 | "### 安全\n", 454 | "\n", 455 | "C++语言最严重的问题与安全性有关。 标准C++实现没有检查数组边界违例和无效指针。\n", 456 | "这是C++程序错误的常见来源,也是黑客攻击的可能点。\n", 457 | "对于安全敏感的程序,必须要坚持一定的编程原则,以防止程序出现此类错误。\n", 458 | "\n", 459 | "无效指针的问题可以通过下列方法避免:\n", 460 | "- 使用引用代替指针\n", 461 | "- 将指针初始化为零\n", 462 | "- 一旦指针指向的对象变为无效,把指针设置为零\n", 463 | "- 并通过避免指针运算和指针类型转换\n", 464 | "\n", 465 | "链接列表和其他通常使用指针的数据结构可能会被更有效的容器类模板所取代,如第95页所述。避免使用`scanf`函数。\n", 466 | "\n", 467 | "违反数组边界可能是C++程序中最常见的错误原因。\n", 468 | "写入数组的末尾可能会导致其他变量被覆盖,甚至更糟糕的是,它可能会覆盖包含此定义数组的函数的返回地址。这可能会导致各种奇怪和意外的行为。\n", 469 | "数组通常用作存储文本或输入数据的缓冲区。对输入数据缓冲区溢出的缺乏检查,是黑客经常利用的常见错误。\n", 470 | "\n", 471 | "防止这种错误的一个好方法是用经过充分测试的容器类替换数组。标准模板库(STL)是这种容器类的有用来源。\n", 472 | "不幸的是,许多标准容器类以低效的方式使用动态内存分配。\n", 473 | "有关如何避免动态内存分配的示例,请参见第92页。\n", 474 | "有关高效容器类的讨论,请参阅第95页。\n", 475 | "本手册的附件[www.agner.org/optimize/cppexamples.zip](www.agner.org/optimize/cppexamples.zip) 包含边数组界检查和各种有效容器类的示例。\n", 476 | "\n", 477 | "文本字符串容易产生特殊问题,因为字符串的长度可能没有一定的限制。\n", 478 | "旧式C语言风格方法是在字符数组中存储字符串,尽管是快速和高效的,但是除非在存储之前,检查每个字符串的长度,否则不安全。\n", 479 | "这个问题的标准解决方案是使用字符串类,如`string`或`CString`。 这兼具安全性和灵活性,但在大型应用中效率很低。\n", 480 | "每次创建或修改字符串时,字符串类都会分配一个新的内存块。这可能会导致内存碎片化,并导致堆管理和垃圾回收的高开销成本。\n", 481 | "不会危及安全性的更有效的解决方案是将所有字符串存储在一个内存池中。\n", 482 | "有关如何将字符串存储在内存池中的信息,请参阅附录中的示例[www.agner.org/optimize/cppexamples.zip](www.agner.org/optimize/cppexamples.zip)。\n", 483 | "\n", 484 | "整数溢出是另一个安全问题。官方的C标准说,在溢出的情况下,有符号整数的行为是“未定义的”。这允许编译器忽略溢出或假定它不发生。\n", 485 | "在Gnu编译器的情况下,不会出现有符号整数溢出的假设,会导致不幸的后果,即它允许编译器优化移除掉溢出检查。\n", 486 | "针对这个问题有许多可能的补救措施:\n", 487 | "1. 溢出发生之前就检查\n", 488 | "- 使用无符号整数 - 它们保证环绕(按照最大值+1取模)\n", 489 | "- 用选项`-ftrapv`捕获整数溢出,但这是非常低效的\n", 490 | "- 通过选项`-Wstrict-overflow=2`获得编译器警告,以便进行此类检查优化\n", 491 | "- 使用选项`-fwrapv`或`-fno-strict-overflow`来良好定义溢出时的行为\n", 492 | "\n", 493 | "在速度很重要的关键部分代码,您可以偏离上述安全建议。\n", 494 | "如果不安全的代码仅限于:\n", 495 | "- 经过良好测试的函数,类,模板\n", 496 | "- 或者是与程序其余部分具有良好定义接口的模块\n", 497 | "\n", 498 | "那么这是允许的。" 499 | ] 500 | } 501 | ], 502 | "metadata": { 503 | "kernelspec": { 504 | "display_name": "C++17", 505 | "language": "C++", 506 | "name": "cling-cpp17" 507 | }, 508 | "language_info": { 509 | "codemirror_mode": "c++", 510 | "file_extension": ".c++", 511 | "mimetype": "text/x-c++src", 512 | "name": "c++" 513 | } 514 | }, 515 | "nbformat": 4, 516 | "nbformat_minor": 2 517 | } 518 | -------------------------------------------------------------------------------- /03-FindingTheBiggestTimeConsumers/03-找到最大的时间消费者.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 3 找到最大的时间消费者\n", 8 | "\n", 9 | "## 3.1 一个时钟周期有多长?\n", 10 | "\n", 11 | "在本手册中,我使用CPU时钟周期而不是秒或微秒作为时间度量。\n", 12 | "这是因为电脑的速度非常不同。如果我写出今天需要10微秒的时间,那么下一代计算机可能只需要5微秒,我的手册很快就会过时。\n", 13 | "但是如果我写出某些东西需要10个时钟周期,那么即使CPU时钟频率加倍,仍然需要10个时钟周期。\n", 14 | "\n", 15 | "时钟周期的长度是时钟频率的倒数。例如,如果时钟频率为2 GHz,则时钟周期的长度为\n", 16 | "$$ \\frac{1}{2GHz} = 0.5ns $$\n", 17 | "\n", 18 | "一台计算机上的时钟周期并不总是与另一台计算机上的时钟周期相当。\n", 19 | "奔腾4(NetBurst)CPU被设计为比其他CPU更高的时钟频率,但是与其他CPU相比,它需要使用更多的时钟周期来执行同一段代码。\n", 20 | "\n", 21 | "假设程序中的循环重复1000次,并且循环内有100个浮点运算(加法,乘法等)。\n", 22 | "如果每个浮点运算需要5个时钟周期,那么我们可以大致估计在2 GHz CPU上该环路需要 `1000 * 100 * 5 * 0.5 ns = 250μs`。\n", 23 | "我们应该尝试优化这个循环吗? 当然不! 250μs小于刷新屏幕所需时间的1/50。用户感觉不到任何延迟。\n", 24 | "但是如果这个循环包含在另一个也重复1000次的循环内,那么我们估计的计算时间为250毫秒。\n", 25 | "这种延迟刚刚够长到引起人们的注意,但还不至于长到让人厌烦。\n", 26 | "我们可能会决定做一些测量,看看我们的估算是否正确,或者估算的时间是否超过250毫秒。\n", 27 | "如果响应时间长到用户实际上需要等待结果,那么我们会考虑是否有可以改进的地方。" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## 3.2 使用分析器找到热点\n", 35 | "\n", 36 | "在开始优化任何事情之前,您必须确定程序的关键部分。\n", 37 | "- 在某些程序中,超过99%的时间花在最内层循环中进行数学计算。\n", 38 | "- 在其他程序中,99%的时间用于读取和写入数据文件,而不到1%用于实际处理这些数据。\n", 39 | "\n", 40 | "优化关键代码的非常重要,而不是优化仅占用一小部分时间的代码。\n", 41 | "优化不太关键的代码部分不仅浪费时间,还会使代码更混乱,更难调试和维护。\n", 42 | "\n", 43 | "大多数编译器软件包都包含一个分析器,它可以告诉每个函数被调用了多少次,占用了多少时间。\n", 44 | "还有些第三方分析器,如AQtime,英特尔VTune和AMD CodeAnalyst。" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "有几种不同的性能分析方法:\n", 52 | "\n", 53 | "- 插桩(Instrumentation)。编译器在每次函数调用时插入额外的代码来计算函数被调用的次数以及需要多少时间。\n", 54 | "- 调试。 分析器在每个函数或每个代码行处,插入临时调试断点。\n", 55 | "- 基于时间的采样:分析器告诉操作系统产生中断,例如,每毫秒一次。分析器计算程序每个部分中断发生的次数。这不需要修改正在测试的程序,但不太可靠。\n", 56 | "- 基于事件的采样:分析器告诉CPU在某些事件时产生中断,例如,每当一千次高速缓存未命中时。这样就可以发现程序的哪一部分具有最多的高速缓存未命中,分支错误预测,浮点异常等。基于事件的采样需要CPU特定的分析器。对于Intel CPU,需要使用Intel VTune,AMD CPU则使用AMD CodeAnalyst。" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "不幸的是,性能分析器(profilers)往往不可靠。 由于技术问题,他们有时会产生令人误解的结果,或完全失败。\n", 64 | "\n", 65 | "分析器的一些常见问题是:\n", 66 | "- 粗粒度时间测量。如果是以毫秒分辨率来测量,但关键函数仅仅需要几微秒执行,则测量结果可能变得不精确或者就为零。\n", 67 | "\n", 68 | "- 执行时间太短或太长。如果被测试程序在短时间内完成,那么采样产生的分析数据太少。如果程序执行时间太长,那么分析器可能会采集到太多的数据,超过它的处理能力。\n", 69 | "\n", 70 | "- 等待用户输入。许多程序花费大部分时间等待用户输入或等待网络资源。这些等待的时间也会被分析器捕捉到。为了使性能分析可行,可能有必要修改程序,以使用一组测试数据而不是从用户输入。\n", 71 | "\n", 72 | "- 来自其他进程的干扰。分析器不仅测量被测试程序的时间,还测量在同一台计算机上运行的所有其他进程(包括分析器本身)所用的时间。\n", 73 | "\n", 74 | "- 函数地址在优化程序中被隐藏。分析器通过地址,识别程序中的所有性能热点,并尝试将这些地址转换为函数名。但是高度优化的程序经常重新组织生成的代码,使得函数名和代码地址之间没有明确的对应关系。内联函数的名称可能对分析器根本就不可见。结果将会生成误导性的报告。\n", 75 | "\n", 76 | "- 使用代码的调试版本。 某些分析器要求您正在测试的代码包含调试信息以便识别单个函数或代码行。代码的调试版本是未被优化的。\n", 77 | "\n", 78 | "- 在CPU不同的核之间跳转。在多核CPU上处理器上,进程或线程不一定会保持在相同的处理器内核上运行,但事件计数器却可以。这会导致在多个CPU内核之间跳转的线程的无意义事件计数。 您可能需要通过设置线程亲和性掩码,来将线程锁定到特定的CPU内核。\n", 79 | "\n", 80 | "- 可重复性差。程序执行的延迟可能由不可重现的随机事件引起。任务切换和垃圾回收等事件可随机发生,并使程序的某些部分看起来比平时花费更长的时间。" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "使用性能分析器有多种方式。\n", 88 | "一个简单的方法是在调试器中运行程序,并在程序运行时按下中断。\n", 89 | "如果有一个热点使用90%的CPU时间,那么有90%的机会在这个热点地区发生中断。重复中断几次可能足以确定一个热点。\n", 90 | "在调试器中查看调用堆栈来确定热点相关的情况。\n", 91 | "\n", 92 | "有时,识别性能瓶颈的最好方法是将测量插桩代码直接放入代码中,而不是使用现成的分析器。\n", 93 | "这并不能解决与分析相关的所有问题,但它通常会提供更可靠的结果。\n", 94 | "如果您对分析器的工作方式不满意,那么您可以将所需的测量插桩代码放入程序本身。\n", 95 | "您可以添加计数器变量来计算程序的每个部分执行的次数。\n", 96 | "此外,您还可以读取每个最重要或最关键部分的前后时间点,以测量每个部分需要花费多少时间。\n", 97 | "有关此方法的进一步讨论,请参阅第157页。\n", 98 | "\n", 99 | "您的测量代码应该被`#if`指令包含住,以便在代码的最终版本中禁用它。\n", 100 | "在代码自身中插入自己的分析插桩代码,是在程序开发过程中跟踪性能的非常有用的方法。\n", 101 | "\n", 102 | "如果时间间隔很短,时间测量可能需要非常高的分辨率。\n", 103 | "在Windows中,您可以使用`GetTickCount`或`QueryPerformanceCounter`函数获取毫秒分辨率。\n", 104 | "使用CPU中的时间戳记计数器可以获得更高的分辨率,该计数器以CPU时钟频率计数(在Windows中:`__rdtsc()`)。\n", 105 | "\n", 106 | "如果线程在不同的CPU内核之间跳转,则时间戳计数器将变为无效。\n", 107 | "您可能需要在时间测量过程中将线固定到特定的CPU内核以避免这种情况。(在Windows中,`SetThreadAffinityMask`,在Linux中,`sched_setaffinity`)。\n", 108 | "\n", 109 | "程序应该用一组真实的测试数据进行测试。\n", 110 | "测试数据应包含典型的随机性,以获得缓存未命中和分支预测失误的实际次数。\n", 111 | "\n", 112 | "当找到程序中最耗时的部分时,重要的是只将优化工作集中在耗时的部分上。\n", 113 | "关键代码段可以通过第157页所述的方法进一步测试和深入探查。\n", 114 | "\n", 115 | "分析器对于查找与CPU密集型代码相关的问题非常有用。\n", 116 | "但是许多程序使用更多时间加载文件或访问数据库,网络和其他资源,而不是进行算术操作。\n", 117 | "以下部分讨论最常见的时间消耗情形。\n" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "## 3.3 程序安装\n", 125 | "\n", 126 | "安装程序包花费的时间通常不被看做软件优化问题。但它肯定是可以窃取用户时间的。\n", 127 | "如果软件优化的目标是为用户节省时间,那么安装软件包并使其工作所花费的时间不能忽视。\n", 128 | "由于现代软件的高度复杂性,安装过程花费一个多小时并不罕见。为了查找和解决兼容性问题,用户不得不多次重新安装软件包,这也常常发生。\n", 129 | "\n", 130 | "在决定是否要将软件包搭建在需要大量文件的复杂框架上时,软件开发人员应该考虑安装时间和兼容性问题。\n", 131 | "\n", 132 | "安装过程应始终使用标准化的安装工具。\n", 133 | "应该允许用户在安装启动时选择安装选项,以便剩下的安装过程可以无人照管直到结束。\n", 134 | "卸载也应该以标准化的方式进行。" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "## 3.4 自动更新\n", 142 | "\n", 143 | "许多软件程序会定期通过Internet自动下载更新。\n", 144 | "- 某些程序每次启动计算机时都会搜索更新,即使程序从未使用过。安装了许多此类程序的计算机可能需要几分钟才能启动,这完全是浪费用户的时间。\n", 145 | "- 其他程序,每次程序启动时,会占用时间搜索更新。如果当前版本满足用户需求,用户可能不需要更新。\n", 146 | "\n", 147 | "搜索更新应该是可选的,默认情况下关闭,除非有非常重要的安全更新。\n", 148 | "更新过程应该在低优先级的线程中运行,并且仅在程序实际使用时才运行。\n", 149 | "程序没在使用时,不应该让后台进程运行。\n", 150 | "对于下载好的程序更新的安装,应该推迟到程序关闭并重新启动。\n", 151 | "\n", 152 | "操作系统的更新可能特别耗时。有时需要花费数小时才能把自动更新安装到操作系统。\n", 153 | "这是非常有问题的,因为这些耗时的更新不知道什么时候,就可能在不方便的时候出现。\n", 154 | "如果用户在离开工作场所之前必须关闭或注销计算机出于安全原因,系统禁止用户在更新过程中关闭计算机,则这可能是一个非常大的问题。\n", 155 | "\n", 156 | "如果用户在离开工作场所之前,出于安全原因必须关闭或注销计算机,但是系统禁止用户在更新过程中关闭计算机,则这可能是一个非常大的问题。" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "## 3.5 程序加载\n", 164 | "\n", 165 | "加载程序经常比执行程序花费更多的时间。\n", 166 | "对于基于大型运行时框架,中间代码,解释器,即时(JIT)编译器等的程序,加载时间可能会长得很烦人。例如用Java,C#,Visual Basic等编写的程序通常就是这种情况。\n", 167 | "\n", 168 | "但是,即使通过C++编译实现的程序,程序加载也可能是耗时的。\n", 169 | "这种情况通常发生在如果程序使用:\n", 170 | "- 很多运行时DLL(动态链接库或共享对象)\n", 171 | "- 资源文件\n", 172 | "- 配置文件\n", 173 | "- 帮助文件和数据库\n", 174 | "\n", 175 | "程序启动时,操作系统可能不去加载大型程序的所有模块。\n", 176 | "某些模块只有在需要时才去加载。如果内存大小不足,它们可能会被交换到硬盘。\n", 177 | "\n", 178 | "用户期望即时响应按键或鼠标移动等简单操作。\n", 179 | "如果因为需要从磁盘加载模块或资源文件,这种响应延迟了几秒钟,用户是不会接受的。\n", 180 | "吃内存的内存饥饿型应用程序迫使操作系统将内存交换到磁盘。\n", 181 | "内存交换是对鼠标移动或按键等简单事情的响应时间过长的常见原因。\n", 182 | "\n", 183 | "应该避免在硬盘上散布过多数量的DLL,配置文件,资源文件,帮助文件等。\n", 184 | "少量一些文件,最好在与.exe文件相同的目录下,是可以接受的。" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "## 3.6 动态链接和位置无关代码\n", 192 | "\n", 193 | "函数库可以作为静态链接库(.lib,.a)或动态链接库(也称为共享对象(.dll,.so))来实现。\n", 194 | "有几个因素可以使动态链接库比静态链接库慢。这些因素在下面的第149页详细解释。\n", 195 | "\n", 196 | "位置无关代码用于类Unix系统中的共享目标文件。\n", 197 | "默认情况下,Mac系统经常使用与位置无关的代码。\n", 198 | "与位置无关的代码效率低下,尤其是在32位模式下,原因如下第149页所述。" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "## 3.7 文件访问\n", 206 | "\n", 207 | "在硬盘上读写文件通常比处理文件中的数据花费更多的时间,特别是在安装了病毒扫描程序,并配置成扫描所有的文件访问。\n", 208 | "\n", 209 | "顺序前向访问文件比随机访问快。\n", 210 | "读取或写入大数据块比一次读取或写入一点点要快。\n", 211 | "不要一次读取或写入少于几千字节的数据。\n", 212 | "\n", 213 | "您可以将整个文件镜像到内存缓冲区中,并在一次操作中进行读取或写入,而不是以非顺序的方式一点点地读取或写入。\n", 214 | "\n", 215 | "访问最近访问的文件通常比第一次访问文件要快得多。这是因为该文件已被复制到磁盘缓存。\n", 216 | "\n", 217 | "远程或可移动介质(如软盘和USB记忆棒)上的文件可能无法缓存。\n", 218 | "这可能会产生相当大的后果。\n", 219 | "我曾经创建了一个Windows程序,通过调用`WritePrivateProfileString`创建一个文件,每写入一行时,该文件会被打开和关闭。\n", 220 | "由于磁盘缓存,这在硬盘上工作得非常快,但花费了几分钟才能将文件写入软盘。\n", 221 | "\n", 222 | "对于包含数值数据的大文件,以二进制形式存储,比数据以ASCII形式存储时更为紧凑和高效。\n", 223 | "二进制数据存储的一个缺点是它不具有人类可读性,不容易移植到具有大端(big-endian)存储的系统。\n", 224 | "\n", 225 | "对于具有许多文件输入/输出操作的程序,优化文件访问比在优化CPU使用更重要。\n", 226 | "如果在等待磁盘操作完成时处理器可以执行其他工作,则将文件访问放入单独的线程中可能是有利的。" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "## 3.8 系统数据库\n", 234 | "\n", 235 | "在Windows中访问系统数据库可能需要几秒钟的时间。\n", 236 | "将特定于应用程序的信息存储在单独的文件中比在Windows系统中的大型注册数据库中更有效。\n", 237 | "请注意,如果您正在使用诸如`GetPrivateProfileString`和`WritePrivateProfileString`等函数来读写配置文件(* .ini文件),系统可能会将信息存储在数据库中。" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "## 3.9 其它数据库\n", 245 | "\n", 246 | "许多软件应用程序使用数据库来存储用户数据。\n", 247 | "数据库可能会消耗大量CPU时间,RAM和磁盘空间。\n", 248 | "在简单情况下,可以用普通的旧式数据文件替换数据库。\n", 249 | "数据库查询通常可以通过使用索引,使用集合而不是循环等来优化。\n", 250 | "优化数据库查询超出了本手册的范围,但您应该意识到通过优化数据库访问常常有很多事情要做。" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "## 3.10 图形\n", 258 | "\n", 259 | "图形用户界面会占用大量的计算资源。\n", 260 | "通常,需要使用一个特定的图形框架。操作系统可以在其API中提供这样的框架。\n", 261 | "在某些情况下,操作系统API和应用程序软件之间还有一层额外的第三方图形框架。\n", 262 | "这样的额外框架可能会消耗大量额外的资源。\n", 263 | "\n", 264 | "应用软件中的每个图形操作都是作为对图形库或API函数的函数调用实现的。这些图形库函数或AP函数再调用设备驱动程序。\n", 265 | "对图形函数的调用非常耗时,因为它可能会经历多层调用,并且需要切换到保护模式并再次返回。\n", 266 | "显然,相比于通过多个函数调用分别绘制每个像素或线段,调用单个图形函数来绘制整个多边形或位图更为高效。\n", 267 | "\n", 268 | "计算机游戏和动画中的,图形对象的计算也很耗时,尤其是在没有图形处理单元的情况下。\n", 269 | "\n", 270 | "各种图形函数库和驱动程序在性能上差别很大。我无法提供具体的推荐。" 271 | ] 272 | }, 273 | { 274 | "cell_type": "markdown", 275 | "metadata": {}, 276 | "source": [ 277 | "## 3.11 其它系统资源\n", 278 | "\n", 279 | "对打印机或其他设备的写操作,应该最好以大块数据完成,而不是一次一小块,因为每次调用驱动程序都会包含切换到保护模式并再次返回的开销。\n", 280 | "\n", 281 | "访问系统设备,使用操作系统的高级设施,可能很耗时,因为它可能涉及加载多个驱动程序,配置文件和系统模块。" 282 | ] 283 | }, 284 | { 285 | "cell_type": "markdown", 286 | "metadata": {}, 287 | "source": [ 288 | "## 3.12 网络访问\n", 289 | "\n", 290 | "某些应用程序使用互联网或内联网来进行\n", 291 | "- 自动更新\n", 292 | "- 远程帮助文件\n", 293 | "- 数据库访问等。\n", 294 | "\n", 295 | "这里的问题是访问时间无法控制。\n", 296 | "- 在简单的测试设置中网络访问可能很快\n", 297 | "- 但在网络过载或用户远离服务器的使用情况下,网络访问速度较慢或完全不通。\n", 298 | "\n", 299 | "在决定是否在本地或远程存储帮助文件和其他资源时,应将这些问题考虑在内。\n", 300 | "如果需要频繁的更新,则最好在本地镜像远程数据。\n", 301 | "\n", 302 | "访问远程数据库通常需要使用密码登录。\n", 303 | "对于许多辛勤工作的软件用户而言,登录过程消耗时间,令人厌烦。\n", 304 | "在某些情况下,如果网络或数据库负载过重,登录过程可能需要超过一分钟。" 305 | ] 306 | }, 307 | { 308 | "cell_type": "markdown", 309 | "metadata": {}, 310 | "source": [ 311 | "## 3.13 内存访问\n", 312 | "\n", 313 | "与对数据进行计算所需的时间相比,从RAM内存访问数据可能需要相当长的时间。\n", 314 | "这就是所有现代计算机都有内存缓存的原因。\n", 315 | "通常情况下,CPU有\n", 316 | "- 8 - 64千字节的一级数据高速缓存\n", 317 | "- 256千字节到2兆字节的2级高速缓存\n", 318 | "- 也可能有一个三级缓存\n", 319 | "\n", 320 | "下列情况下,很可能内存访问是程序中最大的时间消耗:\n", 321 | "- 如果程序中所有数据的组合大小大于二级缓存,\n", 322 | "- 并且数据分散在内存中或以非顺序方式访问\n", 323 | "\n", 324 | "如果它被高速缓存,读取或写入存储器中的变量只需要2-3个时钟周期,但如果没有高速缓存,则需要几百个时钟周期。\n", 325 | "有关数据存储,见第26页。有关内存高速缓存,见第89页。" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "## 3.14 上下文切换\n", 333 | "\n", 334 | "上下文切换是指,\n", 335 | "- 在多任务环境中任务的任务的切换\n", 336 | "- 在多线程程序中,不同线程之间的切换\n", 337 | "- 或大型程序中,不同部分之间的切换\n", 338 | "\n", 339 | "频繁的上下文切换会降低性能,这是因为数据缓存,代码缓存,分支目标缓冲区,分支模式历史,等内容可能不得不被重刷新。\n", 340 | "\n", 341 | "如果分配给每个任务或线程的时间片较小,则上下文切换更频繁。\n", 342 | "时间片的长度由操作系统决定,而不是由应用程序决定。\n", 343 | "\n", 344 | "具有多个CPU或具有多个核心的CPU的计算机中,上下文切换的数量较少。" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "metadata": {}, 350 | "source": [ 351 | "## 3.15 依赖关系链\n", 352 | "\n", 353 | "现代微处理器可以执行乱序执行。\n", 354 | "这意味着如果一个软件指定了A然后B的计算,并且A的计算速度很慢,那么微处理器可以在计算A完成之前开始B的计算。\n", 355 | "显然,这只有在计算B时不需要A的值时才可能。\n", 356 | "\n", 357 | "为了利用乱序执行,您必须避免长依赖关系链。\n", 358 | "依赖关系链是一系列的计算,其中每个计算取决于前一个的结果。\n", 359 | "依赖关系链会阻止CPU同时进行多次计算,且阻止乱序执行。\n", 360 | "有关如何中断依赖关系链的示例,请参见第105页。" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "## 3.16 执行单元吞吐量\n", 368 | "\n", 369 | "**时延和执行单元的吞吐量之间有着重要的区别**。\n", 370 | "例如,在现代CPU上执行浮点加法可能需要3到5个时钟周期。但是有可能在每个时钟周期开始一个新的浮点加法。\n", 371 | "这意味着\n", 372 | "- 如果每次加法都取决于前面加法的结果,那么每三个时钟周期只有一次加法。\n", 373 | "- 但是如果所有的加法都是独立的,那么你可以在每个时钟周期进行一次加法。\n", 374 | "\n", 375 | "计算密集型程序中,要获得的最高性能,需要满足:\n", 376 | "- 在上述章节中提到的各种耗时的情形,没有支配地位\n", 377 | "- 并且没有长依赖关系链\n", 378 | "\n", 379 | "在这种情况下,性能仅受执行单元吞吐量的限制,而不受时延或内存访问的限制。\n", 380 | "\n", 381 | "现代微处理器的执行核心切分为几个执行单元。通常,\n", 382 | "- 有两个或更多个整数单元,\n", 383 | "- 一个或两个浮点加法单元\n", 384 | "- 以及一个或两个浮点乘法单元。\n", 385 | "\n", 386 | "这意味着可以在同一时间进行整数加法,浮点加法和浮点乘法运算。\n", 387 | "\n", 388 | "因此,**如果一段代码进行浮点计算,它最好具有平衡的加法和乘法混合一起**。\n", 389 | "减法使用与加法相同的执行单位。\n", 390 | "除法需要更长的时间。\n", 391 | "在浮点运算之间,可以进行整数运算而不降低性能,这是因为整数运算使用不同的执行单元。\n", 392 | "例如,执行浮点计算的循环,通常会伴有使用整数运算(递增循环计数器),比较运算(循环计数器与其设定值比较)等。\n", 393 | "在大多数情况下,您可以假设这些整数运算不会增加总计算时间。" 394 | ] 395 | } 396 | ], 397 | "metadata": { 398 | "kernelspec": { 399 | "display_name": "Python 3", 400 | "language": "python", 401 | "name": "python3" 402 | }, 403 | "language_info": { 404 | "codemirror_mode": "c++", 405 | "file_extension": ".c++", 406 | "mimetype": "text/x-c++src", 407 | "name": "c++" 408 | } 409 | }, 410 | "nbformat": 4, 411 | "nbformat_minor": 2 412 | } 413 | -------------------------------------------------------------------------------- /04-PerformanceAndUsability/04-性能与易用性.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 4 性能与易用性\n", 8 | "\n", 9 | "性能更好的软件产品是为用户节省时间的产品。\n", 10 | "对于许多计算机用户来说,时间是宝贵的资源。很多时间浪费在了速度慢的,难以使用,不兼容或容易出错的软件上。\n", 11 | "所有这些问题都是可用性问题,我认为应该从更广泛的可用性角度来看待软件性能话题。\n", 12 | "\n", 13 | "这不是一本关于可用性的手册,但我认为有必要要让软件程序员的注意到一些常见障碍,导致用户难以有效使用软件。\n", 14 | "有关此主题的更多信息,请参阅我在Wikibooks上的免费电子书 [Usability for Nerds](https://en.wikibooks.org/wiki/Usability_for_Nerds)。\n", 15 | "\n", 16 | "以下列表指出了造成软件用户的一些典型挫折和时间浪费的原因,\n", 17 | "以及软件开发人员应该注意的重要可用性问题。" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "- 大的运行时框架。\n", 25 | ".NET框架和Java虚拟机是通常比他们正在运行的程序占用更多资源的框架。\n", 26 | "这样的框架常常是资源问题和兼容性问题的原由,它们会在下面情况下浪费大量时间:\n", 27 | " - 在安装框架本身时\n", 28 | " - 在安装运行在框架下的程序期间\n", 29 | " - 程序启动期间\n", 30 | " - 以及程序运行期间。\n", 31 | " \n", 32 | "  使用这种运行时框架的主要原因是为了跨平台的可移植性。\n", 33 | "不幸的是,跨平台兼容性并不总是像预期的那样好。\n", 34 | "我相信,通过更好的编程语言,操作系统和API的标准化,可以更高效地实现可移植性。\n", 35 | "\n", 36 | "- 内存交换。\n", 37 | "软件开发人员通常拥有比最终用户拥有更强的计算机,更多的内存。\n", 38 | "因此,开发人员可能无法看到过度的内存交换和其他资源问题,以至于资源饥渴的应用程序对最终用户表现不佳。\n", 39 | "\n", 40 | "\n", 41 | "- 安装问题。\n", 42 | "程序的安装和卸载程序应该标准化,并由操作系统完成,而不是由单独的安装工具完成。\n", 43 | "\n", 44 | "\n", 45 | "- 自动更新。\n", 46 | "如果网络不稳定或者新版本引入新的问题,自动更新软件可能会导致问题。\n", 47 | "更新机制通常会用唠叨的弹出消息来打扰用户,会显示请安装这个重要的新更新,甚至在用户专注于重要工作时,要求用户重新启动计算机。\n", 48 | "更新机制不应该中断用户,而只该静静地显示一个图标,指示更新已经可用。\n", 49 | "抑或在计算机重新启动时自动更新。\n", 50 | "软件分销商经常滥用更新机制来宣传他们软件的新版本。这对用户来说很烦人。\n", 51 | "\n", 52 | "\n", 53 | "- 兼容性问题。\n", 54 | "所有软件都应在不同平台,不同屏幕分辨率,不同系统颜色设置和不同用户访问权限下进行测试。\n", 55 | "软件应该使用标准的API调用,而不是使用私自修改的版本和直接的硬件访问。\n", 56 | "应该使用可用的协议和标准化的文件格式。\n", 57 | "Web系统应该在不同的浏览器,不同的平台,不同的屏幕分辨率等下进行测试。\n", 58 | "应该遵守无障碍指南(Accessibility guidelines)。\n", 59 | "\n", 60 | "\n", 61 | "- 版权保护。\n", 62 | "一些版权保护方案基于的是违反或规避操作系统标准的黑客行为。这些方案是兼容性问题和系统故障的常见来源。\n", 63 | "许多版权保护方案都基于硬件的ID。当硬件更新时,这样的方案会导致问题。\n", 64 | "大多数版权保护方案对用户来说都很烦人,难以进行合法的备份复制,也无法有效防止非法复制。\n", 65 | "衡量版权保护计划的好处时,应综合权衡可用性问题和必要的支持。\n", 66 | "\n", 67 | "\n", 68 | "- 硬件更新。\n", 69 | "硬盘或其他硬件的更换常常要求重新安装所有软件,并且用户设置会丢失。\n", 70 | "重新安装工作完成需要整个工作日或更长时间,这并不罕见。\n", 71 | "许多软件应用程序需要更好的备份功能,而当前的操作系统需要更好地支持硬盘复制。\n", 72 | "\n", 73 | "\n", 74 | "- 安全。\n", 75 | "易于遭受网络访问病毒攻击,易于受到其它滥用行为,这样的软件,对于许多用户而言是代价很大的。\n", 76 | "防火墙,病毒扫描程序和其他保护手段,是兼容性问题和系统崩溃的最常见原因。\n", 77 | "此外,病毒扫描程序在计算机上消耗的时间比其他任何软件都多。\n", 78 | "作为操作系统一部分的安全软件通常比第三方安全软件更可靠。\n", 79 | "\n", 80 | "\n", 81 | "- 后台服务。\n", 82 | "在后台运行的许多服务对于用户而言是不必要的,并且浪费资源。\n", 83 | "可以考虑只有在用户主动打开时,才运行这些服务。\n", 84 | "\n", 85 | "\n", 86 | "- 功能膨胀。\n", 87 | "出于营销原因,软件通常会为每个新版本添加新功能。\n", 88 | "这可能会导致软件变慢或需要更多资源,即使用户从不使用这些新功能。\n", 89 | "\n", 90 | "\n", 91 | "- 认真对待用户反馈。\n", 92 | "用户投诉应被视为有关错误,兼容性问题,可用性问题和所需新功能的宝贵信息来源。\n", 93 | "应该系统地处理用户反馈,以确保信息得到适当使用。\n", 94 | "用户应该得到关于问题调查和计划解决方案的回复。修补程序应该很容易从网站上获得。" 95 | ] 96 | } 97 | ], 98 | "metadata": { 99 | "kernelspec": { 100 | "display_name": "C++17", 101 | "language": "C++", 102 | "name": "cling-cpp17" 103 | }, 104 | "language_info": { 105 | "codemirror_mode": "c++", 106 | "file_extension": ".c++", 107 | "mimetype": "text/x-c++src", 108 | "name": "c++" 109 | } 110 | }, 111 | "nbformat": 4, 112 | "nbformat_minor": 2 113 | } 114 | -------------------------------------------------------------------------------- /05-ChoosingTheOptimalAlgorithm/05-选择最优算法.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 5 选择最优算法\n", 8 | "\n", 9 | "当你想优化一个CPU密集型软件时,首先要做的就是找到最好的算法。\n", 10 | "算法的选择对于排序,搜索和数学计算之类的任务非常重要。\n", 11 | "在这种情况下,您可以通过选择最佳算法获得更多性能改善,而不是优化首先想到的算法。\n", 12 | "在某些情况下,您可能需要基于一组典型的测试数据,测试几种不同的算法,才能找到最适合的算法。\n", 13 | "\n", 14 | "话虽如此,**我必须警惕大家不要过度(使用算法优化)**。\n", 15 | "如果一个简单的算法可以足够快地完成工作,不要使用过于先进而复杂的算法。\n", 16 | "例如,有些程序员甚至在最小的数据列表上使用散列表。\n", 17 | "对于非常大的数据库,散列表可以极大地提高搜索时间,但是没有理由将其用于非常小的列表,因此二分查找甚至线性搜索已经足够快了。\n", 18 | "散列表增加了程序的大小,以及数据文件的大小。\n", 19 | "如果瓶颈是文件访问或缓存访问,而不是CPU消耗的时间,这实际上会降低速度。\n", 20 | "复杂算法的另一个缺点是它使得程序开发更加复杂且更容易出错。\n", 21 | "\n", 22 | "针对不同目的的不同算法的讨论超出了本手册的范围。\n", 23 | "对于标准任务(如排序和搜索),你需要查阅算法和数据结构的常规文献。\n", 24 | "对于复杂的数学任务,你需要查阅特殊的文献。\n", 25 | "\n", 26 | "在你开始编写代码之前,你可能会考虑其他人是否在你之前已经做过了这项工作。\n", 27 | "对于许多标准任务,有多个来源获得可以获得优化的功能库。例如,\n", 28 | "- Boost的集合包,包含用于许多通用的,经过良好测试的库 [www.boost.org](www.boost.org)。\n", 29 | "- “英特尔数学核心函数库”(\"Intel Math Kernel Library\")包含许多常见数学计算函数,包括线性代数和统计学\n", 30 | "- “英特尔性能基元”(\"Intel Performance Primitives\")函数库包含许多用于音频和视频处理,信号处理,数据压缩和加密的函数 [www.intel.com](www.intel.com)。\n", 31 | "\n", 32 | "如果您使用的是英特尔函数库,那么请确保它在非英特尔处理器上正常工作,如第133页所述。\n", 33 | "\n", 34 | "在开始编程之前选择最优算法,说来容易做起来难。\n", 35 | "许多程序员发现,只有在将整个软件项目集成在一起,并进行测试之后,才有更明智的做法。\n", 36 | "通过测试和分析程序性能,并研究瓶颈,获得的洞察力,可以让人更好地理解程序的整体结构。\n", 37 | "这种新的见解可以导致对程序的全面重新设计。例如,当您发现有更智能的数据组织方式时。\n", 38 | "\n", 39 | "对已经运作的项目进行全面的重新设计当然是一项相当大的工作,但这可能是一笔相当不错的投资。\n", 40 | "重新设计不仅可以提高性能,还可能使得程序结构更完善, 更易于维护。\n", 41 | "相对你花在对付原先那个设计不佳的程序各种问题所花的时间,你花在重新设计一个程序上的时间实际上可能更少。" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "C++17", 48 | "language": "C++", 49 | "name": "cling-cpp17" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": "c++", 53 | "file_extension": ".c++", 54 | "mimetype": "text/x-c++src", 55 | "name": "c++" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 2 60 | } 61 | -------------------------------------------------------------------------------- /06-DevelopmentProcess/06-开发流程.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 6 开发流程\n", 8 | "\n", 9 | "关于使用哪种软件开发流程和软件工程原则,存在很大争议。\n", 10 | "我不会推荐任何具体的模型。\n", 11 | "相反,我会就开发流程如何影响最终产品的性能发表一些意见。\n", 12 | "\n", 13 | "在规划阶段,对数据结构,数据流和算法进行全面分析,以预测哪些资源最为关键,这是一件好事。\n", 14 | "但是,在早期规划阶段,可能会有很多未知因素,因此无法轻易获得问题的详细信息。\n", 15 | "鉴于后一种情况,您可以将软件开发工作看做一种学习的过程,其中的反馈主要反馈来自测试。\n", 16 | "这样情况下,你应该准备几轮迭代的重新设计。\n", 17 | "\n", 18 | "一些软件开发模型具有严格的形式上的要求,在软件的逻辑架构中,必需要具有几个抽象层。\n", 19 | "您应该意识到,这种形式主义存在固有的性能成本。\n", 20 | "将软件过多地划分为分离的抽象层,是性能下降的常见原因。\n", 21 | "\n", 22 | "由于大多数开发方法本质上是增量式或迭代式的,所以有一个策略来保存每个中间版本的备份,是非常重要的。\n", 23 | "对于单人的项目,制作每个版本的zip文件就足够了。\n", 24 | "对于团队项目,建议使用版本控制工具。" 25 | ] 26 | } 27 | ], 28 | "metadata": { 29 | "kernelspec": { 30 | "display_name": "C++17", 31 | "language": "C++", 32 | "name": "cling-cpp17" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": "c++", 36 | "file_extension": ".c++", 37 | "mimetype": "text/x-c++src", 38 | "name": "c++" 39 | } 40 | }, 41 | "nbformat": 4, 42 | "nbformat_minor": 2 43 | } 44 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7-不同的C++构建体的效率.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 不同的C++构建体的效率\n", 8 | "\n", 9 | "大多数程序员很少或根本不知道,一段程序代码是怎么样被翻译成机器代码的,也不知道微处理器是如何处理这些代码的。\n", 10 | "例如,许多程序员不知道双精度计算的速度与单精度一样快。\n", 11 | "谁会知道模板类比多态类更有效率?\n", 12 | "\n", 13 | "本章旨在解释不同C++语言元素的效率相对高低,以帮助程序员选择最有效的方案。\n", 14 | "本系列的其它手册中会进一步解释相关的理论背景。" 15 | ] 16 | } 17 | ], 18 | "metadata": { 19 | "kernelspec": { 20 | "display_name": "C++17", 21 | "language": "C++", 22 | "name": "cling-cpp17" 23 | }, 24 | "language_info": { 25 | "codemirror_mode": "c++", 26 | "file_extension": ".c++", 27 | "mimetype": "text/x-c++src", 28 | "name": "c++" 29 | } 30 | }, 31 | "nbformat": 4, 32 | "nbformat_minor": 2 33 | } 34 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.01-不同种类的变量存储.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.1 不同种类的变量存储\n", 8 | "\n", 9 | "变量和对象存储在内存的不同区域,具体取决于它们在C++程序中的声明方式。\n", 10 | "这会影响数据缓存的效率(请参见第89页)。\n", 11 | "如果数据在内存中随处随机分散,则数据缓存效率很差。\n", 12 | "因此了解变量的存储方式非常重要。\n", 13 | "这个存储的相关原则对于简单变量,数组和对象是相同的。" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "### 栈上的存储\n", 21 | "\n", 22 | "在函数内部声明的变量和对象存储在堆栈中,下面部分描述的情况除外。\n", 23 | "\n", 24 | "堆栈是以先入后出方式组织的一部分内存。\n", 25 | "它用于存储函数返回地址(即函数被调用的地方),函数参数,局部变量,以及用于保存函数返回前必须恢复的寄存器。\n", 26 | "每次调用函数时,都会因此而在堆栈中分配所需的空间。\n", 27 | "该内存空间在函数返回时被释放。\n", 28 | "然后再调用下一个函数时,它可以为新函数的参数重用相同的空间。\n", 29 | "\n", 30 | "该堆栈是用于存储数据的最有效的存储空间,因为同一范围的内存地址被一次又一次地重复使用。\n", 31 | "如果没有大数组,那么几乎可以肯定的是,这部分内存被镜像在一级数据缓存中,访问速度非常快。\n", 32 | "\n", 33 | "我们可以从中学到的教训是,所有变量和对象都应该在使用它们的函数内声明。\n", 34 | "\n", 35 | "通过在{}括号内声明变量,可以使变量的范围更小。\n", 36 | "但是,直到函数返回,大多数编译器不会释放变量使用的内存,即使在退出声明变量的{}括号作用域时它本可以释放内存。\n", 37 | "如果变量存储在一个寄存器中(见下文),那么在函数返回之前它就可能被释放。" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "### 全局或静态存储\n", 45 | "\n", 46 | "在函数之外声明的变量称为全局变量。他们可以从任何函数访问。\n", 47 | "全局变量存储在内存的静态部分。\n", 48 | "静态内存也用于使用`static`关键字声明的变量,浮点常量,字符串常量,数组初始化列表,`switch`语句跳转表和虚函数表。\n", 49 | "\n", 50 | "静态数据区通常分为三类:\n", 51 | "- 程序永远不会修改的常量,\n", 52 | "- 程序可修改的初始化变量,\n", 53 | "- 未初始化的变量,可由程序修改。\n", 54 | "\n", 55 | "静态数据的优缺点:\n", 56 | "- 优点是可以在程序启动之前将其初始化为所需的值。\n", 57 | "- 缺点是在整个程序执行过程中占用了内存空间,即使该变量仅用于程序的一小部分。这使数据缓存效率降低。\n", 58 | "\n", 59 | "如果可以避免,不要将变量设置为全局变量。\n", 60 | "不同线程之间的通信可能需要全局变量,但这是它们不可避免的唯一情况。\n", 61 | "如果几个不同的函数需要访问,并且想要避免将变量作为函数参数传输的开销,则创建一个变量全局可能很有用。\n", 62 | "但是一个可能更好的解决方案是,把需要访问该变量的函数设计成类成员函数,同时把该共享变量也成为同一个类的数据成员。\n", 63 | "喜欢哪种解决方案是编程风格的问题。\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "通常更倾向于让使查找表静态化。例如:\n", 71 | "\n", 72 | "```cpp\n", 73 | "// Example 7.1\n", 74 | "float SomeFunction (int x) {\n", 75 | " static float list[] = {1.1, 0.3, -2.0, 4.4, 2.5};\n", 76 | " return list[x];\n", 77 | "}\n", 78 | "```\n", 79 | "\n", 80 | "这里使用静态的优点是在调用函数时不需要执行列表初始化。当程序加载到内存时,这些值就已经存在那里。\n", 81 | "如果从上面的例子中删除了`static`这个词,那么每次调用该函数时,所有五个值都必须被动态地放入列表中。\n", 82 | "这是通过将整个列表从静态内存复制到堆栈内存来完成的。\n", 83 | "**\n", 84 | "在大多数情况下,将常量数据从静态存储器复制到堆栈是时间的浪费。但在特殊情况下,如果数据需要在循环中多次,这种情况可能是最优的,在这种情况下,几乎整个一级缓存被利用起来,用于你想在堆栈中保持在一起的数组。\n", 85 | "**\n" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "字符串常量和浮点常量存储在静态内存中(以优化的代码的形式)。例如:\n", 93 | "\n", 94 | "```cpp\n", 95 | "// Example 7.2\n", 96 | "a = b * 3.5;\n", 97 | "c = d + 3.5;\n", 98 | "```\n", 99 | "\n", 100 | "这里,常数3.5将被存储在静态存储器中。\n", 101 | "大多数编译器会认识到这两个常量是相同的,所以只需要存储一个常量。\n", 102 | "整个程序中所有相同的常量将被连接在一起,以尽量减少用于常量的缓存空间。\n", 103 | "\n", 104 | "整型常量通常包含在指令代码中。**你可以假设整数常量没有缓存问题。**\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "### 寄存器存储\n", 112 | "\n", 113 | "有限数量的变量可以存储在寄存器中,而不是主存储器中。\n", 114 | "寄存器是用于临时存储的CPU内部的一小块内存。\n", 115 | "存储在寄存器中的变量非常快速地被访问。\n", 116 | "所有优化编译器都会自动选择函数中最常用的变量来存储寄存器。\n", 117 | "只要其用途(活动范围)不重叠,相同的寄存器可用于多个变量。\n", 118 | "\n", 119 | "寄存器的数量非常有限。在32位操作系统中有大约六个整数寄存器可用于通用目的,在64位系统中有十四个整数寄存器。\n", 120 | "\n", 121 | "浮点变量使用不同类型的寄存器。\n", 122 | "在32位操作系统中有8个浮点寄存器,在64位操作系统中有16个浮点寄存器。\n", 123 | "除非启用了SSE2指令集(或更高版本),否则一些编译器难以在32位模式下使用浮点寄存器变量。\n" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "### Volatile\n", 131 | "\n", 132 | "`volatile`关键字指定:一个变量可以被别的线程改变。\n", 133 | "编译器可能会假定变量始终具有之前在代码中赋予的值,基于这种假设来进行某些优化。该关键字可以防止此类优化的发生。\n", 134 | "例如:\n", 135 | "\n", 136 | "```cpp\n", 137 | "// Example 7.3. 解释 volatile\n", 138 | "volatile int seconds; // 每秒会被其它的线程+1\n", 139 | "void DelayFiveSeconds() {\n", 140 | " seconds = 0;\n", 141 | " while (seconds < 5) {\n", 142 | " // 5秒前,什么也不做\n", 143 | " }\n", 144 | "}\n", 145 | "```\n", 146 | "\n", 147 | "在这个例子中,`DelayFiveSeconds`函数将等待,直到变量`seconds`由另一个线程增加到5。\n", 148 | "如果seconds没有被声明为`volatile`,那么优化编译器会假定while循环中的`seconds`保持为零,因为循环中没有代码可以更改该值。\n", 149 | "循环将是`while(0 <5){}`,这将是一个无限循环。\n", 150 | "\n", 151 | "关键字`volatile`的作用是确保变量存储在内存中而不是寄存器中,并防止对变量的所有优化。\n", 152 | "这可能在测试情况下很有用,以避免某些表达式被优化掉。\n", 153 | "\n", 154 | "请注意,`volatile`并不意味着原子性。\n", 155 | "它不会阻止两个线程同时尝试写入变量。\n", 156 | "上面示例中的代码在这样的情况下可能会失败:其它线程增加`seconds`的同时尝,此线程将`seconds`设置为零。\n", 157 | "一个更安全的实现将只读取秒的值,并等待该值改变五次。" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "### 线程本地存储\n", 165 | "\n", 166 | "大多数编译器可以使用关键字`__declspec`或`__declspec(thread)`来创建静态和全局变量的线程本地存储。\n", 167 | "这些变量对于每个线程都有一个实例。\n", 168 | "**线程本地存储效率低下,因为它是通过存储在线程环境块中的指针进行访问的。**\n", 169 | "如果可能的话,应该避免线程局部存储,并用栈中的存储替换(参见上文,第26页)\n", 170 | "存储在栈上的变量总是属于创建它们的线程。" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "### Far\n", 178 | "\n", 179 | "具有分段内存的系统(如DOS和16位Windows)允许通过使用关键词`far`(数组也可以很大)将变量存储在远端数据段中。\n", 180 | "`far`存储,`far`指针和`far`程过程效率低下。\n", 181 | "如果某个程序的某个段的数据太多,则建议使用允许更大的段(32位或64位系统)的不同操作系统。\n" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "### 动态内存分配\n", 189 | "\n", 190 | "动态内存分配是通过运算符`new`和`delete`或`malloc`和`free`函数完成的。这些操作员和功能消耗大量时间。\n", 191 | "一部分内存被保留出来,被称为堆,用于动态分配。\n", 192 | "当随机顺序分配和释放不同大小的对象时,堆容易变得碎片化。\n", 193 | "堆管理器可以花费大量时间清理不再使用的空间,并搜索空闲空间。这被称为垃圾收集。\n", 194 | "以一定顺序分配的一组对象,不一定按顺序存储在内存中。\n", 195 | "当堆已经变得分散时,它们可能散布在不同的地方。这使得数据缓存效率低下。\n", 196 | "\n", 197 | "动态内存分配也会使代码更加复杂且容易出错。\n", 198 | "程序必须保持指向所有分配的对象的指针,并跟踪它们何时不再使用。\n", 199 | "在所有可能的程序流程情况下,所有分配的对象也都被释放是非常重要的。\n", 200 | "没能这样做,会造成一种常见的错误,称为内存泄漏。\n", 201 | "更糟糕的一种错误是在释放对象后访问对象。\n", 202 | "程序逻辑可能需要额外的开销来防止这种错误。\n", 203 | "\n", 204 | "有关使用动态内存分配的优点和缺点,请参见第92页。\n", 205 | "\n", 206 | "一些编程语言(如Java)为所有对象使用动态内存分配。这当然是低效的。" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "### 类中声明的变量\n", 214 | "\n", 215 | "在类中声明的变量按它们出现在类声明中的顺序存储。\n", 216 | "存储类型是在声明类的对象时确定的。\n", 217 | "类,结构或联合的对象可以使用上面提到的任何存储方法。\n", 218 | "除了最简单的情况外,对象不能存储在寄存器中,但其数据成员可以复制到寄存器中。\n", 219 | "\n", 220 | "具有`static`修饰符的类成员变量将存储在静态内存中,并且将只有一个实例。\n", 221 | "同一个类中的非静态成员将与类的每个实例一起存储。\n", 222 | "\n", 223 | "将变量存储在类或结构中,这是确保在程序的相同部分中使用的变量存储也彼此邻近的好方法。\n", 224 | "有关使用类的优点和缺点,请参阅第52页。" 225 | ] 226 | } 227 | ], 228 | "metadata": { 229 | "kernelspec": { 230 | "display_name": "C++17", 231 | "language": "C++", 232 | "name": "cling-cpp17" 233 | }, 234 | "language_info": { 235 | "codemirror_mode": "c++", 236 | "file_extension": ".c++", 237 | "mimetype": "text/x-c++src", 238 | "name": "c++" 239 | } 240 | }, 241 | "nbformat": 4, 242 | "nbformat_minor": 2 243 | } 244 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.02-整型变量和操作符.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.2 整型变量和操作符\n", 8 | "\n", 9 | "### 整数大小\n", 10 | "\n", 11 | "整数可以是不同的大小,并且可以是有符号或无符号的。下表总结了可用的不同整数类型。\n", 12 | "\n", 13 | " declaration | size, bits | minimum value | maximum value | in stdint.h \n", 14 | " ------------|------------|------------ | ------------ | ------------\n", 15 | "char | 8 |-128 | 127 | int8_t\n", 16 | "short int (in 16-bit systems: int) | 16 |-32768 | 32767 | int16_t\n", 17 | "int (in 16-bit systems: long int) | 32 | -$2^{31}$ | $2^{31}$-1| int32_t\n", 18 | "long long or int64_t
MS compiler: `__int64`
64-bit Linux: long int | 64 | -$2^{63}$ | $2^{63}$-1 | int64_t\n", 19 | "unsigned char | 8 | 0 | 255 | uint8_t\n", 20 | "unsigned short int
in 16-bit systems: unsigned int | 6 | 0 | 65535 | uint16_t\n", 21 | "unsigned int
in 16-bit systems: unsigned long | 32 | 0 | $2^{32}$-1 | uint32_t\n", 22 | "unsigned long long or uint64_t
MS compiler: unsigned `__int64`
64-bit Linux: unsigned long int| 64 | 0 | $2^{64}$-1 | uint64_t\n", 23 | "**Table 7.1** 不同整数类型的大小" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "就如上表所看到的,不幸的是,对于不同的平台,声明特定大小的整数的方式是不同的。\n", 31 | "**如果标准头文件`stdint.h`或`inttypes.h`存在,那么建议将其使用它们来定义特定大小的整数类型,以达到可移植的目的。**\n", 32 | "\n", 33 | "无论整数的大小如何,整数运算在大多数情况下都很快。\n", 34 | "**但是,如果超过平台提供的最大寄存器大小的整数, 效率不高。**\n", 35 | "换句话说,在16位系统中使用32位整数,或在32位系统中使用64位整数是低效的,尤其是如果代码涉及乘法或除法。\n", 36 | "\n", 37 | "如果您声明了一个int,编译器将始终选择最有效的整数大小,而不指定大小。\n", 38 | "较小尺寸的整数(char,short int)效率仅仅稍低一些。\n", 39 | "在许多情况下,编译器会在计算时将这些类型转换为默认大小的整数,然后仅使用结果的低8位或16位。\n", 40 | "您可以假设该类型转换需要零个或一个时钟周期。\n", 41 | "在64位系统中,只要不做除法,32位整数和64位整数的效率之间的差异只有很小的差别。\n", 42 | "\n", 43 | "建议在下面情况下使用缺省大小的整数:\n", 44 | "- 大小无关紧要\n", 45 | "- 并且不存在溢出风险\n", 46 | "\n", 47 | "例如,简单变量,循环计数器等。\n", 48 | "\n", 49 | "在大型数组中,推荐首选使用够用的最小类型整数,以便更好地使用数据缓存。\n", 50 | "对于位字段,除8,16,32和64位以外的其他的位,效率较低。\n", 51 | "在64位系统中,如果应用程序可以使用额外的位,则可以使用64位整数。\n", 52 | "\n", 53 | "无符号整数类型`size_t`在32位系统中为32位,在64位系统中为64位。\n", 54 | "此类型一般用于表示数组大小和数组索引,可以确保永远不会发生溢出时,即使对于大于2GB的数组。\n", 55 | "\n", 56 | "在考虑某个整数大小是否足够满足特定目的时,你**必须考虑中间计算是否会导致溢出。**\n", 57 | "例如,在表达式`a=(b*c)/d`中,即使a,b,c和d都会低于最大值,也可能发生`(b*c)`溢出。\n", 58 | "编译器没有自动检查整数溢出。\n" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### 有符号及无符号整数\n", 66 | "\n", 67 | "在大多数情况下,使用带符号和无符号整数的速度没有什么区别。但是有一些值得注意的情况:\n", 68 | "\n", 69 | "- 除以常量:**整数被常量除的时候,无符号整数比有符号要快**(请参见第141页)。这也适用于取模运算符`%`。\n", 70 | "\n", 71 | "\n", 72 | "- 对于大多数指令集,**带符号的转换为浮点的速度比无符号整数的速度快**(请参见第145页)。\n", 73 | "\n", 74 | "\n", 75 | "- **溢出行为在有符号和无符号变量上表现不同**。\n", 76 | " - 无符号变量的溢出产生的小的正整数结果。\n", 77 | " - 有符号变量的溢出产生的没有被正式定义过。一般行为是将正溢出环绕转换为负值,但编译器可能会根据溢出不发生的假设进行优化,把溢出的分支处理给去除。\n" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "有符号和无符号整数之间的转换是无成本的。 这只不过是同一个位数据进行不同解释的问题。\n", 85 | "负整数转换为无符号整数时,将被当作为非常大的正数。\n", 86 | "\n", 87 | "\n", 88 | "```cpp\n", 89 | "// Example 7.4. Signed and unsigned integers\n", 90 | "int a, b;\n", 91 | "double c;\n", 92 | "b = (unsigned int)a / 10; // Convert to unsigned for fast division\n", 93 | "c = a * 2.5; // Use signed when converting to double\n", 94 | "```\n", 95 | "\n", 96 | "**\n", 97 | "在例7.4中,我们将`a`转换为无符号,以使分区更快。\n", 98 | "当然,这只有在确定`a`永远不会为负的情况下才有效。\n", 99 | "最后一行隐含地将`a`转换为`double`,然后与`double`常数`2.5`相乘。在这里当要转换成double时,我们更倾向于使用有符号数。\n", 100 | "**\n", 101 | "\n", 102 | "请务必**不要在比较中混合使用有符号和无符号整数**,例如`<`。 比较有符号与无符号整数的结果不明确,可能会产生不希望的结果" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "### 整数运算\n", 110 | "\n", 111 | "整数操作通常非常快。\n", 112 | "简单的整数运算,例如加法,减法,比较,位操作和移位操作,在大多数微处理器上只需要一个时钟周期。\n", 113 | "\n", 114 | "乘法和除法需要更长的时间。\n", 115 | "整数乘法在Pentium 4处理器上需要11个时钟周期,在大多数其他微处理器上需要3-4个时钟周期。\n", 116 | "**整数除法需要40-80个时钟周期,具体取决于微处理器。**\n", 117 | "整数除法在AMD处理器上,整数宽度越小,速度越快。但在英特尔处理器上不是这样。\n", 118 | "有关指令等待时间的详细信息在手册4:“指令表”中列出。\n", 119 | "有关如何加速乘法和除法的提示分别在第140页和第141页给出。" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "### 自增和自减运算\n", 127 | "\n", 128 | "前加运算符 `++i` 和后加运算符 `i++` 的速度与加法速度一样快。\n", 129 | "当用于简单地自增一个整数变量时,使用前加还是后加没有区别。效果完全相同。\n", 130 | "\n", 131 | "例如:对于`(i = 0; i < n; i++)` 与 `(i = 0; i < n; ++i)`相同。\n", 132 | "**但是,当在表达式中使用时,效率可能会有所不同。**\n", 133 | "**例如,`x = array[i++]`比`x = array[++i]`更高效**,因为在后一种情况下,数组元素地址的计算必须等待`i`的新值,这会延迟大约两个时钟周期后,`x`才可用。\n", 134 | "显然,如果将前增量更改为后增量,则必须调整`i`的初始值。\n", 135 | "\n", 136 | "**还有一种情况,前加比后加更有效。**\n", 137 | "例如,在`a = ++b`的情况下,编译器会认识到,在这个语句之后,a和b的值是相同的,这样它就可以为这两者使用相同的寄存器,而表达式`a = b++;` 将使a和b的值不同,它们不能使用相同的寄存器。\n", 138 | "\n", 139 | "这里所说的有关自增运算符的所有内容,也适用于整数变量的自减运算符。" 140 | ] 141 | } 142 | ], 143 | "metadata": { 144 | "kernelspec": { 145 | "display_name": "C++17", 146 | "language": "C++", 147 | "name": "cling-cpp17" 148 | }, 149 | "language_info": { 150 | "codemirror_mode": "c++", 151 | "file_extension": ".c++", 152 | "mimetype": "text/x-c++src", 153 | "name": "c++" 154 | } 155 | }, 156 | "nbformat": 4, 157 | "nbformat_minor": 2 158 | } 159 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.03-浮点变量和操作符.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.3 浮点变量和操作符\n", 8 | "\n", 9 | "x86系列中的现代微处理器具有两种不同类型的浮点寄存器,以及相应的两种不同类型的浮点指令。每种类型都有其优点和缺点。\n", 10 | "\n", 11 | "**执行浮点操作的原先的方法涉及到8个浮点寄存器,组织为寄存器堆栈。**\n", 12 | "这些寄存器具有长双精度(80位)。使用寄存器堆栈的优点是:\n", 13 | "\n", 14 | "- 所有计算均以长双精度(long double)完成。\n", 15 | "- 不同精度之间的转换不需要额外的时间。\n", 16 | "- 对于数学函数,如对数和三角函数,内部指令来处理。\n", 17 | "- 代码紧凑,在代码缓存中占用很少的空间。\n", 18 | "\n", 19 | "寄存器堆栈也有缺点:\n", 20 | "- 由于寄存器栈的组织方式,编译器难以创建寄存器变量。\n", 21 | "- 除非启用了Pentium-II或更高版本的指令集,否则浮点比较很慢。\n", 22 | "- 整数和浮点数之间的转换效率低下。\n", 23 | "- 当使用长双精度时,除法,平方根等数学函数消耗更多时间用于计算。" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "有一种新的浮点运算方法,涉及八个或十六个向量寄存器(XMM或YMM)。这些向量寄存器可用于多种用途。\n", 31 | "浮点运算以单精度或双精度完成,中间结果始终以与操作数相同的精度进行计算。\n", 32 | "使用矢量寄存器的优点是:\n", 33 | "\n", 34 | "- 创建浮点寄存器变量很容易。\n", 35 | "- 向量操作可用来对XMM寄存器中的两个双精度变量,或四个单精度变量的向量,执行并行计算(参见第108页)。\n", 36 | "如果AVX指令集可用,则每个矢量可以在YMM寄存器中保存四个双精度或八个单精度变量。\n", 37 | "\n", 38 | "缺点是:\n", 39 | "- 不支持长双精度。\n", 40 | "- 对于操作数具有混合精度的表达式的计算,需要精确的转换指令,这可能非常耗时(请参见第144页)。\n", 41 | "- 数学函数必须使用函数库,但这通常比内在硬件函数更快。" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "关于浮点寄存器堆栈和XMM,YMM的可用性:\n", 49 | "- 所有具有浮点功能的系统,都可以使用浮点堆栈寄存器(64位Windows的设备驱动程序除外)。\n", 50 | "- 如果系统支持SSE2或更高版本指令集(单精度仅需要SSE),XMM矢量寄存器可用于64位系统和32位系统。\n", 51 | "- 如果处理器和操作系统支持AVX指令集,则YMM寄存器可用。 \n", 52 | "\n", 53 | "有关如何测试这些指令集的可用性,请参阅第125页。\n", 54 | "\n", 55 | "**大多数编译器在浮点计算都会使用XMM寄存器进行浮点运算,只要条件支持,即64位模式或SSE2指令集启用时。**\n", 56 | "少部分编译器能够支持这两种类型的浮点运算的混合,并为每种计算选择最优的类型。\n", 57 | "\n", 58 | "在大多数情况下,**双精度计算不会比单精度花费更多的时间。**\n", 59 | "当使用浮点寄存器时,单精度和双精度之间的速度没有区别。\n", 60 | "长双精度(long double)只需要稍微多一点的时间。\n", 61 | "在使用XMM寄存器时,对于大多数处理器,当不用矢量运算时,\n", 62 | "- 单精度除法,平方根和数学函数的计算速度快于双精度,\n", 63 | "- 而加,减,乘等速度仍然相同,无论精度如何。\n", 64 | "\n", 65 | "所以:\n", 66 | "- **如果对应用程序有好处,您可以使用双精度而不必担心成本太高。**\n", 67 | "- **如果您有大数组并希望尽可能多地将数据存入数据缓存,则可以使用单精度。**\n", 68 | "- **如果您可以利用矢量操作,单精度是很好的选择,如第108页所述。**\n", 69 | "\n", 70 | "\n", 71 | "取决于微处理器的不同,\n", 72 | "- 浮点加法需要3至6个时钟周期。\n", 73 | "- 乘法需要4至8个时钟周期。\n", 74 | "- 除法需要14-45个时钟周期。\n", 75 | "\n", 76 | "使用浮点堆栈寄存器时,**浮点比较指令效率低下。**\n", 77 | "使用浮点堆栈寄存器时,**浮点或双精度浮点转换为整数需要很长时间。**\n", 78 | "在使用XMM寄存器时,**不要混合使用单精度和双精度。**见第144页。\n", 79 | "\n", 80 | "如果可能,**避免整数和浮点变量之间的转换**。见第144页。" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "在XMM寄存器中产生浮点下溢的应用程序,可以设置`flush-to-zero`模式(清洗至零模式),而不是在下溢的情况下生成非规格化(subnormal)小数:\n", 88 | "\n", 89 | "```cpp\n", 90 | "// Example 7.5. Set flush-to-zero mode (SSE):\n", 91 | "#include \n", 92 | "_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);\n", 93 | "```\n", 94 | "\n", 95 | "强烈建议设置`flash-to-zero`模式,除非你有特殊原因使用非规格化小数。\n", 96 | "另外,如果SSE2可用,你可以设置`denormals-are-zero`模式:\n", 97 | "\n", 98 | "It is strongly recommended to set the flush-to-zero mode unless you have special reasons\n", 99 | "to use subnormal numbers.\n", 100 | "\n", 101 | "You may, in addition, set the denormals-are-zero mode if SSE2 is available:\n", 102 | "\n", 103 | "```cpp\n", 104 | "// Example 7.6. Set flush-to-zero and denormals-are-zero mode (SSE2):\n", 105 | "#include \n", 106 | "_mm_setcsr(_mm_getcsr() | 0x8040);\n", 107 | "```\n", 108 | "\n", 109 | "有关数学函数的更多信息,请参阅第149和122页。" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "译者注(来自网络):\n", 117 | "- 在SSE和SSE2指令集中,有两种模式FTZ( Flush-to-Zero)和DAZ(Denormals-Are-Zero)帮助我们提高非规格化小数的运算速度。\n", 118 | "- 其中FTZ的意思是当运算结果产生非规格化小数时,FTZ模式将运算结果设置为0。\n", 119 | "- 而DAZ的意思是当操作数有非规格化小数时,先将它设置为0再与其他操作数进行运算。\n", 120 | "- 也可以说FTZ影响输出结果而DAZ影响输入结果。" 121 | ] 122 | } 123 | ], 124 | "metadata": { 125 | "kernelspec": { 126 | "display_name": "C++17", 127 | "language": "C++", 128 | "name": "cling-cpp17" 129 | }, 130 | "language_info": { 131 | "codemirror_mode": "c++", 132 | "file_extension": ".c++", 133 | "mimetype": "text/x-c++src", 134 | "name": "c++" 135 | } 136 | }, 137 | "nbformat": 4, 138 | "nbformat_minor": 2 139 | } 140 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.04-7.05.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.4 枚举\n", 8 | "\n", 9 | "一个`enum`只不过是一个伪装的整数。枚举和整数有一样效率。\n", 10 | "\n", 11 | "请注意,枚举(值名称)将与具有相同名称的任何变量或函数冲突。\n", 12 | "因此,头文件中的枚举应具有长且唯一的枚举器名称,或放入命名空间中。" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "## 7.5 布尔类型\n", 20 | "\n", 21 | "### 布尔运算的顺序\n", 22 | "\n", 23 | "布尔运算符`&&`和`||`的操作数,按以下方式进行计算。\n", 24 | "- 如果&&的第一个操作数为`false`,那么第二个操作数根本就不计算,因为无论第二个操作数的值如何,结果都是`false`。\n", 25 | "- 同样,如果`||`的第一个操作数是`true`,那么第二个操作数就不会被评估,因为结果无论如何都是`true`。\n", 26 | "\n", 27 | "将最可能值为`true`的操作数放在`&&`表达式的最后一个,或者放在`||`表达式的第一个,这可能是对性能有利的。\n", 28 | "例如,假设`a`为`true`的时间为50%,`b`为`true`时间为10%。\n", 29 | "- 当`a`为`true`时,表达式`a && b`还需要评估`b`,这占50%的情况。\n", 30 | "- 等价表达式`b && a`只需在`b`为`true`时评估,这只占10%的情形。\n", 31 | "如果`a`和`b`分别需要相同的时间进行评估,并且可能通过分支预测机制进行预测,则(第二种方法)速度会更快。\n", 32 | "有关分支预测的解释,请参阅第44页。\n", 33 | "\n", 34 | "**如果一个操作数比另一个操作数更具可预测性,那么把更可预测的操作数放前面。**\n", 35 | "\n", 36 | "**如果一个操作数比另一个操作数计算得更快,那么首先把最快计算得出的操作数放在前面。**" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "但是,在交换布尔操作数的顺序时必须小心。\n", 44 | "如果操作数的评估有副作用,或者需要第一个操作数确定第二个操作数是否有效,则不能交换操作数。 例如:\n", 45 | "\n", 46 | "```cpp\n", 47 | "// Example 7.7\n", 48 | "unsigned int i;\n", 49 | "const int ARRAYSIZE = 100;\n", 50 | "float list[ARRAYSIZE];\n", 51 | "if (i < ARRAYSIZE && list[i] > 1.0) { ...\n", 52 | "```\n", 53 | "\n", 54 | "这里,你不能交换操作数的顺序,因为当变量`i`不小于ARRAYSIZE时,表达式列表`[i]`是无效的。 另一个例子:\n", 55 | "\n", 56 | "```cpp\n", 57 | "// Example 7.8\n", 58 | "if (handle != INVALID_HANDLE_VALUE && WriteFile(handle, ...)) { ...\n", 59 | "```\n", 60 | "\n", 61 | "这里,你不能交换布尔操作数的顺序,因为如果句柄无效,你不应该调用`WriteFile`。\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "### 布尔变量是超定的\n", 69 | "\n", 70 | "布尔变量存储为8位整数,值为0代表假,1代表真。\n", 71 | "\n", 72 | "布尔变量是超定的(过度确认),因为\n", 73 | "- 所有具有布尔变量作为输入的运算符都检查输入是否具有除0或1以外的其他值,\n", 74 | "- 但具有布尔值作为输出的运算符不能产生除0或1之外的其他值。\n", 75 | "\n", 76 | "这使得使用布尔作为输入变量,操作的效率有些不必要的要低。举个例子:\n", 77 | "\n", 78 | "```cpp\n", 79 | "// Example 7.9a\n", 80 | "bool a, b, c, d;\n", 81 | "c = a && b;\n", 82 | "d = a || b;\n", 83 | "```\n" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "This is typically implemented by the compiler in the following way:\n", 91 | "```cpp\n", 92 | "bool a, b, c, d;\n", 93 | "if (a != 0) {\n", 94 | " if (b != 0) {\n", 95 | " c = 1;\n", 96 | " }\n", 97 | " else {\n", 98 | " goto CFALSE;\n", 99 | " }\n", 100 | "}\n", 101 | "else {\n", 102 | " CFALSE:\n", 103 | " c = 0;\n", 104 | "}\n", 105 | "if (a == 0) {\n", 106 | " if (b == 0) {\n", 107 | " d = 0;\n", 108 | " }\n", 109 | " else {\n", 110 | " goto DTRUE;\n", 111 | " }\n", 112 | "}\n", 113 | "else {\n", 114 | " DTRUE:\n", 115 | " d = 1;\n", 116 | "}\n", 117 | "```\n", 118 | "\n", 119 | "这段代码显然离最优还差得很远。\n", 120 | "如果发现预测失误,分支可能需要很长时间(请参阅第44页)。\n", 121 | "如果可以肯定地知道操作数没有0和1以外的其它值,布尔操作可以更有效率。\n", 122 | "编译器之所以没有做出这种假设,这是因为,如果变量未初始化或来自未知来源,则变量可能具有其它值。\n", 123 | "如果`a`和`b`已经初始化为有效值,或者它们来自产生布尔输出的运算符,则上面的代码可以被优化。\n", 124 | "优化的代码如下所示:\n", 125 | "\n", 126 | "```cpp\n", 127 | "// Example 7.9b\n", 128 | "char a = 0, b = 0, c, d;\n", 129 | "c = a & b;\n", 130 | "d = a | b;\n", 131 | "```" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "在这里,我使用了`char`(或`int`)而不是`bool`,以便使用位运算符(`&`和`|`)而不是布尔运算符(`&&`和`||`)。\n", 139 | "按位运算符是只需要一个时钟周期的一组单指令。\n", 140 | "即使`a`和`b`的值不等于0或1,`OR`运算符(`|`)也能工作。\n", 141 | "如果操作数的值不等于0和1,`AND`运算符(`&`)和异或运算符(`^`)可能会有除0和1之外的其它结果。\n", 142 | "\n", 143 | "请注意,这里有一些坑。 `NOT`不能使用`~`。\n", 144 | "替代的方法是,你可以在已知为0或1的变量上通过与1进行异或运算来取反:\n", 145 | "\n", 146 | "```cpp\n", 147 | "// Example 7.10a\n", 148 | "bool a, b;\n", 149 | "b = !a;\n", 150 | "```\n", 151 | "可以被优化为:\n", 152 | "```cpp\n", 153 | "// Example 7.10b\n", 154 | "char a = 0, b;\n", 155 | "b = a ^ 1;\n", 156 | "```\n", 157 | "\n", 158 | "当a为假,如果b是一个不应被评估的表达式,则不能用`a & b`替换`a && b`。\n", 159 | "同样,当a为真,如果b是一个表达式,你不能把`a || b`替换为`a | b`,则该表达式也不应被评估。\n", 160 | "\n", 161 | "操作数是变量的情况,比起操作数是比较等表达式的情况,使用按位运算符的技巧等更有利。例如:\n", 162 | "\n", 163 | "```cpp\n", 164 | "// Example 7.11\n", 165 | "bool a; float x, y, z;\n", 166 | "a = x > y && z != 0;\n", 167 | "```\n", 168 | "\n", 169 | "这段代码在大多数情况下是最优的。除非预计`&&`表达式会产生很多分支预测错误,否则请勿把`&&`改成`&`。" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "### 布尔向量操作\n", 177 | "\n", 178 | "一个整数可以用作布尔向量。\n", 179 | "例如,如果`a`和`b`是32位整数,则表达式`y = a & b;` 将在一个时钟周期内完成32个与操作。\n", 180 | "运算符 `&`, `|`, `^`, `~` 对布尔向量运算很有用。" 181 | ] 182 | } 183 | ], 184 | "metadata": { 185 | "kernelspec": { 186 | "display_name": "Python 3", 187 | "language": "python", 188 | "name": "python3" 189 | }, 190 | "language_info": { 191 | "codemirror_mode": { 192 | "name": "ipython", 193 | "version": 3 194 | }, 195 | "file_extension": ".py", 196 | "mimetype": "text/x-python", 197 | "name": "python", 198 | "nbconvert_exporter": "python", 199 | "pygments_lexer": "ipython3", 200 | "version": "3.6.7" 201 | } 202 | }, 203 | "nbformat": 4, 204 | "nbformat_minor": 2 205 | } 206 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.06-指针和引用.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.6 指针和引用\n", 8 | "\n", 9 | "### 指针与引用\n", 10 | "\n", 11 | "指针和引用有同样的效率,因为它们实际上是在做同样的事情。\n", 12 | "例如:\n", 13 | "\n", 14 | "```cpp\n", 15 | "// Example 7.12\n", 16 | "void FuncA (int * p) {\n", 17 | " *p = *p + 2;\n", 18 | "}\n", 19 | "void FuncB (int & r) {\n", 20 | " r = r + 2;\n", 21 | "}\n", 22 | "```" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "这两个函数是做同样的事情,如果你看看编译器生成的代码,你会注意到这两个函数的生成代码完全相同。\n", 30 | "区别仅仅是编程风格的问题。\n", 31 | "使用指针而不是引用的优点是:\n", 32 | "\n", 33 | "- 当您查看上面的函数体时,很明显`p`是一个指针,但不清楚`r`是参考变量还是简单变量。\n", 34 | "使用指针可以让读者更清楚发生了什么。\n", 35 | "- 有可能用引用不可能的指针来做事情。\n", 36 | "您可以更改指针指向的内容,并可以对指针进行算术运算。\n", 37 | "\n", 38 | "使用引用而不是指针的优点是:\n", 39 | "\n", 40 | "- 使用引用时语法更简单。\n", 41 | "- 引用比指针更安全,因为在大多数情况下,它们确定会指向有效的地址。\n", 42 | "如果指针未被初始化,或者指针的算术计算超出了有效地址的范围,或者指针被转换到错误的类型,则指针会无效并导致致命错误。\n", 43 | "- 引用对拷贝构造函数和运算符重载很有用。\n", 44 | "- **被声明为常量引用的函数参数接受表达式作为参数**,而指针和非常量引用则需要一个变量。" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "### 效率\n", 52 | "\n", 53 | "通过指针或引用访问变量或对象,可以和直接访问变量或对象一样快。\n", 54 | "能达到这样的效率,其原因在于微处理器的构建方式。\n", 55 | "在函数内声明的所有非静态变量和对象,都存储在堆栈中。因此,实际上这些变量和对象是相对于堆栈指针进行编址的。\n", 56 | "同样,在类中声明的所有非静态变量和对象,都可以通过C++中已知的隐式指针`this`来访问。\n", 57 | "因此,我们可以得出结论,一个结构良好的C++程序,大多数变量事实上都是以某种方式通过指针访问的。\n", 58 | "因此,微处理器不得不设计成便于高效地使用指针,这就是它们的原理。\n", 59 | "\n", 60 | "但是,使用指针和引用存在缺点。\n", 61 | "最重要的是,它需要一个额外的寄存器来保存指针或引用的值。\n", 62 | "寄存器是一种稀缺资源,特别是在32位模式下。\n", 63 | "如果没有足够的寄存器,则每次使用指针时都必须从内存中加载指针,这会使程序变慢。\n", 64 | "另一个缺点是,在指针指向的变量可以访问之前,还需要几个时钟周期,以便于获取指针本身的值。" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "### 指针运算\n", 72 | "\n", 73 | "指针实际上是一个包含内存地址的整数。\n", 74 | "因此指针算术运算与整数算术运算一样快。\n", 75 | "当一个整数被加到到一个指针上,实际加的是整数的值乘以指向的对象的大小。 例如:\n", 76 | "\n", 77 | "```cpp\n", 78 | "// Example 7.13\n", 79 | "struct abc {int a; int b; int c;};\n", 80 | "abc * p; int i;\n", 81 | "p = p + i;\n", 82 | "```\n", 83 | "\n", 84 | "这里,添加到`p`的值不是`i`而是`i * 12`,因为`abc`的大小是12个字节。\n", 85 | "将`i`加到`p`所需的时间等于进行乘法和加法所花费的时间。\n", 86 | "如果`abc`的大小是`2`的幂,那么乘法可以用快得多的移位操作来代替。\n", 87 | "在上面的例子中,通过在结构中增加一个整数,`abc`的大小可以增加到16个字节。\n", 88 | "\n", 89 | "递增或递减指针不需要乘法,只需要加法。\n", 90 | "比较两个指针只需要一个整数比较,速度很快。\n", 91 | "**计算两个指针之间的差异需要除法,除非指向的对象类型的大小是2的幂(请参阅第141页有关除法),否则这种除法很慢。**\n", 92 | "\n", 93 | "在计算出指针的值后大约需要两个时钟周期,可以访问指向的对象。\n", 94 | "因此,建议在使用指针之前计算好指针的值。\n", 95 | "**例如,`x = *(p++)` 比`x = *(++p)` 更有效**,因为\n", 96 | "- 在后一种情况下,`x`的读数必须等到指针p增加后的几个时钟周期,\n", 97 | "- 而在前一种情况下,`x`可以在`p`递增之前读取。\n", 98 | "\n", 99 | "有关自增和自减运算符的更多讨论,请参见第31页。" 100 | ] 101 | } 102 | ], 103 | "metadata": { 104 | "kernelspec": { 105 | "display_name": "C++17", 106 | "language": "C++", 107 | "name": "cling-cpp17" 108 | }, 109 | "language_info": { 110 | "codemirror_mode": "c++", 111 | "file_extension": ".c++", 112 | "mimetype": "text/x-c++src", 113 | "name": "c++" 114 | } 115 | }, 116 | "nbformat": 4, 117 | "nbformat_minor": 2 118 | } 119 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.07-7.09.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.7 函数指针\n", 8 | "\n", 9 | "如果可以预测目标地址,通过函数指针调用函数通常需要比直接调用函数多几个时钟周期。\n", 10 | "- 如果函数指针的值与上次执行语句时的值相同,则目标地址可以被预测。\n", 11 | "- 如果函数指针的值已经改变,那么目标地址很可能会被错误预测,这会导致很长的延迟。\n", 12 | "\n", 13 | "关于分支预测请参阅第44页。\n", 14 | "如果函数指针的变化遵循简单的常规模式,Pentium M处理器可能能够预测目标,而Pentium 4和AMD处理器在每次函数指针发生变化时,肯定会做出错误的预测。" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "## 7.8 成员指针\n", 22 | "\n", 23 | "在简单情况下,\n", 24 | "- 数据成员指针仅存储数据成员相对于对象开始的偏移量,\n", 25 | "- 而成员函数指针只是成员函数的地址。\n", 26 | "\n", 27 | "但是还有一些特殊情况,例如需要更复杂实现的多重继承的情形。\n", 28 | "这些复杂的情况一定要避免。\n", 29 | "\n", 30 | "如果没有关于成员指针引用的类的完整信息, 编译器必须使用最复杂的成员指针的实现。 例如:\n", 31 | "\n", 32 | "```cpp\n", 33 | "// Example 7.14\n", 34 | "class c1;\n", 35 | "int c1::*MemberPointer;\n", 36 | "```\n", 37 | "\n", 38 | "这里,在声明`MemberPointer`时,编译器没有关于类`c1`,除了名字以外的信息。\n", 39 | "因此,它必须假定最糟糕的情况,并对成员指针进行复杂的实现。\n", 40 | "这可以通过在声明`MemberPointer`之前,对`c1`完全声明来避免。\n", 41 | "多重继承,虚拟函数和其他使成员指针效率降低的复杂因素,这些都需要避免。\n", 42 | "\n", 43 | "\n", 44 | "大多数C++编译器都有各种选项来控制成员指针的实现方式。\n", 45 | "如果可能,请使用可能是最简单实现的选项,并确保对所有使用相同成员指针的模块使用相同的编译器选项。" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## 7.9 智能指针\n", 53 | "\n", 54 | "智能指针是一个拥有像指针一样行为的对象。\n", 55 | "它具有特殊的功能,当指针被删除时,它指向的对象被删除。\n", 56 | "智能指针仅用于存储在动态分配的内存中的,使用`new`操作符生成的对象。\n", 57 | "使用智能指针的目的是,当对象不再使用时,确保对象被正确删除,并且释放内存。\n", 58 | "智能指针可被认为是仅包含单个元素的容器。\n", 59 | "\n", 60 | "智能指针的最常见实现是`auto_ptr`和`shared_ptr`。\n", 61 | "`auto_ptr`具有一个特性,即总是有一个且仅有一个`auto_ptr`拥有分配的对象,并且通过赋值,会将所有权从一个`auto_ptr`转移到另一个`auto_ptr`。\n", 62 | "`shared_ptr`允许指向同一对象的多个指针。\n", 63 | "\n", 64 | "(译者注:`auto_ptr`已经过时,使用`unique_ptr`)\n", 65 | "\n", 66 | "通过智能指针访问对象不需要额外的代价。\n", 67 | "通过`*p`或`p->member`访问对象的速度同样快,无论`p`是简单指针还是智能指针。\n", 68 | "但是,每当智能指针被创建,删除,复制或从一个函数转移到另一个函数时,都会产生额外的成本。\n", 69 | "shared_ptr的成本要高于auto_ptr的成本。\n", 70 | "\n", 71 | "智能指针在此情况下是很有用的:\n", 72 | "程序的逻辑结构要求一个对象必须由一个函数动态创建,并且随后被另一个函数删除,并且这两个函数彼此不相关(不是相同类的成员)的。\n", 73 | "如果相同的函数或类负责创建和删除对象,则你不需要智能指针。\n", 74 | "\n", 75 | "如果一个程序使用许多小动态分配的对象,每个对象使用智能指针,那么你可能需要考虑这个解决方案的成本是否太高。\n", 76 | "将所有对象集中池化在一个容器中,容器最好是是使用连续的内存,这样可能更有效率。\n", 77 | "请参阅第95页的关于容器类的讨论。\n" 78 | ] 79 | } 80 | ], 81 | "metadata": { 82 | "kernelspec": { 83 | "display_name": "Python 3", 84 | "language": "python", 85 | "name": "python3" 86 | }, 87 | "language_info": { 88 | "codemirror_mode": { 89 | "name": "ipython", 90 | "version": 3 91 | }, 92 | "file_extension": ".py", 93 | "mimetype": "text/x-python", 94 | "name": "python", 95 | "nbconvert_exporter": "python", 96 | "pygments_lexer": "ipython3", 97 | "version": "3.6.7" 98 | } 99 | }, 100 | "nbformat": 4, 101 | "nbformat_minor": 2 102 | } 103 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.10-数组.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.10 数组\n", 8 | "\n", 9 | "数组是通过简单地通过将元素连续存储在存储器中来实现的。\n", 10 | "关于数组维度的信息不会被存储。\n", 11 | "这使得在C和C++中使用数组比在其他编程语言中使用更快,但也更不安全。\n", 12 | "通过定义一个类似于具有边界检查的数组的容器类,这个安全问题可以被克服,如下例所示:\n", 13 | "\n", 14 | "```cpp\n", 15 | "// Example 7.15a. Array with bounds checking\n", 16 | "template class SafeArray {\n", 17 | "protected:\n", 18 | " T a[N]; // Array with N elements of type T\n", 19 | "public:\n", 20 | " SafeArray() { // Constructor\n", 21 | " memset(a, 0, sizeof(a)); // Initialize to zero\n", 22 | " }\n", 23 | " int Size() { // Return the size of the array\n", 24 | " return N;\n", 25 | " }\n", 26 | " T & operator[] (unsigned int i) { // Safe [] array index operator\n", 27 | " if (i >= N) {\n", 28 | " // Index out of range. The next line provokes an error.\n", 29 | " // You may insert any other error reporting here:\n", 30 | " return *(T*)0; // Return a null reference to provoke error\n", 31 | " }\n", 32 | " // No error\n", 33 | " return a[i]; // Return reference to a[i]\n", 34 | " }\n", 35 | "}\n", 36 | "```" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "更多关于容器类的例子:[www.agner.org/optimize/cppexamples.zip](www.agner.org/optimize/cppexamples.zip).\n", 44 | "\n", 45 | "上述模板类的数组将类型和大小指定为模板参数,对该类的使用,如下面的示例7.15b所示。\n", 46 | "就像普通数组一样,使用方括号索引访问元素。\n", 47 | "构造函数将所有元素设置为零。\n", 48 | "你可以删除该`memset`行,如果:\n", 49 | "- 不需要此初始化,\n", 50 | "- 或者类型T是一个类,有默认构造函数来执行必要初始化。\n", 51 | "\n", 52 | "编译器可能会报告`memset`已被弃用。\n", 53 | "这是因为如果`size`参数错误,它可能会导致错误,但`memset`仍然是将数组设置为零的最快方法。\n", 54 | "如果索引超出范围,`[]`操作符将检测到错误(请参阅第138页上的边界检查)。\n", 55 | "当错误消息被引发时,会通过返回空引用这种非常规的方式。\n", 56 | "当访问此空引用时,又会引发错误消息(在受保护的操作系统上),使用调试器很容易跟踪此错误。\n", 57 | "你可以替换此行,通过任何其他形式的错误报告。\n", 58 | "例如,在Windows中,你可以换成`FatalAppExitA(0,\"Array index out of range\");`,或更好的方法,或者使用你自己的错误信息函数。\n", 59 | "\n", 60 | "以下示例说明如何使用`SafeArray`:\n", 61 | "```cpp\n", 62 | "// Example 7.15b\n", 63 | "SafeArray list; // Make array of 100 floats\n", 64 | "for (int i = 0; i < list.Size(); i++) { // Loop through array\n", 65 | " cout << list[i] << endl; // Output array element\n", 66 | "}\n", 67 | "```\n", 68 | "\n", 69 | "由列表初始化的数组应该最好是静态的,如第27页所述。\n", 70 | "可以使用`memset`将数组初始化为0:\n", 71 | "```cpp\n", 72 | "// Example 7.16\n", 73 | "float list[100];\n", 74 | "memset(list, 0, sizeof(list));\n", 75 | "```\n", 76 | "\n", 77 | "对于多维数组,其设计和访问应该便于最后一个索引更改(频率)最快:\n", 78 | "```cpp\n", 79 | "// Example 7.17\n", 80 | "const int rows = 20, columns = 50;\n", 81 | "float matrix[rows][columns];\n", 82 | "int i, j; float x;\n", 83 | "for (i = 0; i < rows; i++)\n", 84 | " for (j = 0; j < columns; j++)\n", 85 | " matrix[i][j] += x;\n", 86 | "```\n", 87 | "\n", 88 | "这确保了元素被顺序访问。\n", 89 | "如果两个循环的对调,会使对内存访问没有顺序,导致数据缓存效率降低。" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "如果行以非连续顺序被索引,**除了第一维以外的所有维的大小可以最好是为2的幂**,以便使地址计算更高效:\n", 97 | "\n", 98 | "```cpp\n", 99 | "// Example 7.18\n", 100 | "int FuncRow(int); int FuncCol(int);\n", 101 | "const int rows = 20, columns = 32;\n", 102 | "float matrix[rows][columns];\n", 103 | "int i; float x;\n", 104 | "for (i = 0; i < 100; i++)\n", 105 | " matrix[FuncRow(i)][FuncCol(i)] += x;\n", 106 | "```\n", 107 | "\n", 108 | "在这里,代码必须计算`(FuncRow(i)*columns + FuncCol(i)) * sizeof(float)`才能找到矩阵元素的地址。\n", 109 | "当列数是2的幂时,在这种情况下列的乘法更快。\n", 110 | "在上例前面的示例中,这不是问题,因为优化的编译器可以看到行被连续访问,并且可以通过将行的长度加上前一行的地址,来计算每行的地址。\n", 111 | "\n", 112 | "该建议也适用于结构或类对象的数组。 如果以非顺序方式访问元素,则对象的大小(以字节为单位)应优选为2的幂。\n", 113 | "\n", 114 | "**将列数设置为2的幂的建议并不总适用于比一级数据高速缓存更大,并且非顺序访问的数组,因为这可能会导致缓存争用。\n", 115 | "有关此问题的讨论,请参阅第89页。**" 116 | ] 117 | } 118 | ], 119 | "metadata": { 120 | "kernelspec": { 121 | "display_name": "Python 3", 122 | "language": "python", 123 | "name": "python3" 124 | }, 125 | "language_info": { 126 | "codemirror_mode": { 127 | "name": "ipython", 128 | "version": 3 129 | }, 130 | "file_extension": ".py", 131 | "mimetype": "text/x-python", 132 | "name": "python", 133 | "nbconvert_exporter": "python", 134 | "pygments_lexer": "ipython3", 135 | "version": "3.6.7" 136 | } 137 | }, 138 | "nbformat": 4, 139 | "nbformat_minor": 2 140 | } 141 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.11-类型转换.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.11 类型转换\n", 8 | "\n", 9 | "C++语法有几种不同的类型转换方法:\n", 10 | "\n", 11 | "```cpp\n", 12 | "// Example 7.19\n", 13 | "int i;\n", 14 | "float f;\n", 15 | "f = i; // Implicit type conversion\n", 16 | "f = (float)i; // C-style type casting\n", 17 | "f = float(i); // Constructor-style type casting\n", 18 | "f = static_cast(i); // C++ casting operator\n", 19 | "```\n", 20 | "\n", 21 | "这些方法具有完全相同的效果。使用哪种方法是编程风格的问题。\n", 22 | "下面讨论不同类型转换的时间消耗。" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "### 有符号 / 无符号转换\n", 30 | "\n", 31 | "```cpp\n", 32 | "// Example 7.20\n", 33 | "int i;\n", 34 | "if ((unsigned int)i < 10) { ...\n", 35 | "```\n", 36 | "\n", 37 | "有符号和无符号整数之间的转换,只是编译器以不同的方式解释整数的位。\n", 38 | "这里没有溢出检查,代码不需要额外的时间。\n", 39 | "这些转换可以自由使用,而不会产生任何性能损失。" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "### 整型大小转换\n", 47 | "\n", 48 | "```cpp\n", 49 | "// Example 7.21\n", 50 | "int i;\n", 51 | "short int s;\n", 52 | "i = s;\n", 53 | "```\n", 54 | "\n", 55 | "- 对于有符号整数,通过扩展符号位将整数转换为更长的大小,\n", 56 | "- 对于无符号整数,则通过扩展为零位来将整数转换为更长的大小。\n", 57 | "\n", 58 | "如果转换源是算术表达式,这通常需要一个时钟周期。\n", 59 | "如果读取存储器中变量的值是时候,来完成大小转换,则通常不需要额外的时间,如例7.22所示。\n", 60 | "\n", 61 | "```cpp\n", 62 | "// Example 7.22\n", 63 | "short int a[100]; int i, sum = 0;\n", 64 | "for (i=0; i<100; i++) sum += a[i];\n", 65 | "```\n", 66 | "\n", 67 | "将整数转换为较小的大小,则简单地通过忽略较高的位来完成。不需要溢出检查。 例如:\n", 68 | "\n", 69 | "```cpp\n", 70 | "// Example 7.23\n", 71 | "int i; short int s;\n", 72 | "s = (short int)i;\n", 73 | "```\n", 74 | "\n", 75 | "这种转换不需要额外的时间。它只是存储32位整数的低16位。" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "### 浮点数精度转换\n", 83 | "\n", 84 | "- 在使用浮点寄存器堆栈时,`float`,`double`和`long double`之间的转换不需要额外的时间。\n", 85 | "- 当使用XMM寄存器时,它需要2到15个时钟周期(取决于处理器)。\n", 86 | "\n", 87 | "有关寄存器堆栈与XMM寄存器的说明,请参见第32页。 例:\n", 88 | "\n", 89 | "```cpp\n", 90 | "// Example 7.24\n", 91 | "float a; double b;\n", 92 | "a += b;\n", 93 | "```\n", 94 | "\n", 95 | "在这个例子中,**如果使用XMM寄存器,转换成本很高**。`a`和`b`应该是相同的类型以避免这种情况。\n", 96 | "有关进一步讨论,请参阅第144页。" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "### 整数到浮点数的转换\n", 104 | "\n", 105 | "将带符号整数转换为浮点数或双精度数据需要4到16个时钟周期,具体取决于处理器和所用寄存器的类型。\n", 106 | "**从无符号整数的转换需要更长的时间。\n", 107 | "如果没有溢出风险,先将无符号整数转换为有符号整数会更快:**\n", 108 | "\n", 109 | "```cpp\n", 110 | "// Example 7.25\n", 111 | "unsigned int u; double d;\n", 112 | "d = (double)(signed int)u; // Faster, but risk of overflow\n", 113 | "```\n", 114 | "\n", 115 | "通过用浮点变量替换整型变量,整数到浮点的转换有时可以避免。例如,待优化代码:\n", 116 | "\n", 117 | "```cpp\n", 118 | "// Example 7.26a\n", 119 | "float a[100]; int i;\n", 120 | "for (i = 0; i < 100; i++) a[i] = 2 * i;\n", 121 | "```\n", 122 | "\n", 123 | "**通过创建一个额外的浮点变量,可以避免在此示例中将`i`转换为浮点:**\n", 124 | "\n", 125 | "```cpp\n", 126 | "// Example 7.26b\n", 127 | "float a[100]; int i; float i2;\n", 128 | "for (i = 0, i2 = 0; i < 100; i++, i2 += 2.0f) a[i] = i2;\n", 129 | "```" 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "### 浮点到整型的转换\n", 137 | "\n", 138 | "**除非启用SSE2或更高版本的指令集,否则将浮点数转换为整数需要很长时间。通常需要50-100个时钟周期。**\n", 139 | "原因是C/C++标准规定使用截断模式,因浮点舍入模式要更改为截断,然后再次改回来。\n", 140 | "\n", 141 | "如果在代码的关键部分中存在浮点到整数转换,那么是有必要做一些事情是。\n", 142 | "可能的解决方案是:\n", 143 | "\n", 144 | "- 通过使用不同类型的变量来避免转换。\n", 145 | "- 将中间结果存储为浮点数,把转换移出最内层循环。\n", 146 | "- 使用64位模式或启用SSE2指令集(需要支持此功能的微处理器)。\n", 147 | "- 使用舍入而不是截断,并使用汇编语言创建舍入函数。有关舍入的详细信息,请参见第144页。" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "### 指针类型转换\n", 155 | "\n", 156 | "指针可以转换为其它不同类型的指针。\n", 157 | "同样,一个指针可以转换为一个整数,一个整数也可以转换为一个指针。\n", 158 | "整数有足够的位来保存指针是很重要的。\n", 159 | "\n", 160 | "这些转换不会产生任何额外的代码。\n", 161 | "转换只是以不同方式解释相同数据位,或绕过语法检查。\n", 162 | "\n", 163 | "当然,这些转换并不安全。程序员有责任确保结果有效。" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "metadata": {}, 169 | "source": [ 170 | "### 重新解释对象的类型\n", 171 | "\n", 172 | "通过对其地址进行类型转换,可以使编译器将变量或对象视为具有不同的类型:\n", 173 | "\n", 174 | "```cpp\n", 175 | "// Example 7.27\n", 176 | "float x;\n", 177 | "*(int*)&x |= 0x80000000; // Set sign bit of x\n", 178 | "```\n", 179 | "\n", 180 | "这里的语法可能有点奇怪。\n", 181 | "`x`的地址被类型转换为指向整数的指针,然后解引用该指针以便将`x`作为整数访问。\n", 182 | "编译器不会为生成该指针而产生任何额外的代码。\n", 183 | "指针被简单地优化掉了,结果是`x`被视为整数。\n", 184 | "**但是`&`运算符强制编译器将`x`存储在内存中,而不是寄存器中。**\n", 185 | "上面的例子使用`|`运算符来设置`x`的符号位。该运算符只能应用于整数。 它比`x = -abs(x);`更快。\n", 186 | "\n", 187 | "在指针进行类型转换时有许多危险需要注意:\n", 188 | "\n", 189 | "- 这个技巧违反了标准C的严格别名规则(strict aliasing rule),该规则指定不同类型的两个指针不能指向同一个对象(char指针除外)。\n", 190 | "优化的编译器可能把浮点数和整数存储在两个不同的寄存器中。\n", 191 | "您需要检查编译器是否会按照你预想的去工作。\n", 192 | "这里使用`union`更安全,如示例14.23第146页中所示。\n", 193 | "\n", 194 | "- 如果对象被视为比实际更大,那么该技巧将失败。如果int使用的位数多于浮点数,则上面的代码将失败。\n", 195 | "(两者都在x86系统中使用32位)。\n", 196 | "\n", 197 | "- 如果访问变量的一部分,例如64位双精度的其中32位,则代码将无法移植到使用大端存储的平台。\n", 198 | "\n", 199 | "- 如果一次访问一部分变量,例如,如果你分两次写入64位的两个32位,那么由于CPU中的存储转发延迟(store forwarding delay),代码可能执行速度比预期慢(参见手册3:“英特尔,AMD和VIA CPU的微体系结构”)。(译者注:存储转发延迟大概意思是先缓存好需要的数据然后一起发送,显然这里会影响性能)\n" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "### 常量强制转换\n", 207 | "\n", 208 | "`const_cast`运算符用于规避常量指针的常量限制。\n", 209 | "它具有语法检查,因此比C风格的类型转换更安全,且不需要加任何额外的代码。 例如:\n", 210 | "\n", 211 | "\n", 212 | "```cpp\n", 213 | "// Example 7.28\n", 214 | "class c1 {\n", 215 | " const int x; // constant data\n", 216 | "public:\n", 217 | " c1() : x(0) {}; // constructor initializes x to 0\n", 218 | " void xplus2() { // this function can modify x\n", 219 | " *const_cast(&x) += 2; // add 2 to x\n", 220 | " }\n", 221 | "};\n", 222 | "```\n", 223 | "\n", 224 | "这里`const_cast`运算符的作用是删除`x`上的`const`限制。\n", 225 | "这是一种规避语法限制的方法,但它不会生成任何额外的代码,也不会占用任何额外的时间。\n", 226 | "这是确保某个函数可以修改`x`的有用方法,而其它函数则不能修改。\n", 227 | "\n", 228 | "(译者注:实际未必需要这么麻烦,`(int&)x += 2;`,`const_cast(x) += 2;`都可以进行该转换)" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "### 静态强制转换\n", 236 | "`static_cast`运算符与C风格的类型转换相同。 例如,它用于将`float`转换为`int`。\n", 237 | "\n", 238 | "### 重新解释型强制转换\n", 239 | "`reinterpret_cast`运算符用于指针转换。\n", 240 | "它与C语言风格类型转换进行同样的操作,但需要更一点点的语法检查。\n", 241 | "它不会产生任何额外的代码。\n", 242 | "\n", 243 | "### 动态强制转换\n", 244 | "`dynamic_cast`运算符用于将指向一个类的指针转换为指向另一个类的指针。\n", 245 | "它使运行时检查转换是否有效。\n", 246 | "例如,当指向基类的指针转换为指向派生类的指针时,它会检查原始指针是否实际指向派生类的对象。\n", 247 | "这种检查使`dynamic_cast`比简单的类型转换更耗时,但也更安全。\n", 248 | "它可能会捕获到其它方法检测不到的编程错误。\n", 249 | "\n", 250 | "### 转换类的对象\n", 251 | "要进行类对象(而不是指向对象的指针)的转换,必须满足下面的条件:\n", 252 | "- 定义了构造函数\n", 253 | "- 重载了赋值运算符,或者重载了类型转换操作符(指定如何进行转换)\n", 254 | "\n", 255 | "构造函数或重载运算符,与成员函数效率相同。" 256 | ] 257 | } 258 | ], 259 | "metadata": { 260 | "kernelspec": { 261 | "display_name": "Python 3", 262 | "language": "python", 263 | "name": "python3" 264 | }, 265 | "language_info": { 266 | "codemirror_mode": { 267 | "name": "ipython", 268 | "version": 3 269 | }, 270 | "file_extension": ".py", 271 | "mimetype": "text/x-python", 272 | "name": "python", 273 | "nbconvert_exporter": "python", 274 | "pygments_lexer": "ipython3", 275 | "version": "3.6.7" 276 | } 277 | }, 278 | "nbformat": 4, 279 | "nbformat_minor": 2 280 | } 281 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.12-分支和switch语句.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.12 分支和 switch 语句\n", 8 | "\n", 9 | "现代微处理器的高速度是通过流水线获得的。流水线中,指令在执行前分成几个阶段来获取和解码。\n", 10 | "然而,流水线结构有一个大问题。一旦代码中有分支出现(例如,`if-else`),微处理器并不能预先知道该把两个分支中的哪一个送入流水线。\n", 11 | "如果错误的分支被喂给了流水线,该错误直到10至20个时钟周期后才能被发现,这期间所有的取指,解码,甚至推测性指令执行工作都被浪费掉了。\n", 12 | "结果是,只要微处理器将分支喂给流水线并稍后发现它选错了分支,微处理器就会浪费小部分时钟周期。\n", 13 | "\n", 14 | "微处理器设计者已经竭尽全力减轻了该问题的后果,使用的最重要的方法是分支预测。\n", 15 | "现代微处理器使用先进的算法,根据该分支及附近分支的历史数据来预测分支的走向。\n", 16 | "不同的微处理器使用的分支预测算法并不相同。在手册3:“Inter, AMD 和 VIA CPU 的微处理器架构”中有对这些算法的描述。\n", 17 | "\n", 18 | "在微处理器做出正确预测的情况下,分支指令通常需要0-2个时钟周期。从分支错误预测中恢复,所需的时间大约为12-25个时钟周期,具体取决于处理器。 这被称为分支错误预测惩罚。\n", 19 | "\n", 20 | "在大多数情况下的正确预测分支是相对代价较小,但如果它们经常被错误预测则代价高昂。\n", 21 | "- 显然,某个分支总重复相同的走法,预测会很准。\n", 22 | "- 如果某个大多数时间走法相同,但很少的情况下走法不同,该分支只会在不同走法时错误预测。\n", 23 | "- 如果一个分支多次重复一种方式,然后多次重复另一种,只有在变化时才会被错误预测。\n", 24 | "- 如果分支遵循简单的周期性的模式,也可以被很好的预测,当它是不包含其它分支,或者只包含很少其它分支的内层循环时。简单周期性模式的一个例子是,一种方式走两次,另一种方式走一次。然后,第一种方式走两次,第二种方式走三次,等等。\n", 25 | "- 最糟糕的情况是一个分支随机选择某种一条路,或者以50%-50%的概率走任意一条。 这样的分支将在50%的时间内被错误预测。\n", 26 | "\n", 27 | "for循环或while循环也是一种分支。每次循环迭代后,会决定是重复循环还是退出。\n", 28 | "如果循环计数很小并且不变,循环分支通常可以很好的被预测。\n", 29 | "可以完美预测的最大循环计数通常介于9到64之间,具体取决于处理器。\n", 30 | "仅仅某些处理器上可以较好地预测嵌套循环。\n", 31 | "在许多处理器上,一个循环内包含多个分支,不能很好地被预测。" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "switch 语句是一种超过两条路的分支。\n", 39 | "下面情形 switch 语句最为高效:\n", 40 | "- 每个 case 的标签遵循一个序列\n", 41 | "- 在该序列中,某个标签的当前值等于上一个标签加一。\n", 42 | "\n", 43 | "这是因为,这种情况可以使用跳转目标表来实现。\n", 44 | "具有许多标签的 switch 语句,并且标签的值离得很远,该情形是很低效的,因为编译器必须将其转换为分支树。\n", 45 | "\n", 46 | "在较老的处理器上,对于带有顺序标签的switch语句,CPU仅仅重复上次执行时的方式来进行分支预测。\n", 47 | "因此,只要当程序实际走入分支中的另一条路时,都肯定会被错误预测。\n", 48 | "\n", 49 | "较新的处理器有时能够较好的预测switch语句,\n", 50 | "- 如果它遵循简单的周期性模式,\n", 51 | "- 或者它与前面的分支相关,并且不同目标的数量很小。(原文:if it is correlated with preceding branches and the number of different targets is small.)\n", 52 | "\n", 53 | "**在程序的关键部分,分支和switch语句的数量最好保持较小,特别是如果分支的可预测性很差。**\n", 54 | "如果可以消除分支,则展开循环可能很有用。下一段将会细讲。" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "分支和函数调用的目标(地址)保存在称为分支目标缓冲区的特殊高速缓存中。\n", 62 | "如果程序具有许多分支或函数调用,则可能在分支目标缓冲区中发生争用。\n", 63 | "这种争用的后果是,即使在其它情况下,分支预测得很好,分支也可能被错误预测。\n", 64 | "甚至是函数调用也可能因此被错误预测。\n", 65 | "**因此,在代码的关键部分,具有许多很多分支和函数调用的程序可能导致错误的分支预测。**\n", 66 | "\n", 67 | "在某些情况下,可以通过查找表来替换难以预测的分支。 例如:\n", 68 | "\n", 69 | "```cpp\n", 70 | "// Example 7.29a\n", 71 | "float a; bool b;\n", 72 | "a = b ? 1.5f : 2.6f;\n", 73 | "```\n", 74 | "\n", 75 | "这里`?:`操作符就是一个分支。如果它很难被预测,就用一个查找表来代替:\n", 76 | "\n", 77 | "```cpp\n", 78 | "// Example 7.29b\n", 79 | "float a; bool b = 0;\n", 80 | "const float lookup[2] = {2.6f, 1.5f};\n", 81 | "a = lookup[b];\n", 82 | "```\n", 83 | "如果将bool用作数组索引,则确保它正确的初始化,或自可靠来源非常重要,这样它就不能具有0或1以外的其他值。请参见第34页。\n", 84 | "\n", 85 | "在某些情况下,编译器可以通过“条件移动”指令自动替换分支,具体取决于指定的指令集。\n", 86 | "\n", 87 | "第138页和第139页的示例显示了减少分支数量的各种方法。\n", 88 | "\n", 89 | "手册3:“Intel,AMD和VIA CPU的微体系结构”提供了有关不同微处理器中分支预测的更多详细信息。" 90 | ] 91 | } 92 | ], 93 | "metadata": { 94 | "kernelspec": { 95 | "display_name": "Python 3", 96 | "language": "python", 97 | "name": "python3" 98 | }, 99 | "language_info": { 100 | "codemirror_mode": { 101 | "name": "ipython", 102 | "version": 3 103 | }, 104 | "file_extension": ".py", 105 | "mimetype": "text/x-python", 106 | "name": "python", 107 | "nbconvert_exporter": "python", 108 | "pygments_lexer": "ipython3", 109 | "version": "3.6.5" 110 | } 111 | }, 112 | "nbformat": 4, 113 | "nbformat_minor": 2 114 | } 115 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.13-循环.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.13 循环\n", 8 | "\n", 9 | "循环的效率取决于微处理器对循环控制分支预测的好坏。\n", 10 | "有关分支预测的说明,请参阅前一段和手册3:“Intel,AMD和VIA CPU的微架构”。\n", 11 | "对于循环次数固定且较小的循环,并且循环内部没有分支的情形,循环控制分支可以被完美的预测。\n", 12 | "如上所述,可以预测的最大循环次数取决于处理器类型。\n", 13 | "对于嵌套循环,\n", 14 | "- 仅仅某些具有特殊的循环预测器的处理器可以预测得很好。\n", 15 | "- 在其它处理器上,仅仅最内层循环可以被很好的预测。\n", 16 | "\n", 17 | "重复计数次数很大的循环仅仅在退出循环是才被错误预测。\n", 18 | "例如,如果一个循环重复一千次,那么循环控制分支在一千次中只被错误预测一次。因此错误预测惩罚对总执行时间的贡献可以忽略不计。" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### 循环展开\n", 26 | "\n", 27 | "在某些情况下,展开循环是一个优势。 例如:\n", 28 | "```cpp\n", 29 | "// Example 7.30a\n", 30 | "int i;\n", 31 | "for (i = 0; i < 20; i++) {\n", 32 | " if (i % 2 == 0) {\n", 33 | " FuncA(i);\n", 34 | " }\n", 35 | " else {\n", 36 | " FuncB(i);\n", 37 | " }\n", 38 | " FuncC(i);\n", 39 | "}\n", 40 | "```\n", 41 | "\n", 42 | "该循环重复20次并交替调用`FuncA`和`FuncB`,然后调用`FuncC`。 将循环展开两次代码如下:\n", 43 | "```cpp\n", 44 | "// Example 7.30b\n", 45 | "int i;\n", 46 | "for (i = 0; i < 20; i += 2) {\n", 47 | " FuncA(i);\n", 48 | " FuncC(i);\n", 49 | " FuncB(i+1);\n", 50 | " FuncC(i+1);\n", 51 | "}\n", 52 | "```" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "此代码有三个优势:\n", 60 | "- `i < 20`循环控制分支执行10次而不是20次。\n", 61 | "- 重复次数从20减少到10,意味着它可以在奔腾4上完美预测。\n", 62 | "- `if`分支被移除。\n", 63 | "\n", 64 | "此循环展开也有下面的缺陷:\n", 65 | "- 展开的循环在代码缓存或微操作缓存中占用更多空间。\n", 66 | "- Core2处理器在非常小的循环(少于65个字节的代码)上表现更好。\n", 67 | "- 如果重复次数是奇数并且你展开因子是2,那么必须在循环外进行额外的迭代。 通常,当重复次数不能被展开因子整除时,会出现此问题。\n", 68 | "\n", 69 | "只有在可以获得特定优势的情况下才应使用循环展开。\n", 70 | "如果循环包含浮点计算并且循环计数器是整数,那么通常可以假设整个计算时间由浮点代码而不是循环控制分支确定。\n", 71 | "在这种情况下,通过展开循环没有任何好处。\n", 72 | "\n", 73 | "在具有微操作高速缓存(例如,Sandy Bridge)的处理器上应该避免循环展开,因为重要的是节省微操作高速缓存的使用。\n", 74 | "如果看上去有好处,编译器通常会自动展开循环(参见第72页)。\n", 75 | "\n", 76 | "**程序员不必手动展开循环,除非能获得特别的好处,例如在示例`7.30b`中消除`if-branch`。**" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "### 循环控制条件\n", 84 | "\n", 85 | "最有效的循环控制条件是一个简单的整数计数器。具有乱序功能的微处理器(参见第105页)将能够在几次迭代之前评估循环控制语句。\n", 86 | "如果循环控制分支依赖于循环内的计算,则效率较低。\n", 87 | "以下示例将以零结尾的ASCII字符串转换为小写:\n", 88 | "\n", 89 | "```cpp\n", 90 | "// Example 7.31a\n", 91 | "char string[100], *p = string;\n", 92 | "while (*p != 0) *(p++) |= 0x20;\n", 93 | "```\n", 94 | "\n", 95 | "如果已知字符串的长度,则使用循环计数器更有效:\n", 96 | "```cpp\n", 97 | "// Example 7.31b\n", 98 | "char string[100], *p = string; int i, StringLength;\n", 99 | "for (i = StringLength; i > 0; i--) *(p++) |= 0x20;\n", 100 | "```" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "循环控制分支依赖于循环内部计算的常见情况是数学迭代,例如泰勒展开和牛顿迭代(牛顿-拉弗森迭代)。\n", 108 | "这里重复迭代直到残余误差低于某个容差。\n", 109 | "计算残余误差的绝对值并将其与容差进行比较,所花费的时间可能非常高,**以至于确定最坏情况最大重复计数,并始终使用该迭代次数效率更高。**\n", 110 | "\n", 111 | "这种方法的优点是微处理器可以提前执行循环控制分支,并在循环内的浮点计算完成之前很久就解决任何分支错误预测。\n", 112 | "如果典型的重复次数接近最大重复次数,并且每轮残差的计算时间相对总计算时间的贡献明显,则该方法是有利的。\n", 113 | "\n", 114 | "循环计数器应该优先选择整数。如果循环需要浮点计数器,则创建一个额外的整数计数器。例如:\n", 115 | "```cpp\n", 116 | "// Example 7.32a\n", 117 | "double x, n, factorial = 1.0;\n", 118 | "for (x = 2.0; x <= n; x++) factorial *= x;\n", 119 | "```\n", 120 | "这可以**通过添加整数计数器,并在循环控制条件中使用整数来加以改进:**\n", 121 | "```cpp\n", 122 | "// Example 7.32b\n", 123 | "double x, n, factorial = 1.0; int i;\n", 124 | "for (i = (int)n - 2, x = 2.0; i >= 0; i--, x++) factorial *= x;\n", 125 | "```\n", 126 | "\n", 127 | "注意具有多个计数器的循环中逗号和分号之间的区别,如示例7.32b所示。\n", 128 | "`for`循环有三个子句:初始化,条件和增量。这三个子句用分号分隔,而每个子句中的多个语句用逗号分隔。\n", 129 | "**条件子句中应该只有一个语句。**\n", 130 | "将**整数与零进行比较有时比将其与任何其他数字进行比较更有效**。\n", 131 | "因此,将循环计数降至零比使其计数到某个正值`n`更为有效。\n", 132 | "但是**如果循环计数器用作数组索引,不要这么做。\n", 133 | "对数组的存取,数据缓存被优化为向前访问,而不是向后。**" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": {}, 139 | "source": [ 140 | "### 复制或清除数组\n", 141 | "\n", 142 | "将循环用于琐碎的任务,例如复制数组或将数组设置为全零,可能不是最佳选择。 例如:\n", 143 | "```cpp\n", 144 | "// Example 7.33a\n", 145 | "const int size = 1000; int i;\n", 146 | "float a[size], b[size];\n", 147 | "// set a to zero\n", 148 | "for (i = 0; i < size; i++) a[i] = 0.0;\n", 149 | "// copy a to b\n", 150 | "for (i = 0; i < size; i++) b[i] = a[i];\n", 151 | "```\n", 152 | "\n", 153 | "使用函数 `memset` 和 `memcpy` 一般更快一些:\n", 154 | "```cpp\n", 155 | "// Example 7.33b\n", 156 | "const int size = 1000;\n", 157 | "float a[size], b[size];\n", 158 | "// set a to zero\n", 159 | "memset(a, 0, sizeof(a));\n", 160 | "// copy a to b\n", 161 | "memcpy(b, a, sizeof(b));\n", 162 | "```\n", 163 | "\n", 164 | "至少在简单的情况下,大多数编译器会通过调用memset和memcpy自动替换这样的循环。\n", 165 | "显式使用`memset`和`memcpy`是不安全的,因为如果size参数大于目标数组,则可能发生严重错误。\n", 166 | "但是如果循环计数太大,循环也会发生相同的错误。" 167 | ] 168 | } 169 | ], 170 | "metadata": { 171 | "kernelspec": { 172 | "display_name": "Python 3", 173 | "language": "python", 174 | "name": "python3" 175 | }, 176 | "language_info": { 177 | "codemirror_mode": { 178 | "name": "ipython", 179 | "version": 3 180 | }, 181 | "file_extension": ".py", 182 | "mimetype": "text/x-python", 183 | "name": "python", 184 | "nbconvert_exporter": "python", 185 | "pygments_lexer": "ipython3", 186 | "version": "3.6.7" 187 | } 188 | }, 189 | "nbformat": 4, 190 | "nbformat_minor": 2 191 | } 192 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.14-7.18-函数相关.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.14 函数\n", 8 | "\n", 9 | "\n", 10 | "函数调用可能会降低程序速度,原因如下:\n", 11 | "\n", 12 | "- 函数调用会让微处理器跳转到不同的代码地址然后返回。这可能需要多达4个时钟周期。\n", 13 | "在大多数情况下,微处理器能够将调用及返回操作与其他计算重叠运行以节省时间。\n", 14 | "\n", 15 | "- 如果代码碎片化地分散在内存中,代码缓存的效率会降低。\n", 16 | "\n", 17 | "- 函数参数以32位模式存储在堆栈中。\n", 18 | "将参数存储在堆栈上并再次读取它们需要额外的时间。\n", 19 | "如果某个参数处于关键依赖链条中,因此带来的延时是需要重视的。\n", 20 | "\n", 21 | "- 设置堆栈帧,保存和恢复寄存器,以及保存异常处理信息(可能需要,也可能不需要),都需要额外的时间。\n", 22 | "\n", 23 | "- 每个函数调用语句占用分支目标缓冲区(BTB)中的一个单元。\n", 24 | "如果程序的关键部分有很多函数调用和分支,BTB中的争用可能导致分支预测错误" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "可以使用以下方法,来减少在关键程序的中花费在函数调用上的时间。\n", 32 | "\n", 33 | "### 避免非必要的函数\n", 34 | "\n", 35 | "**一些编程教科书建议每个长于若干行的函数应该分成多个函数。我不赞成此规则。**\n", 36 | "将函数拆分为多个较小的函数只会降低程序的效率。\n", 37 | "除非函数执行多个逻辑上不同的任务,否则仅仅因为它很长就拆分函数,并不会使程序更加清晰。\n", 38 | "可能的话,关键性的最内层环应该完全放入一个函数内。\n", 39 | "\n", 40 | "### 使用内联函数\n", 41 | "\n", 42 | "内联函数可像宏一样扩展,以便调用函数的每个语句都被函数体替换。\n", 43 | "如果使用了`inline`关键字或者在类定义中包含了函数体,则通常函数会被内联。\n", 44 | "如果函数很小,或者仅从程序中的一个地方调用该函数,则内联函数是有好处的。\n", 45 | "小函数通常由编译器自动内联。\n", 46 | "另一方面,如果内联导致技术问题或性能问题,编译器在某些情况下可能会忽略内联函数的请求。\n", 47 | "\n", 48 | "### 最内层循环避免函数嵌套\n", 49 | "\n", 50 | "调用其他函数的函数称为**帧函数**,而不调用任何其他函数的函数称为**叶函数**。\n", 51 | "叶函数比函数有更高效率,原因如第63页所述。\n", 52 | "如果程序的关键部分的最内层循环包含对帧函数的调用,则可通过下面方法来改进代码:\n", 53 | "- 内联帧函数\n", 54 | "- 把帧函数转换为叶函数,转换方法:内联所有被调用的函数\n", 55 | "\n", 56 | "### 使用宏代替函数\n", 57 | "\n", 58 | "使用`#define`声明的宏肯定会被内联。但请注意,每次使用宏参数时都会对其进行评估。例如:\n", 59 | "```cpp\n", 60 | "// Example 7.34a. Use macro as inline function\n", 61 | "#define MAX(a,b) (a > b ? a : b)\n", 62 | "y = MAX(f(x), g(x));\n", 63 | "```\n", 64 | "在这个例子中, `f(x)` 或 `g(x)` 被计算了两次,原因是它被引用了两次。\n", 65 | "你可以通过使用内联函数而不是用宏,来避免这种情况。如果您希望该内联函数支持任何类型的参数,那么将参数设计为模板:\n", 66 | "```cpp\n", 67 | "// Example 7.34b. Replace macro by template\n", 68 | "template \n", 69 | "static inline T max(T const & a, T const & b) {\n", 70 | " return a > b ? a : b;\n", 71 | "}\n", 72 | "```\n", 73 | "\n", 74 | "宏的另一个问题是名称不能重载,或者(把名称)限制在作用域中。\n", 75 | "无论是在作用域,还是名字空间中,宏都将干扰具有相同名称的任何函数或变量。\n", 76 | "因此,为宏使用足够长且唯一的名称非常重要,尤其是在头文件中。" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "### 使用`fastcall`函数\n", 84 | "\n", 85 | "- 在32位模式下,关键字`__fastcall`更改函数调用方法,以便前两个(CodeGear编译器上是三个)整型参数使用寄存器传输,而不是用堆栈传输。\n", 86 | "这可以提高具有整型参数函数的速度。浮点参数不受`__fastcall`的影响。\n", 87 | "类的成员函数中的隐式“this”指针也被视为参数,因此可能只剩下一个空闲寄存器来传输其他参数。\n", 88 | "因此,在使用`__fastcall`时,请确保最关键的整型参数首先出现在函数参数中。\n", 89 | "- 在64位模式下,函数参数默认传输到寄存器中。 因此,在64位模式下无法识别`__fastcall`关键字。\n", 90 | "\n", 91 | "\n", 92 | "### 函数本地化\n", 93 | "\n", 94 | "仅在同一模块(即当前.cpp文件)中使用的函数应该被设置为本地函数。\n", 95 | "这使编译器更容易生成内联函数,并跨函数间进行优化。\n", 96 | "有三种方法可以使函数本地化:\n", 97 | "\n", 98 | "1. 将关键字static添加到函数声明中。这是最简单的方法,但它不适用于类成员函数,其中static具有不同的含义。\n", 99 | "\n", 100 | "2. 将函数或类放入匿名的名字空间。\n", 101 | "\n", 102 | "3. Gnu编译器允许使用`\"__attribute__((visibility(\"hidden\")))\"`.\n", 103 | "\n", 104 | "### 使用整个程序优化\n", 105 | "\n", 106 | "有些编译器有编译选项用于整个程序优化,或者将多个.cpp文件组合到一个目标文件中。\n", 107 | "这使编译器能够优化组成程序的所有.cpp模块的寄存器分配和参数传输。\n", 108 | "整个程序优化不能用于函数库(以目标文件或库文件的方式分发)。\n", 109 | "\n", 110 | "### 使用64位模式\n", 111 | "\n", 112 | "**参数传输在64位模式下比在32位模式下更高效,在64位Linux中比在64位Windows中更高效。**\n", 113 | "- 在64位Linux中,前六个整数参数和前八个浮点参数在寄存器中传输,总计最多十四个寄存器参数。\n", 114 | "- 在64位Windows中,前四个参数在寄存器中传输,无论它们是整数还是浮点数。\n", 115 | "\n", 116 | "因此,如果函数具有四个以上的参数,则64位Linux比64位Windows更有效。\n", 117 | "在参数传递这方面,32位Linux和32位Windows之间没有区别。\n" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "### 7.15 函数参数\n", 125 | "\n", 126 | "在大多数情况下,函数参数按值传输。这意味着参数的值将复制到局部变量。\n", 127 | "这对于简单类型(如int,float,double,bool,enum以及指针和引用)都很有效。\n", 128 | "\n", 129 | "数组总是作为指针传递,除非它们被封装到类或结构中。\n", 130 | "\n", 131 | "如果参数具有复合类型(例如结构或类),则情况会更复杂。\n", 132 | "如果满足以下所有条件,则复合类型参数的传输效率可以很高:\n", 133 | "- 对象足够小,可以装进单个寄存器\n", 134 | "- 对象没有复制构造函数,也没有析构函数\n", 135 | "- 对象没有虚拟成员(虚拟函数)\n", 136 | "- 对象不使用运行时类型标识(RTTI)" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "如果上述某一或某些条件不满足,则将使用指向对象的指针或引用来传递对象通常会更快。\n", 144 | "如果对象很大,那么复制整个对象显然需要时间。\n", 145 | "- 将对象复制到参数时,必须调用复制构造函数,\n", 146 | "- 并且函数返回之前,如果有析构函数的话,还必须调用析构函数\n", 147 | "\n", 148 | "将复合对象传递给函数的**首选方法是使用const引用**。\n", 149 | "const引用确保不修改原始对象。\n", 150 | "与指针或非const引用不同,const引用允许函数参数是表达式或匿名对象。\n", 151 | "如果函数被内联,编译器可以轻松地优化掉const引用。\n", 152 | "\n", 153 | "另一种方法是使该函数成为对象类或结构的成员。\n", 154 | "这种方法效率同等的高。\n", 155 | "\n", 156 | "简单的函数参数\n", 157 | "- 在32位系统中传输到堆栈中,\n", 158 | "- 但在64位系统的寄存器中传输。\n", 159 | "\n", 160 | "后者更有效率。\n", 161 | "- 64位Windows允许在寄存器中传输最多四个参数。\n", 162 | "- 64位Unix系统允许在寄存器中传输多达14个参数(8个浮点数或2个加6个整数,指针或引用参数)。\n", 163 | "\n", 164 | "成员函数中的this指针也计入一个参数。进一步的细节在手册5中给出:“不同的C++编译器和操作系统调用约定”。" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "### 7.16 函数返回类型\n", 172 | "\n", 173 | "函数的返回类型最好是\n", 174 | "- 简单类型\n", 175 | "- 指针\n", 176 | "- 引用\n", 177 | "- 或void\n", 178 | "\n", 179 | "返回复合类型的对象更复杂,并且通常效率低下。\n", 180 | "\n", 181 | "对于要返回复合类型对象的情况,只有在该复合类型是最简单的情况,才能在寄存器中返回。\n", 182 | "有关何时可以在寄存器中返回对象的详细信息,请参见手册5:“调用不同C++编译器和操作系统的约定”。\n", 183 | "\n", 184 | "除最简单的情况外,复合对象还有一种返回方法,即把它们复制到一个位置,该位置由调用者使用一个隐藏的指针来指定。\n", 185 | "如果有拷贝构造函数的话,通常会在此时被调用。当原始的(被拷贝的)对象被销毁时,析构函数也会被调用。\n", 186 | "在简单的情况下,编译器可以避免对复制构造函数和析构函数的调用,方法是直接在最终目标上构造对象来,但不要指望它。\n", 187 | "\n", 188 | "(译者注:这大概是指现在编译器基本都支持的返回值优化return value optimization吧。)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "您可以考虑以下备选方案,而不是返回复合对象:\n", 196 | "\n", 197 | "- 使该函数成为该对象的构造函数。\n", 198 | "\n", 199 | "- 使该函数修改现有对象而不是创建新对象。\n", 200 | "现有对象可以通过指针或引用传递给该函数,或者让该函数成为此对象类的成员。\n", 201 | "\n", 202 | "- 让该函数返回指针或者引用,该指针或引用指向函数内定义的静态对象。\n", 203 | "这种方法效率高,但有风险。\n", 204 | "返回的指针或引用仅在下次调用函数前有效,此对象也可能被在不同的线程中改写。\n", 205 | "如果您忘记将此本地对象设置为静态,则只要函数返回它就会立即失效。\n", 206 | "\n", 207 | "- 让函数使用`new`操作构造一个对象,并返回指向它的指针。\n", 208 | "由于动态内存分配的成本,这种方法效率较低。如果您忘记删除对象,此方法还会导致内存泄漏的风险。" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "### 7.17 函数尾调用\n", 216 | "\n", 217 | "尾调用是优化函数调用的一种方法。\n", 218 | "如果函数的最后一个语句是对另一个函数的调用,那么编译器可以通过跳转到第二个函数来替换该调用。\n", 219 | "具有优化能力的编译器将自动执行此操作。第二个函数不会返回到第一个函数,而是直接返回到调用第一个函数的位置。\n", 220 | "这样效率更高,因为它消除了一次返回。例如:\n", 221 | "\n", 222 | "```cpp\n", 223 | "// Example 7.35. Tail call\n", 224 | "void function2(int x);\n", 225 | "void function1(int y) {\n", 226 | " ...\n", 227 | " function2(y+1);\n", 228 | "}\n", 229 | "```" 230 | ] 231 | }, 232 | { 233 | "cell_type": "markdown", 234 | "metadata": {}, 235 | "source": [ 236 | "这里,从`function1`的返回被消除掉了,因为直接跳转到`function2`。即使有返回值,这个机制也能工作:\n", 237 | "\n", 238 | "\n", 239 | "```cpp\n", 240 | "// Example 7.36. Tail call with return value\n", 241 | "int function2(int x);\n", 242 | "int function1(int y) {\n", 243 | " ...\n", 244 | " return function2(y+1);\n", 245 | "}\n", 246 | "```\n", 247 | "**只有当两个函数具有相同的返回类型时,尾调用优化才有效。\n", 248 | "如果函数在堆栈上有参数(在32位模式下多数情况如此),那么这两个函数的参数必须使用相同数量的堆栈空间。**" 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "### 7.18 递归函数\n", 256 | "\n", 257 | "递归函数是一个调用自身的函数。\n", 258 | "递归函数调用对于处理递归型的数据结构非常有用。\n", 259 | "递归函数的代价是所有参数和局部变量,在每次递归的递归调用里,都要获取一个新实例,这占用了堆栈空间。\n", 260 | "深度递归也会使返回地址的预测效率降低。\n", 261 | "此问题通常出现在递归深度高于16的情况下(请参阅手册3中的返回堆栈缓冲区说明:“Intel,AMD和VIA CPU的微架构”)。\n", 262 | "\n", 263 | "递归函数调用仍然是处理分支数据树结构的最有效解决方案。\n", 264 | "**如果树结构的广度大于深度,则递归更有效。**\n", 265 | "非分支性的递归(非树形递归)总是可以用循环代替,这样更高效。\n", 266 | "教科书上,递归函数的常见示例是阶乘函数:\n", 267 | "\n", 268 | "```cpp\n", 269 | "// Example 7.37. Factorial as recursive function\n", 270 | "unsigned long int factorial(unsigned int n) {\n", 271 | " if (n < 2) return 1;\n", 272 | " return n * factorial(n-1);\n", 273 | "}\n", 274 | "```" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "此实现效率非常低,因为`n`和所有实例和所有的返回地址占用堆栈上的存储空间。\n", 282 | "使用循环效率更高:\n", 283 | "\n", 284 | "```cpp\n", 285 | "// Example 7.38. Factorial function as loop\n", 286 | "unsigned long int factorial(unsigned int n) {\n", 287 | " unsigned long int product = 1;\n", 288 | " while (n > 1) {\n", 289 | " product *= n;\n", 290 | " n--;\n", 291 | " }\n", 292 | " return product;\n", 293 | "}\n", 294 | "```\n", 295 | "\n", 296 | "递归尾调用比其它递归调用更高效,但仍然比循环效率低。\n", 297 | "\n", 298 | "新手程序员有时会通过调用`main`来重启他们的程序。\n", 299 | "这是一个糟糕的想法,因为每次递归调用`main`时,堆栈都会填满所有局部变量的新实例。\n", 300 | "重新启动程序的正确方法是在`main`中创建一个循环。\n" 301 | ] 302 | } 303 | ], 304 | "metadata": { 305 | "kernelspec": { 306 | "display_name": "Python 3", 307 | "language": "python", 308 | "name": "python3" 309 | }, 310 | "language_info": { 311 | "codemirror_mode": { 312 | "name": "ipython", 313 | "version": 3 314 | }, 315 | "file_extension": ".py", 316 | "mimetype": "text/x-python", 317 | "name": "python", 318 | "nbconvert_exporter": "python", 319 | "pygments_lexer": "ipython3", 320 | "version": "3.6.5" 321 | } 322 | }, 323 | "nbformat": 4, 324 | "nbformat_minor": 2 325 | } 326 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.19-7.25-结构与类相关.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.19 结构与类\n", 8 | "\n", 9 | "如今,编程教科书推荐使用面向对象编程,使软件开发更加清晰和模块化。\n", 10 | "对象是结构和类的实例。\n", 11 | "面向对象的编程风格对程序性能,既有正面又有负面的影响。积极影响是:\n", 12 | "\n", 13 | "- 如果一组变量是相同结构或类的成员,则它们一起使用,也存储在一起。这使数据缓存更有效。\n", 14 | "- 类成员变量不需要作为参数传递给类成员函数,避免了参数传输的开销。\n", 15 | "\n", 16 | "面向对象编程的负面影响是:\n", 17 | "\n", 18 | "- 非静态成员函数有一个'`this`'指针,它作为隐式参数传递给函数。所有非静态成员函数都会产生'`this`'参数传输的开销。\n", 19 | "- '`this`'指针占用一个寄存器。寄存器是32位系统中的稀缺资源。\n", 20 | "- 虚拟成员函数效率较低(请参阅第55页)。\n", 21 | "\n", 22 | "**关于面向对象编程的正面影响,还是负面影响占主导地位,没有一般性的定论。**\n", 23 | "至少,可以说使用类和成员函数,代价并不昂贵。\n", 24 | "你可以使用面向对象的编程风格:\n", 25 | "- 如果它对程序的逻辑结构和清晰度有好处\n", 26 | "- 只要在程序的最关键部分避免过多的函数调用,\n", 27 | "\n", 28 | "单纯结构的使用(没有成员函数)对性能没有负面影响。" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## 7.20 类数据成员(实例变量)\n", 36 | "\n", 37 | "无论在什么时间创建类或结构的实例,其数据成员都按照它们被声明的顺序连续存储。\n", 38 | "将数据组织到类或结构中,并不会导致性能损失。\n", 39 | "访问类或结构对象的数据成员并不会比访问简单变量话费更多的时间。\n", 40 | "\n", 41 | "大多数编译器会将数据成员与地址对齐,以便优化访问,如下表所示。\n", 42 | "\n", 43 | "Type | size, bytes | alignment, bytes\n", 44 | " --- | --- | --- |\n", 45 | "bool | 1 | 1\n", 46 | "char, signed or unsigned | 1 | 1\n", 47 | "short int, signed or unsigned | 2 | 2\n", 48 | "int, signed or unsigned | 4 | 4\n", 49 | "64-bit integer, signed or unsigned | 8 | 8\n", 50 | "pointer or reference, 32-bit mode | 4 | 4\n", 51 | "pointer or reference, 64-bit mode | 8 | 8\n", 52 | "float | 4 | 4\n", 53 | "double | 8 | 8\n", 54 | "long double | 8, 10, 12 or 16 | 8 or 16\n", 55 | "\n", 56 | "Table 7.2. 数据成员的对齐" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "结构或类中如果混有不同大小的数据成员,数据对齐可能会导致未使用字节的空洞。例如:\n", 64 | "```cpp\n", 65 | "// Example 7.39a\n", 66 | "struct S1 {\n", 67 | " short int a; // 2个字节. 第一个在偏移位置0, 第二个在位置1\n", 68 | " // 6个未使用的字节\n", 69 | " double b; // 8个字节. 第一个在偏移位置8, 最后一个在位置15\n", 70 | " int d; // 4个字节. 第一个在偏移位置16, 最后一个在位置19\n", 71 | " // 4个未使用字节\n", 72 | "};\n", 73 | "S1 ArrayOfStructures[100];\n", 74 | "```\n", 75 | "此处数据对齐导致结构缩小了8个字节,数组缩小了800个字节。\n", 76 | "\n", 77 | "通过重新安排数据成员的编码顺序,通常可以使结构和类对象更小。\n", 78 | "如果类有至少一个虚拟函数,则在第一个数据成员之前,或最后一个成员之后,有一个指向虚拟表的指针。\n", 79 | "该指针在32位系统中为4个字节,在64位系统中为8个字节。\n", 80 | "如果你对结构或其每个成员的大小有疑问,可以使用`sizeof`运算符进行测试。\n", 81 | "`sizeof`运算符返回的值包括对象末尾的任何未使用的字节的大小。\n", 82 | "\n", 83 | "- **如果数据成员相对于结构或类的开头的偏移量小于128,则生成的访问数据成员的代码更为紧凑,因为偏移量可以表示为8位有符号数。**\n", 84 | "- 如果相对于结构或类的开头的偏移量大于等于128字节,则偏移量必须表示为32位数(指令集在介于8位和32位偏移之间,没有任何区别)。 \n", 85 | "\n", 86 | "例如:\n", 87 | "```cpp\n", 88 | "// Example 7.40\n", 89 | "class S2 {\n", 90 | "public:\n", 91 | " int a[100]; // 400字节. 首字节偏移量0, 尾字节偏移量399\n", 92 | " int b; // 4字节. 首字节偏移量400, 尾字节偏移量403\n", 93 | " int ReadB() {return b;}\n", 94 | "};\n", 95 | "```\n", 96 | "\n", 97 | "这里`b`的偏移量为400。通过指针或成员函数(如`ReadB`)访问`b`的任何代码都需要将偏移量编码为32位数。\n", 98 | "如果交换`a`和`b`,则可以使用编码为8位有符号数的偏移量访问这两个变量,或者根本不用偏移。\n", 99 | "这使代码更紧凑,从而更有效地使用代码缓存。\n", 100 | "因此,对于大型数组和其他大型对象,建议在结构或类声明中排在最后。并且最常用的数据成员放在前面。\n", 101 | "如果不能把所有数据成员包含前128个字节内,则将最常用的成员放在前128个字节中。" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "## 7.21 类成员函数(方法)\n", 109 | "\n", 110 | "每次声明或创建类的新对象时,它都将生成数据成员的新实例。\n", 111 | "但是每个成员函数只有一个实例。函数代码不用复制多份,因为相同的代码可以应用于类的所有实例。\n", 112 | "\n", 113 | "下面两种方式速度一样快:\n", 114 | "- 调用结构的成员函数\n", 115 | "- 调用简单函数,以指针(或引用)为参数,该指针向此结构\n", 116 | "\n", 117 | "例如:\n", 118 | "\n", 119 | "```cpp\n", 120 | "// Example 7.41\n", 121 | "class S3 {\n", 122 | "public:\n", 123 | " int a;\n", 124 | " int b;\n", 125 | " int Sum1() {return a + b;}\n", 126 | "};\n", 127 | "int Sum2(S3 * p) {return p->a + p->b;}\n", 128 | "int Sum3(S3 & r) {return r.a + r.b;}\n", 129 | "```\n", 130 | "`Sum1`,`Sum2`和`Sum3`这三个函数完成相同的工作,效率也相同。\n", 131 | "如果查看编译器生成的代码,您会注意到一些编译器将为这三个函数生成完全相同的代码。\n", 132 | "`Sum1`有一个隐含的'`this`'指针,它与`Sum2`和`Sum3`中的`p`和`r`做同样的事情。\n", 133 | "无论你是想让函数成为类的成员,还是给它一个指针或引用,指该向类或结构,都只是编程风格的问题。\n", 134 | "有些编译器通过在寄存器而不是堆栈中传输'`this`',使得`Sum1`在32位Windows中的效率略高于`Sum2`和`Sum3`。" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "## 7.22 虚拟成员函数\n", 142 | "\n", 143 | "虚函数用于实现多态类。\n", 144 | "多态类的每个实例都有一个指针,该指针指向一个指针表,表中是不同版本的虚拟函数。\n", 145 | "这个所谓的虚表用于在运行时定位到正确版本的虚拟函数。\n", 146 | "多态性是面向对象程序比非面向对象程序效率低的主要原因之一。\n", 147 | "如果可以避免使用虚函数,那么你可以获得面向对象编程的大部分优势,而无需支付性能成本。\n", 148 | "\n", 149 | "- 如果函数调用语句总是调用相同版本的虚函数,调用虚拟成员函数所花费的时间比调用非虚拟成员函数所花费的时间,只多几个时钟周期。\n", 150 | "- 如果虚函数版本发生变化,那么可能会得到10到20个时钟周期的错误预测惩罚。\n", 151 | "\n", 152 | "虚函数调用的预测正确和错误的规则与`switch`语句相同,如第44页所述。\n", 153 | "\n", 154 | "当在已知确定类型的对象上调用虚函数时,可以绕过调度机制。但是你不能总是依赖编译器能绕过该调度机制,即使有时候看似很明显可以绕过,它也未必能成功。\n", 155 | "详见第75页。\n", 156 | "\n", 157 | "只有在编译时无法知道调用哪个版本的多态成员函数时,才需要运行时多态性。\n", 158 | "如果需要在程序的关键部分使用虚函数,那么你可以考虑下面的方法:\n", 159 | "- 是否可以在没有多态实现该所需功能\n", 160 | "- 或使用编译时多态实现该功能\n", 161 | "\n", 162 | "**有时可以使用模板而不是虚函数来获得所需的多态效果。**\n", 163 | "模板参数应该是个类,该类包含具有多个版本的函数。\n", 164 | "此方法更快,因为模板参数始终在编译时而不是在运行时解析。\n", 165 | "第59页上的示例7.47是执行此操作的例子。\n", 166 | "不幸的是,语法非常糟糕,可能不值得付出努力学这样的东西。" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "## 7.23 运行时类型识别(RTTI)\n", 174 | "\n", 175 | "运行时类型标识会向所有类对象添加额外信息,其效率不高。\n", 176 | "如果编译器有RTTI选项,把它关闭,寻找其它替代方法。" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "## 7.24 继承\n", 184 | "\n", 185 | "编译器对于派生类的对象的实现方式,与包含父类和子类成员的简单类的对象相同。\n", 186 | "父类和子类的成员访问同样的快。通常,可以假定使用继承几乎没有任何性能损失。\n", 187 | "\n", 188 | "由于以下原因,代码缓存性能可能会略有下降:\n", 189 | "\n", 190 | "- 父类数据成员的大小将添加到子类成员的偏移量中。\n", 191 | "访问总偏移量大于127字节的数据成员的代码,会稍微变得不那么紧凑。详见第54页。\n", 192 | "\n", 193 | "- 父和子的成员函数通常存储在不同的模块中。\n", 194 | "这可能会导致大量跳转,代码缓存效率降低。\n", 195 | "解决这个问题的方法是,确保彼此相邻调用的函数也存储在彼此附近。\n", 196 | "详情参见第90页。" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "从同一代中的多个父类的多重继承,会导致成员指针和虚函数的复杂化。通过指向多个基类中一个基类的指针,来访问派生类的对象时,会变得很复杂。\n", 204 | "你可以在派生类中创建对象,来避免多重继承:\n", 205 | "\n", 206 | "```cpp\n", 207 | "// Example 7.42a. 多重继承\n", 208 | "class B1; class B2;\n", 209 | "class D : public B1, public B2 {\n", 210 | "public:\n", 211 | " int c;\n", 212 | "};\n", 213 | "```\n", 214 | "替换为:\n", 215 | "```cpp\n", 216 | "// Example 7.42b. 多重继承替代方案\n", 217 | "class B1; class B2;\n", 218 | "class D : public B1 {\n", 219 | "public:\n", 220 | " B2 b2;\n", 221 | " int c;\n", 222 | "};\n", 223 | "```" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "## 7.25 构造函数和析构函数\n", 231 | "\n", 232 | "构造函数在内部实现为成员函数,该函数返回该对象的引用。\n", 233 | "新对象的内存分配不一定由构造函数自身完成。\n", 234 | "因此,构造函数与任何其他成员函数效率没有区别。\n", 235 | "此结论适用于默认构造函数,拷贝构造函数和任何其它构造函数。\n", 236 | "\n", 237 | "一个类可以不需要构造函数。\n", 238 | "- 如果对象不需要初始化,可以没有默认构造函数。\n", 239 | "- 如果只需复制所有数据成员就可以复制对象,则不需要拷贝构造函数。\n", 240 | "\n", 241 | "简单的构造函数可以被内联,以提高性能。\n", 242 | "\n", 243 | "当通过赋值,函数参数或函数返回值来复制对象时,拷贝构造函数都可能被调用。\n", 244 | "如果拷贝构造函数涉及内存或其他资源的分配,可能要花费一定时间。\n", 245 | "有多种方法可以避免这种内存复制浪费,例如:\n", 246 | "\n", 247 | "- 使用指向对象的引用或指针,而不是复制对象本身。\n", 248 | "- 使用“移动构造函数”来转移内存块的所有权。这需要一个支持C++ 0x的编译器。\n", 249 | "- 创建一个类的成员函数,或者友元函数,或者运算符,将内存块的所有权从一个对象转移到另一个对象。\n", 250 | "失去内存块所有权的对象应将其指针设置为`NULL`。\n", 251 | "当然应该有一个析构函数来销毁对象拥有的任何内存块。\n", 252 | "\n", 253 | "析构函数与成员函数效率相同。如果没有必要,不要写析构函数。\n", 254 | "虚拟析构函数与虚拟成员函数效率相同。详见第55页。" 255 | ] 256 | } 257 | ], 258 | "metadata": { 259 | "kernelspec": { 260 | "display_name": "Python 3", 261 | "language": "python", 262 | "name": "python3" 263 | }, 264 | "language_info": { 265 | "codemirror_mode": { 266 | "name": "ipython", 267 | "version": 3 268 | }, 269 | "file_extension": ".py", 270 | "mimetype": "text/x-python", 271 | "name": "python", 272 | "nbconvert_exporter": "python", 273 | "pygments_lexer": "ipython3", 274 | "version": "3.6.7" 275 | } 276 | }, 277 | "nbformat": 4, 278 | "nbformat_minor": 2 279 | } 280 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.26-7.29.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.26 联合(Unions)\n", 8 | "\n", 9 | "联合是一种数据成员共享相同内存空间的数据结构。\n", 10 | "通过允许两个从不同时使用的数据成员共享同一块内存,可以使用联合来节省内存空间。\n", 11 | "有关示例,请参见第91页。\n", 12 | "\n", 13 | "**联合也可用于以不同方式访问相同数据**。例如:\n", 14 | "```cpp\n", 15 | "// Example 7.43\n", 16 | "union {\n", 17 | " float f;\n", 18 | " int i;\n", 19 | "} x;\n", 20 | "x.f = 2.0f;\n", 21 | "x.i |= 0x80000000; // 设置符号位\n", 22 | "cout << x.f; // 会输出 -2.0\n", 23 | "```\n", 24 | "\n", 25 | "在此示例中,`f`的符号位通过使用或运算符来设置。该运算符只能用在整数。\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## 7.27 位字段(Bitfields)\n", 33 | "\n", 34 | "位字段可能有助于使数据更紧凑。\n", 35 | "访问位字段的成员比访问结构的普通成员效率低一些。\n", 36 | "对于大型数组,由于可以节省缓存空间或缩小文件,那么该额外的时间可能是合理的。\n", 37 | "**通过使用`<<`和`|`来进行位写操作,比向独立的位字段成员写操作更快。**\n", 38 | "例如:\n", 39 | "\n", 40 | "```cpp\n", 41 | "// Example 7.44a\n", 42 | "struct Bitfield {\n", 43 | " int a:4;\n", 44 | " int b:2;\n", 45 | " int c:2;\n", 46 | "};\n", 47 | "Bitfield x;\n", 48 | "int A, B, C;\n", 49 | "x.a = A;\n", 50 | "x.b = B;\n", 51 | "x.c = C;\n", 52 | "```\n", 53 | "\n", 54 | "假设`A`,`B`和`C`的值很小,绝对不会溢出,可以通过以下方式改进此代码:\n", 55 | "```cpp\n", 56 | "// Example 7.44b\n", 57 | "union Bitfield {\n", 58 | " struct {\n", 59 | " int a:4;\n", 60 | " int b:2;\n", 61 | " int c:2;\n", 62 | " };\n", 63 | " char abc;\n", 64 | "};\n", 65 | "Bitfield x;\n", 66 | "int A, B, C;\n", 67 | "x.abc = A | (B << 4) | (C << 6);\n", 68 | "```\n", 69 | "或者,如果需要防止溢出:\n", 70 | "```cpp\n", 71 | "// Example 7.44c\n", 72 | "x.abc = (A & 0x0F) | ((B & 3) << 4) | ((C & 3) <<6 );\n", 73 | "```" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## 7.28 函数重载\n", 81 | "\n", 82 | "重载函数的不同版本被简单地视为不同的函数。使用重载函数不会有性能损失。" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "metadata": {}, 88 | "source": [ 89 | "## 7.29 运算符重载\n", 90 | "\n", 91 | "重载运算符等效于函数。使用重载运算符与使用执行相同操作的函数完全效率相同。\n", 92 | "\n", 93 | "具有多个重载运算符的表达式会为中间结果创建临时对象,这可能是不期望发生的。例如:\n", 94 | "\n", 95 | "```cpp\n", 96 | "// Example 7.45a\n", 97 | "class vector { // 2-dimensional vector\n", 98 | "public:\n", 99 | " float x, y; // x,y coordinates\n", 100 | " vector() {} // default constructor\n", 101 | " vector(float a, float b) {x = a; y = b;} // constructor\n", 102 | " vector operator + (vector const & a) { // sum operator\n", 103 | " return vector(x + a.x, y + a.y); // add elements\n", 104 | " }\n", 105 | "};\n", 106 | "vector a, b, c, d;\n", 107 | "a = b + c + d; // makes intermediate object for (b + c)\n", 108 | "```\n", 109 | "\n", 110 | "通过以下操作,可以避免为中间结果(`b + c`)创建临时对象:\n", 111 | "\n", 112 | "```cpp\n", 113 | "// Example 7.45b\n", 114 | "a.x = b.x + c.x + d.x;\n", 115 | "a.y = b.y + c.y + d.y;\n", 116 | "```\n", 117 | "幸运的是,在简单的情况下,大多数编译器会自动执行此优化。\n" 118 | ] 119 | } 120 | ], 121 | "metadata": { 122 | "kernelspec": { 123 | "display_name": "Python 3", 124 | "language": "python", 125 | "name": "python3" 126 | }, 127 | "language_info": { 128 | "codemirror_mode": { 129 | "name": "ipython", 130 | "version": 3 131 | }, 132 | "file_extension": ".py", 133 | "mimetype": "text/x-python", 134 | "name": "python", 135 | "nbconvert_exporter": "python", 136 | "pygments_lexer": "ipython3", 137 | "version": "3.6.7" 138 | } 139 | }, 140 | "nbformat": 4, 141 | "nbformat_minor": 2 142 | } 143 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.30-模板.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.30 模板\n", 8 | "\n", 9 | "在编译之前,模板参数被其值替换。在这一点上,模板类似于宏。\n", 10 | "以下示例说明了函数参数和模板参数之间的区别:\n", 11 | "\n", 12 | "```cpp\n", 13 | "// Example 7.46\n", 14 | "int Multiply (int x, int m) {\n", 15 | " return x * m;\n", 16 | "}\n", 17 | "\n", 18 | "template \n", 19 | "int MultiplyBy (int x) {\n", 20 | " return x * m;\n", 21 | "}\n", 22 | "\n", 23 | "int a, b;\n", 24 | "a = Multiply(10,8);\n", 25 | "b = MultiplyBy<8>(10);\n", 26 | "```" 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "`a`和`b`都会得到计算结果:`10 * 8 = 80`。\n", 34 | "不同之处在于`m`传递到函数的方式。\n", 35 | "- 在简单函数中,`m`在运行时从调用者传递到被调用函数。\n", 36 | "- 但是在模板函数中,`m`在编译时被其值替换,因此编译器会看到常量8而不是变量`m`。\n", 37 | "\n", 38 | "使用模板参数,而不是函数参数,\n", 39 | "- 优势是,避免了参数传输的开销。\n", 40 | "- 缺点是,编译器需要为模板参数的每个不同值创建模板函数的新实例。\n", 41 | "\n", 42 | "此示例中的`MultiplyBy`函数模板,如果使用许多不同因子作为模板参数来调用,则代码可能变得非常大。\n", 43 | "\n", 44 | "在上面的示例中,模板函数比简单函数更快,这是因为编译器知道:可以通过使用移位操作来实现乘以2的幂。\n", 45 | "即,`x * 8`由`x << 3`代替,速度更快。\n", 46 | "在简单函数的情况下,编译器不知道`m`的值,因此除非可以内联函数,否则不能进行优化。\n", 47 | "(在上面的例子中,编译器能够内联和优化两个函数,并简单地将80放入变量`a`和`b`。但在更复杂的情况下,它可能无法做到这样的优化)。\n", 48 | "\n", 49 | "模板参数也可以是类型。\n", 50 | "第38页上的示例显示了:如何使用相同的模板创建不同类型的数组。\n", 51 | "\n", 52 | "模板是效率很高的,因为模板参数总是在编译时解析。\n", 53 | "模板使源代码更复杂,但编译生成的代码并不会因此也复杂。\n", 54 | "通常,就执行速度方面,在使用模板的没有成本。\n", 55 | "\n", 56 | "如果模板参数完全相同,则将两个或多个模板实例会合并成一个。\n", 57 | "如果模板参数不同,那么将为每组模板参数生成一个实例。\n", 58 | "生成很多实例的模板会使编译的生成代码变大,并占用更多的缓存空间。\n", 59 | "\n", 60 | "过度使用模板会使代码可读性差。\n", 61 | "如果模板只有一个实例,那么你也可以使用`#define`,`const`或`typedef`,而不是模板参数。\n", 62 | "\n", 63 | "模板可用于元编程,如第154页所述。" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "### 用模板实现多态\n", 71 | "\n", 72 | "模板类可用于实现编译时多态,这比使用虚拟函数获得的运行时多态,效率更高。\n", 73 | "下例首先示范了运行时多态性:\n", 74 | "\n", 75 | "```cpp\n", 76 | "// Example 7.47a. Runtime polymorphism with virtual functions\n", 77 | "class CHello {\n", 78 | "public:\n", 79 | " void NotPolymorphic(); // Non-polymorphic functions go here\n", 80 | " virtual void Disp(); // Virtual function\n", 81 | " void Hello() {\n", 82 | " cout << \"Hello \";\n", 83 | " Disp(); // Call to virtual function\n", 84 | " }\n", 85 | "};\n", 86 | "\n", 87 | "class C1 : public CHello {\n", 88 | "public:\n", 89 | " virtual void Disp() {\n", 90 | " cout << 1;\n", 91 | " }\n", 92 | "};\n", 93 | "\n", 94 | "class C2 : public CHello {\n", 95 | "public:\n", 96 | " virtual void Disp() {\n", 97 | " cout << 2;\n", 98 | " }\n", 99 | "};\n", 100 | "\n", 101 | "void test () {\n", 102 | " C1 Object1; C2 Object2;\n", 103 | " CHello * p;\n", 104 | " p = &Object1;\n", 105 | " p->NotPolymorphic(); // Called directly\n", 106 | " p->Hello(); // Writes \"Hello 1\"\n", 107 | " p = &Object2;\n", 108 | " p->Hello(); // Writes \"Hello 2\"\n", 109 | "}\n", 110 | "```\n", 111 | "\n", 112 | "如果编译器不知道`p`指向哪个类对象(参见第75页),只好在运行时来调度`C1::Disp()`或`C2::Disp()`。\n", 113 | "当前的编译器还不太擅长优化掉`p`,改为内联对`Object1.Hello()`的调用,尽管未来的编译器可能会有这个能力。" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "如果在编译时知道对象是属于类C1还是C2,我们可以避免低效的虚函数调度过程。\n", 121 | "**这可以通过使用的特殊技巧来完成。**该技巧已经用在活动模板库(ATL)和Windows模板库(WTL)中。\n", 122 | "(译者注:这两个库都比较老,现在基本很少使用,但该技巧还是有效的。)\n", 123 | "\n", 124 | "```cpp\n", 125 | "// Example 7.47b. 使用模板实现编译时多态\n", 126 | "// 把非多态函数放入祖父类中:\n", 127 | "class CGrandParent {\n", 128 | "public:\n", 129 | " void NotPolymorphic();\n", 130 | "};\n", 131 | "\n", 132 | "// 所有要调用多态函数的函数放入父类。\n", 133 | "// 子类类名作为父类的模板参数:\n", 134 | "template \n", 135 | "class CParent : public CGrandParent {\n", 136 | "public:\n", 137 | " void Hello() {\n", 138 | " cout << \"Hello \";\n", 139 | " // call polymorphic child function:\n", 140 | " (static_cast(this))->Disp();\n", 141 | " }\n", 142 | "};\n", 143 | "\n", 144 | "// 多个子类,每个子类实现一个版本的函数\n", 145 | "class CChild1 : public CParent {\n", 146 | "public:\n", 147 | " void Disp() {\n", 148 | " cout << 1;\n", 149 | " }\n", 150 | "};\n", 151 | "\n", 152 | "class CChild2 : public CParent {\n", 153 | "public:\n", 154 | " void Disp() {\n", 155 | " cout << 2;\n", 156 | " }\n", 157 | "};\n", 158 | "\n", 159 | "void test () {\n", 160 | " CChild1 Object1; CChild2 Object2;\n", 161 | " CChild1 * p1;\n", 162 | " p1 = &Object1;\n", 163 | " p1->Hello(); // Writes \"Hello 1\"\n", 164 | " CChild2 * p2;\n", 165 | " p2 = &Object2;\n", 166 | " p2->Hello(); // Writes \"Hello 2\"\n", 167 | "}\n", 168 | "```\n" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "这里的`CParent`是一个模板类,它通过模板参数获取有关其子类的信息。\n", 176 | "它可以将其“`this`”指针类型转换为指向其子类的指针,来调用其子类的多态成员函数。\n", 177 | "仅仅当它具有正确的子类名作为模板参数时,这才是安全的。\n", 178 | "换句话说,**你必须确保下面声明中,子类名称与和模板参数名称相同:**\n", 179 | "```cpp\n", 180 | "class CChild1 : public CParent {\n", 181 | "``` \n", 182 | "\n", 183 | "**继承顺序如下:**\n", 184 | "- 第一代类(`CGrandParent`)包含所有非多态成员函数。\n", 185 | "- 第二代类(`CParent<>`)包含需要调用多态函数的所有成员函数。\n", 186 | "- 第三代类包含不同版本的多态函数。\n", 187 | "\n", 188 | "第二代类通过模板参数获取有关第三代类的信息。\n", 189 | "\n", 190 | "如果是已知对象的类,不会有时间浪费到运行时虚函数调度上。\n", 191 | "该信息已经包含在具有不同类型的`p1`和`p2`中。\n", 192 | "**缺点是`CParent::Hello()`有多个占用缓存空间的实例。**\n", 193 | "\n", 194 | "示例7.47b中的语法无疑是非常复杂的。\n", 195 | "避免虚函数调度来节省的这几个时钟周期,这个理由还不够充分,因为这样的设计带来了难以理解,难以维护的复杂代码。\n", 196 | "如果编译器能够自动去虚拟化(devirtualization,参见第75页),依靠编译器进行优化的方法,肯定比使用复杂的模板方法更方便。" 197 | ] 198 | } 199 | ], 200 | "metadata": { 201 | "kernelspec": { 202 | "display_name": "Python 3", 203 | "language": "python", 204 | "name": "python3" 205 | }, 206 | "language_info": { 207 | "codemirror_mode": { 208 | "name": "ipython", 209 | "version": 3 210 | }, 211 | "file_extension": ".py", 212 | "mimetype": "text/x-python", 213 | "name": "python", 214 | "nbconvert_exporter": "python", 215 | "pygments_lexer": "ipython3", 216 | "version": "3.6.5" 217 | } 218 | }, 219 | "nbformat": 4, 220 | "nbformat_minor": 2 221 | } 222 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.31-线程.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.31 线程\n", 8 | "\n", 9 | "线程用于同时或看似同时执行两个或多个作业。\n", 10 | "如果计算机只有一个CPU核心,则无法同时执行两个作业。\n", 11 | "每个线程将为前台作业获取通常为30毫秒的时间片,为后台作业获取10毫秒的时间片。\n", 12 | "每个时间片之后的线程上下文切换代价非常高昂,因为所有高速缓存都必须更新,以适应新的线程上下文。\n", 13 | "通过延长时间片,可以减少上下文切换的次数。\n", 14 | "这将使应用程序运行得更快,代价是用户输入的响应时间更长。\n", 15 | "(在Windows中,可以在高级系统性能选项下,选择为后台服务优化性能,可将时间片增加到120毫秒。我不知道这是否可以在Linux中实现)。\n", 16 | "\n", 17 | "\n", 18 | "线程可为不同任务分配不同的优先级。\n", 19 | "例如,在文字处理器中,用户期望按键或鼠标移动的立即响应。\n", 20 | "此任务必须具有高优先级。\n", 21 | "其他任务(如拼写检查和重新分页),可在其它优先级较低的线程中运行。\n", 22 | "如果没有把不同的任务划分为具有不同优先级的线程,当程序忙于进行拼写检查时,用户可能会感到键盘和鼠标输入响应时间太长而难于接受。\n", 23 | "\n", 24 | "如果应用程序具有图形用户界面,则应该为需要长时间运行任务(例如繁重的数学计算)安排在单独的线程中。\n", 25 | "否则程序将无法快速响应键盘或鼠标输入。\n", 26 | "\n", 27 | "可以在应用程序中实现类似线程的调度,而无需使用操作系统线程调度,导致额外的程序开销。\n", 28 | "实现方法:在GUI消息循环中的一个特殊函数(Windows MFC中的OnIdle)中,逐条进行繁重的后台计算。\n", 29 | "比起在系统中单独开一个线程,这种方法要快些,\n", 30 | "但它要求后台作业在实际上可以分成适当的小块。\n", 31 | "\n", 32 | "充分利用具有多核CPU的系统的最佳方法是将作业切分进入多个线程。\n", 33 | "每个线程都可以在自己的CPU核心上运行。" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "在优化多线程应用程序时,我们必须考虑多线程的四种成本:\n", 41 | "\n", 42 | "- 启动和停止线程的成本。\n", 43 | " - 如果任务的持续时间短于启动和停止线程所需的时间,则不要将任务放入单独的线程中。\n", 44 | "- 任务切换的成本。\n", 45 | " - 如果具有相同优先级的线程数不超过CPU核心数,则此成本最低。\n", 46 | "- 线程之间同步和通信的成本。\n", 47 | " - 信号量,互斥量等的开销很大。如果两个线程经常相互等待以便访问同一资源,那么最好将它们连接到一个线程中。\n", 48 | " - 必须将多个线程之间共享的变量声明为`volatile`。这可以防止编译器把该变量进行优化消失。\n", 49 | "- 不同的线程需要单独存储。\n", 50 | " - 多个线程使用的函数或类都不应该依赖静态或全局变量。(参见第28页的线程本地存储)\n", 51 | " - 线程有自己的堆栈。如果线程共享相同的缓存,则可能导致缓存争用。\n", 52 | "\n", 53 | "多线程程序必须使用线程安全函数。\n", 54 | "线程安全函数永远不应该使用静态变量。\n", 55 | "\n", 56 | "有关多线程技术的进一步讨论,参见第103页第10章。" 57 | ] 58 | } 59 | ], 60 | "metadata": { 61 | "kernelspec": { 62 | "display_name": "Python 3", 63 | "language": "python", 64 | "name": "python3" 65 | }, 66 | "language_info": { 67 | "codemirror_mode": { 68 | "name": "ipython", 69 | "version": 3 70 | }, 71 | "file_extension": ".py", 72 | "mimetype": "text/x-python", 73 | "name": "python", 74 | "nbconvert_exporter": "python", 75 | "pygments_lexer": "ipython3", 76 | "version": "3.6.7" 77 | } 78 | }, 79 | "nbformat": 4, 80 | "nbformat_minor": 2 81 | } 82 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.32-异常和错误处理.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.32 异常和错误处理\n", 8 | "\n", 9 | "运行时错误会导致异常,这些异常以陷阱或软件中断的形式被检测到。\n", 10 | "代码中使用`try-catch`块可以捕获这些异常。\n", 11 | "如果启用了异常处理,代码却没有`try-catch`块,程序将崩溃并显示错误消息。\n", 12 | "\n", 13 | "异常处理旨在检测很少发生的错误,并以优雅的方式从错误情形中恢复。\n", 14 | "您可能认为只要不发生错误,异常处理就不会占用额外的时间,但不幸的是,事实并非总是如此。\n", 15 | "程序可能需要进行大量(关于恢复信息的)簿记工作(bookkeeping),才能知道如何在发生异常时进行恢复。\n", 16 | "这种簿记花费的成本在很大程度上取决于不同的编译器。\n", 17 | "一些编译器使用高效的基于表的方法,几乎没有额外开销,而其它编译器具有低效的基于代码的方法,或者依赖运行时类型识别(RTTI),会影响代码的其他部分。\n", 18 | "\n", 19 | "进一步解释参照[ISO/IEC TR18015 Technical Report on C++ Performance](http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf)。" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "下示例说明了为何需要簿记:\n", 27 | "\n", 28 | "```cpp\n", 29 | "// Example 7.48\n", 30 | "class C1 {\n", 31 | "public:\n", 32 | " ...\n", 33 | " ~C1();\n", 34 | "};\n", 35 | "\n", 36 | "void F1() {\n", 37 | " C1 x;\n", 38 | " ...\n", 39 | "}\n", 40 | "\n", 41 | "void F0() {\n", 42 | " try {\n", 43 | " F1();\n", 44 | " }\n", 45 | " catch (...) {\n", 46 | " ...\n", 47 | " }\n", 48 | "}\n", 49 | "```\n", 50 | "\n", 51 | "函数`F1`本该在返回时调用对象`x`的析构函数。\n", 52 | "但是如果在`F1`的某个地方发生异常怎么办? 然后执行跳出了`F1`,函数没有返回。\n", 53 | "由于`F1`被中断,因此无法完成清理。\n", 54 | "现在,调用`x`的析构函数,就成了异常处理程序的责任。\n", 55 | "要保证此操作可以执行,`F1`必需要保存所有相关的信息:\n", 56 | "- 被调用的析构函数的所有信息\n", 57 | "- 或可能需要的任何其他清理工作的信息\n", 58 | "\n", 59 | "如果F1调用另一个函数,该函数又调用另一个函数等,并且如果在最里面的函数中发生异常,则异常处理程序需要有关函数调用链的所有信息,并且它需要通过函数调用向后跟踪路径,检查所有必要的清理工作。\n", 60 | "这称为堆栈展开。\n", 61 | "\n", 62 | "如果:\n", 63 | "- `F1`调用另一个函数,该函数又调用另一个函数,等等,\n", 64 | "- 并且如果在最里面的函数中发生异常\n", 65 | "\n", 66 | "异常处理程序需要有关函数调用链的所有信息,并且它需要沿着函数调用的反向跟踪轨迹,检查所有必要的清理工作。\n", 67 | "这称为堆栈展开。\n", 68 | "\n", 69 | "所有函数都必须为异常处理程序保存一些信息,即使没有异常发生。\n", 70 | "这就是为什么在某些编译器中异常处理可能代价高昂的原因。\n", 71 | "如果你的应用程序异常处理不是必需的,你应该禁用它,这样代码更小,也更高效。\n", 72 | "- 你可以通过关闭编译器中的异常处理选项,来禁用整个程序的异常处理。\n", 73 | "- **你可以通过向函数原型添加`throw()`来禁用单个函数的异常处理:**\n", 74 | "```cpp\n", 75 | "void F1() throw();\n", 76 | "```\n", 77 | "\n", 78 | "**译者注:** `C++11`推荐使用 `noexcept`,`throw()`已经过时。\n", 79 | "\n", 80 | "\n", 81 | "这允许编译器假定`F1`永远不会抛出任何异常,因此它不必保存函数`F1`的恢复信息。\n", 82 | "然而,如果`F1`调用另一个可能抛出异常的函数`F2`,则`F1`必须检查`F2`抛出的异常,并在`F2`实际抛出异常的情况下调用`std::unexpected()`函数。\n", 83 | "因此,只有当`F1`调用的所有函数都使用`throw()`限定符时,才能把`throw()`应用于`F1`。`throw()`对库函数很有用。\n", 84 | "\n", 85 | "编译器区分**叶函数**和**帧函数**。\n", 86 | "- 帧函数是调用至少一个其它函数的函数。\n", 87 | "- 叶函数是没有调用任何其它函数的函数。\n", 88 | "\n", 89 | "叶函数比帧函数简单,因为在某些情况下,叶函数可以省略掉堆栈展开信息:\n", 90 | "- 如果可以排除异常\n", 91 | "- 或者,如果在异常情况下没有任何真正需要清理的工作。\n", 92 | "\n", 93 | "把被调用的所有函数内联,可以将帧函数转换为叶函数。\n", 94 | "如果程序关键的最内层循环不包含对帧函数的调用,可以获得最佳性能。\n", 95 | "\n", 96 | "尽管空`throw()`语句在某些情况下可以优化提高效率,但是没有必要添加诸如`throw(A,B,C)`之类的语句,来明确告诉函数可以抛出什么类型的异常。\n", 97 | "实际上,如果添加这样的语句的话,编译器可能还需要添加额外的代码,来检查抛出的异常确实是指定的类型。\n", 98 | "(参阅 Sutter:务实的异常规范, [Dr Dobbs Journal, 2002](http://drdobbs.com/architecture-and-design/184401544J))." 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "在某些情况下,即使在程序的最关键部分,使用异常处理也是最优策略。\n", 106 | "通常出现这种情况的场景是:你希望程序能在出错时候恢复,且使用异常之外的其它错误恢复方法效率低下。\n", 107 | "以下示例说明了这种情况:\n", 108 | "\n", 109 | "```cpp\n", 110 | "// Example 7.49\n", 111 | "// Portability note: This example is specific to Microsoft compilers.\n", 112 | "// It will look different in other compilers.\n", 113 | "#include \n", 114 | "#include \n", 115 | "#include \n", 116 | "#define EXCEPTION_FLT_OVERFLOW 0xC0000091L\n", 117 | "\n", 118 | "void MathLoop() {\n", 119 | " const int arraysize = 1000; unsigned int dummy;\n", 120 | " double a[arraysize], b[arraysize], c[arraysize];\n", 121 | " // Enable exception for floating point overflow:\n", 122 | " _controlfp_s(&dummy, 0, _EM_OVERFLOW);\n", 123 | " // _controlfp(0, _EM_OVERFLOW); // if above line doesn't work\n", 124 | " int i = 0; // Initialize loop counter outside both loops\n", 125 | " // The purpose of the while loop is to resume after exceptions:\n", 126 | " while (i < arraysize) {\n", 127 | " // Catch exceptions in this block:\n", 128 | " __try {\n", 129 | " // Main loop for calculations:\n", 130 | " for ( ; i < arraysize; i++) {\n", 131 | " // Overflow may occur in multiplication here:\n", 132 | " a[i] = log (b[i] * c[i]);\n", 133 | " }\n", 134 | " }\n", 135 | " // Catch floating point overflow but no other exceptions:\n", 136 | " __except (GetExceptionCode() == EXCEPTION_FLT_OVERFLOW ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {\n", 137 | " // Floating point overflow has occurred.\n", 138 | " // Reset floating point status:\n", 139 | " _fpreset();\n", 140 | " _controlfp_s(&dummy, 0, _EM_OVERFLOW);\n", 141 | " // _controlfp(0, _EM_OVERFLOW); // if above doesn't work\n", 142 | " // Re-do the calculation in a way that avoids overflow:\n", 143 | " a[i] = log(b[i]) + log(c[i]);\n", 144 | " // Increment loop counter and go back into the for-loop:\n", 145 | " i++;\n", 146 | " }\n", 147 | " }\n", 148 | "}\n", 149 | "```" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "假定`b[i]`和`c[i]`中的数字太大,以至于在乘法`b[i] * c[i]`中可能发生溢出,尽管这种情况很少发生。\n", 157 | "上面的代码将在溢出的情况下捕获异常,并以一种效率更低,但却能避免溢出的方式重做计算。\n", 158 | "即:取每个因子的对数,而不是乘积的对数,确保不会发生溢出,但计算时间会加倍。\n", 159 | "\n", 160 | "这里,用来支撑异常处理所需的时间可以忽略不计,因为在关键的最内层循环中没有`try`块,也没有函数调用(除了`log`函数)。\n", 161 | "`log`是一个库函数,我们假设它是非常优化的。\n", 162 | "不管怎样,我们无法去除为`log`函数可能的异常处理提供的支撑(即记录额外的信息帮助在异常时恢复)。\n", 163 | "当异常真的发生时,代价是高昂的。但这不是问题,因为我们知道这种情况很少发生。\n", 164 | "\n", 165 | "在循环内判断溢出,并不需要任何代价,因为我们依靠的是微处理器硬件在溢出时引发的硬件异常。\n", 166 | "如果程序中存在`try`块,操作系统会将异常处理转交到程序中的异常处理程序。\n", 167 | "\n", 168 | "让我们看看这个例子中,有没有异常处理可能的替代方案。\n", 169 | "我们可以检查`b[i]`和`c[i]`是否过大来判断溢出。\n", 170 | "这将需要比较两个浮点数,因为它们必须在最内存的循环内,所以代价很高。\n", 171 | "另一种可能的方法是,始终使用安全公式`a[i] = log(b[i])+ log(c[i]);`。\n", 172 | "这会使`log`函数调用次数加倍,而且对数计算需要很长时间。\n", 173 | "如果有一种方法可以在循环外检查异常,而不用检查所有数组元素,那么这可能是一个更好的解决方案。\n", 174 | "- 如果所有因子都是从相同的几个参数生成的话,在循环之前进行这样的检查或许是可行的。\n", 175 | "- 或者,如果计算结果由某个公式生成的单个结果,在循环后进行检查也许可行。" 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "metadata": {}, 181 | "source": [ 182 | "### 异常和向量代码\n", 183 | "\n", 184 | "向量指令对于多个计算并行执行很有用。\n", 185 | "这将在下面的第12章中描述。\n", 186 | "异常处理与向量代码兼容性不好,因为向量中的单个元素可能导致异常,而其他向量元素可能正常运行。\n", 187 | "你甚至可能会在未采用的分支中发生异常,仅仅是因为分支在向量代码中的特殊实现方式。\n", 188 | "如果代码可以从向量指令中受益,那么最好禁用异常捕获,而依赖`NAN`和`INF`进行错误处理。\n", 189 | "详见下文第7.34章。[www.agner.org/optimize/nan_propagation.pdf]() 文章中也有进一步讨论。" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "### 避免异常处理的开销\n", 197 | "\n", 198 | "当不需要从错误情形中恢复时,异常处理时不必要的。\n", 199 | "当错误发生时,如果你仅仅想要程序输出条错误消息后就终止运行,此时就没有理由使用 `try`, `catch`, 和 `throw`。这时,更有效的方法是:定义你自己的出错处理函数。该函数只需简单输出合适的错误消息,然后调用 `exit`。\n", 200 | "\n", 201 | "当有分配的资源需要被清理是,调用 `exit` 也许并不安全,具体如后面所述。\n", 202 | "还有其它不用异常的选项可供选择。\n", 203 | "发现错误的函数可以返回一个错误代码,以供调用方来进行错误恢复,或者输出错误信息。\n", 204 | "\n", 205 | "推荐使用系统化的,仔细斟酌过的方法来进行出错处理。\n", 206 | "你**必须**要:\n", 207 | "- 区分可恢复的错误和不可恢复的错误\n", 208 | "- 确保分配的资源在出错时会被释放\n", 209 | "- 输出适当的错误消息给用户" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "### 编写异常安全代码\n", 217 | "\n", 218 | "假定一个函数以排斥模式打开了一个文件,然后一个错误情况发生导致程序在文件关闭前终止。程序终止后,文件会保持在锁定状态,直到计算机重启,用户会一直无法访问该文件。\n", 219 | "位防止此类问题发生,你必须确保你的程序**异常安全**。\n", 220 | "换言之,程序必须在异常或者出错情况下清理一切(资源)。可能需要清理的东西包括:\n", 221 | "\n", 222 | "- 使用 new/malloc 分配的内存\n", 223 | "- 窗口的句柄,图形刷等等的句柄\n", 224 | "- 加了锁的互斥对象\n", 225 | "- 打开的数据库连接\n", 226 | "- 打开的文件和网络连接\n", 227 | "- 需要被删除的临时文件\n", 228 | "- 用户需要被保村的工作\n", 229 | "- 其它的已分配的资源\n", 230 | "\n", 231 | "C++ 处理清理工作的方式是使用析构函数。一个包含读写文件操作的函数可以被包装进入一个类。改类的析构函数要确保文件被关闭。\n", 232 | "同样的方法也可以用在其它资源上,例如动态分配的内存,窗口,互斥体,数据库连接,等等。\n", 233 | "\n", 234 | "C++ 异常处理系统会确保局部对象的所有析构函数被调用。\n", 235 | "如果该带有析构函数的包装类负责处理所有的资源清理工作,程序是异常安全的。\n", 236 | "但如果析构函数会引起别的异常,系统可能会失败。\n", 237 | "\n", 238 | "如果你使用自己的出错处理系统,而不是使用异常处理,你不能确保所有的析构函数被调用,所有的资源被清理。\n", 239 | "如果(你自己的)一个出错处理函数调用了`exit()`,`abort()`,`_endthread()`等,那么也不能保证所有的析构函数被调用。\n", 240 | "不适用异常的情况下,处理不可恢复错误的安全方法是直接从函数返回。可能的话,该函数可以返回一个出错代码。或者把出错代码保存在一个全局对象里。调用方的函数必须检查该错误代码。\n", 241 | "如果后一个函数也有一些东西要清理,那么它必须返回给它自己的调用者,依此类推。\n" 242 | ] 243 | } 244 | ], 245 | "metadata": { 246 | "kernelspec": { 247 | "display_name": "Python 3", 248 | "language": "python", 249 | "name": "python3" 250 | }, 251 | "language_info": { 252 | "codemirror_mode": { 253 | "name": "ipython", 254 | "version": 3 255 | }, 256 | "file_extension": ".py", 257 | "mimetype": "text/x-python", 258 | "name": "python", 259 | "nbconvert_exporter": "python", 260 | "pygments_lexer": "ipython3", 261 | "version": "3.9.7" 262 | } 263 | }, 264 | "nbformat": 4, 265 | "nbformat_minor": 2 266 | } 267 | -------------------------------------------------------------------------------- /07-TheEfficiencyOfDifferentC++Constructs/7.33-7.36.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 7.32 栈展开的其它情况\n", 8 | "\n", 9 | "前面的段落描述了一种叫做栈展开的机制。该机制用于异常处理。具体情形是:\n", 10 | "- 当异常发生时候,程序不进入一般的返回路径,而是跳出函数\n", 11 | "- 然后进行清理工作,且调用必要的析构函数\n", 12 | "该机制也用在下面两种情况:\n", 13 | "\n", 14 | "当线程终止时,堆栈展开机制可能被使用。\n", 15 | "这么做的目的是,检测线程中声明的任何对象是否有需要调用的析构函数。\n", 16 | "一般建议,对于需要清理的函数,在终止线程之前,主动从函数返回。\n", 17 | "这是因为,你不能确定对`_endthread()`的调用会清理堆栈。该清理行为依赖于系统的具体实现。\n", 18 | "\n", 19 | "当函数 `longjmp` 被用于跳出函数时,堆栈展开机制也会被使用。\n", 20 | "尽可能避免使用 `longjmp`。 不要在时间关键的代码中调用 `longjmp`。" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "## 7.34 `NAN`和`INF`的传播\n", 28 | "\n", 29 | "在大多数情况下,浮点错误会传播到一系列计算的结果的终点。\n", 30 | "这是异常和故障捕获的一种非常有效的替代方法。\n", 31 | "\n", 32 | "Floating point overflow and division by zero gives infinity. If you add or multiply infinity with\n", 33 | "something you get infinity as a result. The INF code may propagate to the end result in this\n", 34 | "way. However, not all operations with INF input will give INF as a result. If you divide a\n", 35 | "normal number by INF you get zero. The special cases INF-INF and INF/INF give NAN (nota-number). The special code NAN also occurs when you divide zero by zero and when the\n", 36 | "input of a function is out of range, such as sqrt(-1) and log(-1).\n", 37 | "\n", 38 | "Most operations with a NAN input will give a NAN output, so that the NAN will propagate to\n", 39 | "the end result. This is a simple and efficient way of detecting floating point errors. Almost all\n", 40 | "floating point errors will propagate to the end result where they appear as INF or NAN. If\n", 41 | "you print out the results, you will see INF or NAN instead of a number. No extra code is\n", 42 | "needed to keep track of the errors, and there is no extra cost to the propagation of INF and\n", 43 | "NAN.\n", 44 | "\n", 45 | "A NAN can contain a payload with extra information. A function library can put an error code\n", 46 | "into this payload in case of an error, and this payload will propagate to the end result.\n", 47 | "\n", 48 | "The function finite() will return false when the parameter is INF or NAN, and true if it is a\n", 49 | "normal floating point number. This can be used for detecting errors before a floating point\n", 50 | "number is converted to an integer and in other cases where we want to check for errors.\n", 51 | "\n", 52 | "The details of INF and NAN propagation are further explained in the document \"NAN\n", 53 | "propagation versus fault trapping in floating point code\" at\n", 54 | "www.agner.org/optimize/nan_propagation.pdf. This document also discusses situations\n", 55 | "where the propagation of INF and NAN fails, as well as compiler optimization options that\n", 56 | "influence the propagation of these codes.\n" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## 7.35 Preprocessing directives\n", 64 | "\n", 65 | "Preprocessing directives (everything that begins with #) are costless in terms of program\n", 66 | "performance because they are resolved before the program is compiled.\n", 67 | "\n", 68 | "#if directives are useful for supporting multiple platforms or multiple configurations with the\n", 69 | "same source code. #if is more efficient than if because #if is resolved at compile time\n", 70 | "while if is resolved at runtime.\n", 71 | "\n", 72 | "#define directives are equivalent to const definitions when used for defining constants.\n", 73 | "For example, #define ABC 123 and const int ABC = 123; are equally efficient\n", 74 | "because, in most cases, an optimizing compiler can replace an integer constant with its\n", 75 | "value. However, the const int declaration may in some cases take memory space \n", 76 | "where a #define directive never takes memory space. A floating point constant always\n", 77 | "takes memory space, even when it has not been given a name.\n", 78 | "\n", 79 | "#define directives when used as macros are sometimes more efficient than functions.\n", 80 | "See page 48 for a discussion." 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "## 7.36 Namespaces\n", 88 | "There is no cost in terms of execution speed to using namespaces." 89 | ] 90 | } 91 | ], 92 | "metadata": { 93 | "interpreter": { 94 | "hash": "a14051f21a11444245b2f4ecda18756d21687dd3b2165409207c8164cdeb73d9" 95 | }, 96 | "kernelspec": { 97 | "display_name": "Python 3.9.9 64-bit (windows store)", 98 | "language": "python", 99 | "name": "python3" 100 | }, 101 | "language_info": { 102 | "name": "python", 103 | "version": "3.9.9" 104 | }, 105 | "orig_nbformat": 4 106 | }, 107 | "nbformat": 4, 108 | "nbformat_minor": 2 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optimizing Software In Cpp 2 | 3 | 英文原文链接:http://agner.org/optimize/optimizing_cpp.pdf 4 | 5 | 翻译:https://github.com/eagle-dai/OptimizingSoftwareInCpp 6 | 7 | 这里不是正式翻译,而更像较为详细的读书笔记 8 | - 原文中的长句如果语法结构比较复杂,一般被按照汉语习惯拆成短句 9 | - 多使用白话,避免过多术语 10 | - 格式有所改变,例如,对并列的知识点,多使用列表 11 | - 稍微加入自己的理解 12 | - 有些重点的地方,翻译时加上了黑体强调 13 | - 借助谷歌翻译,但加以修改 14 | 15 | 因工作很忙,翻译全靠个人兴趣挤出时间来做。如果不够准确,欢迎交流。 16 | --------------------------------------------------------------------------------