撒谎,即讲述美丽而不真实的故事,乃是艺术的真正目的。
——奥斯卡 • 王尔德,“谎言的衰朽”,《意图集》,1891 年
本篇的目的是为大家提供与优化技术相关的计算机硬件的最基本的背景知识,这样大家就不必疯狂地研究那 处理器手册了。本篇我们将简单地了解处理器的体系结构,从中获得性能优化的启发。虽然本篇中的信息非常重要且实用,但迫不及待地想学习优化技术的读者可以先跳过本篇,当在后面的篇节中遇到本篇中的知识时再回过头来学习。
如今所使用的微处理器设备的种类多样,从只有几千个逻辑门且时钟频率低于 1MHz 的价 值 1 美元的嵌入式设备,到有数十亿逻辑门且时钟频率达到千兆赫兹级别的桌面级设备。 一台包含数千个独立执行单元的大型计算机的尺寸可以与一个大房间相当,它消耗的电力足够点亮一座小城市中所有的电灯。这很容易让人误以为这些种类繁多的计算设备之间的联系不具有一般性。 但事实上,它们之间是有可利用的相似点的。毕竟,如果没有任何相 似点的话,编译器就无法为这么多处理器编译 C++ 代码了。
所有这些被广泛使用的计算机都会执行存储在内存中的指令。指令所操作的数据也是存储在内存中的。内存被分为许多小的字(word),这些字由若干位(bit)组成。其中一小部分宝贵的内存字是寄存器(register),它们的名字被直接定义在机器指令中。其他绝大多数内存字则都是以数值型的地址(address)命名的。每台计算机中都有一个特殊的寄存器保存着下一条待执行的指令的地址。如果将内存看作一本书,那么执行地址(execution address)就相当于指向要阅读的下一个单词的手指。执行单元(execution unit,也被称为处理器、核心、CPU、运算器等其他名字)从内存中读取指令流,然后执行它们。指令会告诉执行单元要从内存中读取(加载,取得)什么数据,如何处理数据, 以及将什么结果写入(存储、保存)到内存中。计算机是由遵守物理定律的设备组成的。
从内存地址读取数据和向内存地址写入数据是需要花费时间的,指令对数据进行操作也是需要花费时间的。
除了这条基本原则外,就如每个计算机专业新生都知道的,计算机体系结构的“族谱”也会不断地扩大。因为计算机体系结构是易变的,所以很难严格地测量出硬件行为在数值 的规律。现代处理器做了许多不同的、交互的事情来提高指令执行速度,导致指令的执行时间实际上变得难以确定。还有一个问题是,许多开发人员甚至无法准确地知道他们的代 码会运行在什么处理器上,多数情况下只能用试探法。
2.1 C++所相信的计算机谎言
当然,C++ 程序至少会假装相信上节中讲解过的简单的计算机基本模型中的一个版本。其中有可以以固定字符长度的字节为单位寻址,在本质上容量是无限的内存。有一个与其他任何有效的内存地址都不同的特殊的地址,叫作 nullptr。整数 0 会被转换为 nullptr,尽管在地址 0 上不需要 nullptr。有一个概念上的执行地址指向正在被执行的源代码语句。 各条语句会按照编写顺序执行,受到 C++ 控制流程语句的控制。
- C++ 知道计算机远比这个简单模型要复杂。它在这台闪闪发亮的机器下提供了一些快速功能。
- C++ 程序只需要表现得好像语句是按照顺序执行的。C++ 编译器和计算机自身只要能够确保每次计算的含义都不会改变,就可以改变执行顺序使程序运行得更快。
- 自 C++11 开始,C++ 不再认为只有一个执行地址。C++ 标准库现在支持启动和终止线 程以及同步线程间的内存访问。在C++11之前,程序员对C++编译器隐瞒了他们的线程, 有时候这会导致难以调试。 • 某些内存地址可能是设备寄存器,而不是普通内存。这些地址的值可能会在同一个 线程对该地址的两次连续读的间隔发生变化,这表示硬件发生了变化。在 C++ 中用 volatile 关键字定义这些地址。声明一个 volatile 变量会要求编译器在每次使用该变 量时都获取它的一份新的副本,而不用通过将该变量的值保存在一个寄存器中并复用它 来优化程序。另外,也可以声明指向 volatile 内存的指针。
- C++11 提供了一个名为 std::atomic<> 的特性,可以让内存在一段短暂的时间内表现得 仿佛是字节的简单线性存储一样,这样可以远离所有现代处理器的复杂性,包括多线程 执行、多层高速缓存等。有些开发人员误以为这与 volatile 是一样的,其实他们错了。
操作系统也欺骗了程序和用户。实际上,操作系统的目的就是为了给每个程序讲一个让它们信服的谎言。最重要的谎言之一是,操作系统希望每个程序都相信它们是独立运行于计算机上的,而且这些计算机的内存是无限的,还有无限的处理器来运行程序的所有线程。
操作系统会使用计算机硬件来隐藏这些谎言,这样 C++ 不得不相信它们。除了降低程序的运 行速度外,这些谎言其实对程序运行并没有什么影响。不过,它们会导致性能测量变得复杂。
2.2 计算机的真相
只有最简单的微处理器和某些具有悠久历史的大型机才直接与 C++ 模型相符。对性能优化影响优化的计算机行为而言非常重要的是,真实计算机的实际内存硬件的处理速度与指令的执行速率相比是很慢 的。内存并非真的是以字节为单位被访问的,内存并非是一个由相同元素组成的简单的线性数组,而且它的容量也是有限的。真实的计算机可能有不止一个指令地址。真实的计算机非常快,但并非因为它们执行指令非常快,而是因为它们同时执行许多指令,而且它们 内部的复杂电路可以确保这些同时执行的指令表现得就像一个接一个地执行一样。
2.2.1 内存很慢
计算机的主内存相对于它内部的逻辑门和寄存器来说非常慢。将电子从微处理器芯片中注入 相对广阔的一块铜制电路板上的电路,然后将其沿着电路推到几厘米外的内存芯片中,这个 过程所花费的时间为电子穿越微处理器内各个独立的微距晶体管所需时间的数千倍。主内存 太慢,所以桌面级处理器在从主内存中读取一个数据字的时间内,可以执行数百条指令。
优化的根据在于处理器访问内存的开销远比其他开销大,包括执行指令的开销。
诺伊曼瓶颈
通往主内存的接口是限制执行速度的瓶颈。这个瓶颈甚至有一个名字,叫冯 • 诺伊曼 瓶颈。它是以著名的计算机体系结构先锋和数学家约翰 • 冯 • 诺伊曼(1903—1957)的 名字命名的。
例如,一台使用主频为 1000MHz 的 DDR2 内存设备的个人计算机(几年前典型的计 算机,容易计算其性能),其理论带宽是每秒 20 亿字,也就是每字 500 皮秒(ps)。但 这并不意味着这台计算机每 500 皮秒就可以读或写一个随机的数据字。
首先,只有顺序访问才能在一个周期内完成(相当于频率为 1000MHz 的时钟的半个时 标)。而访问一个非连续的位置则会花费 6 至 10 个周期。
多个活动会争夺对内存总线的访问。处理器会不断地读取包含下一条需要执行的指令的内存。高速缓存控制器会将数据内存块保存至高速缓存中,刷新已写的缓存行。 DRAM 控制器还会“偷用”周期刷新内存中的动态 RAM 基本存储单元的电荷。多核处理器的核心数量足以确保内存总线的通信数据量是饱和的。数据从主内存读取至某 个核心的实际速率大概是每字 20 至 80 纳秒(ns)。
根据摩尔定律,每年处理器核心的数量都会增加。但是这也无法让连接主内存的接口 变快。因此,未来核心数量成倍地增加,对性能的改善效果却是递减的。这些核心只 能等待访问内存的机会。上述对性能的隐式限制被称为内存墙(memory wall)。
2.2.2 内存访问并非以字节为单位
虽然 C++ 认为每个字节都是可以独立访问的,但计算机会通过获取更大块的数据来补偿缓慢的内存速度。最小型的处理器可以每次从主内存中获取 1 字节,桌面级处理器则可以立即获取 64 字节。一些超级计算机和图形处理器还可以获取更多。
当 C++ 获取一个多字节类型的数据,比如一个 int、double 或者指针时,构成数据的字节可能跨越了两个物理内存字。这种访问被称为非对齐的内存访问(unaligned memory access)。此处优化的意义在于,一次非对齐的内存访问的时间相当于这些字节在同一个字中时的两倍,因为需要读取两个字。C++ 编译器会帮助我们对齐结构体,使每个字段的起 始字节地址都是该字段的大小的倍数。但是这样也会带来相应的问题:结构体的“洞”中 包含了无用的数据。在定义结构体时,对各个数据字段的大小和顺序稍加注意,可以在保持对齐的前提下使结构体更加紧凑。
2.2.3 某些内存访问会比其他的更慢
为了进一步补偿主内存的缓慢速度,许多计算机中都有高速缓存(cache memory),一种非常接近处理器的快速的、临时的存储,来加快对那些使用最频繁的内存字的访问速度。一些计算机没有高速缓存,其他一些计算机则有一层或多层高速缓存,其中每一层都比前一层更小、更快和更昂贵。当一个执行单元要获取的字节已经被缓存时,无需访问主内存即可立即获得这些字节。高速缓存的速度快多少呢?一种大致的估算经验是,高速缓存层次中每一层的速度大约是它下面一层的 10 倍。在桌面级处理器中,通过一级高速缓存、二级高速缓存、三级高速缓存、主内存和磁盘上的虚拟内存页访问内存的时间开销范围可以跨越五个数量级。这就是专注于指令的时钟周期和其他“奥秘”经常会令人恼怒而且没有效果的一个原因,高速缓存的状态会让指令的执行时间变得非常难以确定。
当执行单元需要获取不在高速缓存中的数据时,有一些当前处于高速缓存中的数据必须被 舍弃以换取足够的空余空间。通常,选择放弃的数据都是最近很少被使用的数据。这一点与性能优化有着紧密的关系,因为这意味着访问那些被频繁地访问过的存储位置的速度会 比访问不那么频繁地被访问的存储位置更快。
读取一个不在高速缓存中的字节甚至会导致许多临近的字节也都被缓存起来(这也意味 着,许多当前被缓存的字节将会被舍弃)。这些临近的字节也就可以被高速访问了。对于 性能优化而言,这一点非常重要,因为这意味着平均而言,访问内存中相邻位置的字节要 比访问互相远隔的字节的速度更快。
就 C++ 而言,这表示一个包含循环处理的代码块的执行速度可能会更快。这是因为组成 循环处理的指令会被频繁地执行,而且互相紧挨着,因此更容易留在高速缓存中。一段包 含函数调用或是含有 if 语句导致执行发生跳转的代码则会执行得较慢,因为代码中各个 独立的部分不会那么频繁地被执行,也不是那么紧邻着。相比紧凑的循环,这样的代码在 高速缓存中会占用更多的空间。如果程序很大,而且缓存有限,那么一些代码必须从高速 缓存中舍弃以为其他代码腾出空间,当下一次需要这段代码时,访问速度会变慢。类似 地,访问包含连续地址的数据结构(如数组或矢量),要比访问包含通过指针链接的节点 的数据结构快,因为连续地址的数据所需的存储空间更少。访问包含通过指针链接的记录 的数据结构(例如链表或者树)可能会较慢,这是因为需要从主内存读取每个节点的数据 到新的缓存行中。
2.2.4 内存字分为大端和小端
处理器可以一次从内存中读取一字节的数据,但是更多时候都会读取由几个连续的字节组成的一个数字。例如,在微软的 Visual C++ 中,读取 int 值时会读取 4 字节。由于同一个 内存可以以两种不同的方式访问,设计计算机的人必须面对一个问题:首字节,即最低地址字节,是组成 int 的最高有效位还是最低有效位呢?
乍一看,这似乎没什么问题。当然,一台计算机中的所有部件就“最低地址是 int 的哪一 端”这一点达成一致是非常重要的,否则就会出现混乱。而且,它们之间的区别是非常明显的。如果 int 值 0x01234567 存储在地址 1000~1003 中,而且首先存储小端,那么在地 址 1000 中存储的是 0x01,在地址 1003 中存储的是 0x67。反之,如果首先存储大端,那 么在地址 1000 中存储的是 0x67,0x01 被存储在地址 1003 中。从首字节地址读取最高有 效位的计算机被称为大端计算机,小端计算机则会首先读取最低有效位。因为有两种存储 整数值(或指针)的方式,而且找不到偏向其中一种的理由,所以工作在不同处理器上的 不同公司的不同团队的选择可能会不同。
问题出在当被写至磁盘上的数据或者由一台计算机通过网络传输的数据会被另外一台计算机读取的时候。磁盘和网络一次只传送一字节,而不是整个 int 值。所以,这关系到哪一 端首先被存储或发送。如果发送数据的计算机与接收数据的计算机在这一点上不一致,那 么发送的 0x01234567 则会被接收为 0x67452301,导致 int 值发生了改变。
字节序(endian-ness)只是 C++ 不能指定 int 中位的存储方式或是设置联合体中的一个字 段会如何影响其他字段的原因之一。所编写的程序可以工作于一类计算机上,却在另一类 计算机上崩溃,原因也在于字节序。
2.2.5 内存容量是有限的
实际上,计算机中的内存容量并非是无限的。为了维持内存容量无限的假象,操作系统可以如同使用高速缓存一样使用物理内存,将没有放入物理内存中的数据作为文件存储在磁盘上。这种机制被称为虚拟内存(virtual memory)。虚拟内存制造出了拥有充足的物理内存的假象。
不过,从磁盘上获取一个内存块需要花费数十毫秒,对现代计算机来说,这几乎是一个恒定值。
想让高速缓存更快是非常昂贵的。一台台式计算机或是手机中可能会有数吉字节的主内存, 但是只有几百万字节的高速缓存。通常,程序和它们的数据不会被存储在高速缓存中。
高速缓存和虚拟内存带来的一个影响是,由于高速缓存的存在,在进行性能测试时,一个函数运行于整个程序的上下文中时的执行速度可能是运行于测试套件中时的万分之一。当运行于整个程序的上下文中时,函数和它的数据不太可能存储至缓存中,而在测试套件的 上下文中,它们则通常会被缓存起来。这个影响放大了减少内存或磁盘使用量带来的优化 收益,而减小代码体积的优化收益则没有任何变化。
第二个影响则是,如果一个大程序访问许多离散的内存地址,那么可能没有足够的高速缓 存来保存程序刚刚使用的数据。这会导致一种性能衰退,称为页抖动(page thrashing)。当 在微处理器内部的高速缓存中发生页抖动时,性能会降低;当在操作系统的虚拟缓存文件 中发生页抖动时,性能会下降为原来的 1/1000。过去,计算机的物理内存很少,页抖动更 加普遍。不过,如今,这个问题仍然会发生。
2.2.6 指令执行缓慢
嵌入在咖啡机和微波炉中的简单的微处理器被设计为执行指令的速度与从内存中获取指令一样快。桌面级微处理器则有额外的资源并发地处理指令,因此它们执行指令的速度可以比从主内存获取指令快很多倍,多数时候都需要高速缓存去“喂饱”它们的执行单元。对优化而言,这意味着内存访问决定了计算开销。
如果没有其他东西“妨碍”,现代桌面级处理器可以以惊人的速率执行指令。它们每几百皮秒(1 皮秒是 10-12 秒,一段非常非常短的时间)就可以完成一次指令处理。但这并不意 味着每条指令只需要皮秒数量级的时间即可执行完毕。处理器中包含一条指令“流水线”, 它支持并发执行指令。指令在流水线中被解码、获取参数、执行计算,最后保存处理结 果。处理器的性能越强大,这条流水线就越复杂。它会将指令分解为若干阶段,这样就可 以并发地处理更多的指令。
如果指令 B 需要指令 A 的计算结果,那么在计算出指令 A 的处理结果前是无法执行指令 B 的计算的。这会导致在指令执行过程中发生流水线停滞(pipeline stall)——一个短暂的暂 停,因为两条指令无法完全同时执行。如果指令 A 需要从内存中获取值,然后进行运算得 到线程 B 所需的值,那么流水线停滞时间会特别长。流水线停滞会拖累高性能微处理器, 让它变得与烤面包机中的处理器的速度一样慢。
2.2.7 计算机难以作决定
另一个会导致流水线停滞的原因是计算机需要作决定。大多数情况下,在执行完一条指令 后,处理器都会获取下一个内存地址中的指令继续执行。这时,多数情况下,下一条指令已经被保存在高速缓存中了。一旦流水线的第一道工序变为可用状态,指令就可以连续地进入到流水线中。
但是控制转义指令略有不同。跳转指令或跳转子例程指令会将执行地址变为一个新的值。 在执行跳转指令一段时间后,执行地址才会被更新。在这之前是无法从内存中读取“下 一条”指令并将其放入到流水线中的。新的执行地址中的内存字不太可能会存储在高速 缓存中。在更新执行地址和加载新的“下一条”指令到流水线中的过程中,会发生流水 线停滞。
在执行了一个条件分支指令后,执行可能会走向两个方向:下一条指令或者分支目标地址 中的指令。最终会走向哪个方向取决于之前的某些计算的结果。这时,流水线会发生停 滞,直至与这些计算结果相关的全部指令都执行完毕,而且还会继续停滞一段时间,直至决定一下条指令的地址并取得下一条指令为止。
对性能优化而言,这一项的意义在于计算比做决定更快。
2.2.8 程序执行中的多个流
任何运行于现代操作系统中的程序都会与同时运行的其他程序、检查磁盘或者新的 Java 和 Flash 版本的定期维护进程以及控制网络接口、磁盘、声音设备、加速器、温度计和其他 外设的操作系统的各个部分共享计算机。每个程序都会与其他程序竞争计算机资源。
程序不会过多在意这些事情。它只是会运行得稍微慢一点而已。不过有一个例外,那就是 当许多程序一齐开始运行,互相竞争内存和磁盘时。为了性能调优,如果一个程序必须在 启动时执行或是在负载高峰期时执行,那么在测量性能时也必须带上负载。
在 2016 年早期,台式计算机有多达 16 个处理器核心。手机和平板电脑中的微处理器也有 多达 8 个核心。但是,快速地浏览下 Windows 的任务管理器、Linux 的进程状态输出结果 和 Android 的任务列表就可以发现,微处理器所执行的软件进程远比这个数量大,而且绝 大多数进程都有多个线程在执行。操作系统会执行一个线程一段很短的时间,然后将上下 文切换至其他线程或进程。对程序而言,就仿佛执行一条语句花费了一纳秒,但执行下一 条语句花费了 60 毫秒。
切换上下文究竟是什么意思呢?如果操作系统正在将一个线程切换至同一个程序的另外一 个线程,这表示要为即将暂停的线程保存处理器中的寄存器,然后为即将被继续执行的线程加载之前保存过的寄存器。现代处理器中的寄存器包含数百字节的数据。当新线程继续执行时,它的数据可能并不在高速缓存中,所以当加载新的上下文到高速缓存中时,会有一个缓慢的初始化阶段。因此,切换线程上下文的成本很高。
当操作系统从一个程序切换至另外一个程序时,这个过程的开销会更加昂贵。所有脏的高速缓存页面(页面被入了数据,但还没有反映到主内存中)都必须被刷新至物理内存中。 所有的处理器寄存器都需要被保存。然后,内存管理器中的“物理地址到虚拟地址”的内 存页寄存器也需要被保存。接着,新线程的“物理地址到虚拟地址”的内存页寄存器和处 理器寄存器被载入。最后就可以继续执行了。但是这时高速缓存是空的,因此在高速缓存 被填充满之前,还有一段缓慢且需要激烈地竞争内存的初始化阶段。
当一个程序必须等某个事件发生时,它甚至可能会在这个事件发生后继续等待,直至操作 系统让处理器为继续执行程序做好准备。这会导致当程序运行于其他程序的上下文中,竞 争计算机资源时,程序的运行时间变得更长和更加难以确定。
为了能够达到更好的性能,一个多核处理器的执行单元及相关的高速缓存,与其他的执行 单元及相关的高速缓存都是或多或少互相独立的。不过,所有的执行单元都共享同样的主 内存。执行单元必须竞争使用那些将可以它们链接至主内存的硬件,使得在拥有多个执行 单元的计算机中,冯 • 诺依曼瓶颈的限制变得更加明显。
当执行单元写值时,这个值会首先进入高速缓存内存。不过最终,这个值将被写入至主内 存中,这样其他所有的执行单元就都可以看见这个值了。但是,这些执行单元在访问主内 存时存在着竞争,所以可能在执行单元改变了一个值,然后又执行几百个指令后,主内存中的值才会被更新。
因此,如果一台计算机有多个执行单元,那么一个执行单元可能需要在很长一段时间后才 能看见另一个执行单元所写的数据被反映至主内存中,而且主内存发生改变的顺序可能与 指令的执行顺序不一样。受到不可预测的时间因素的干扰,执行单元看到的共享内存字中 的值可能是旧的,也可能是被更新后的值。这时,必须使用特殊的同步指令来确保运行于 不同执行单元间的线程看到的内存中的值是一致的。对优化而言,这意味着访问线程间的 共享数据比访问非共享数据要慢得多。
2.2.9 调用操作系统的开销是昂贵的
除了最小的处理器外,其他处理器都有硬件可以确保程序之间是互相隔离的。这样,程序 A 不能读写和执行属于程序 B 的物理内存。这个硬件还会保护操作系统内核不会被程序覆 写。另一方面,操作系统内核需要能够访问所有程序的内存,这样程序就可以通过系统调 用访问操作系统。有些操作系统还允许程序发送访问共享内存的请求。许多系统调用的发 生方式和共享内存的分布方式是多样和神秘的。对优化而言,这意味着系统调用的开销是 昂贵的,是单线程程序中的函数调用开销的数百倍。
2.3 C++也会说谎
C++ 对用户所撒的最大的谎言就是运行它的计算机的结构是简单的、稳定的。为了假装相 信这条谎言,C++ 让开发人员不用了解每种微处理器设备的细节即可编程,如同正在使用 真实得近乎残酷的汇编语言编程一样。
2.3.1 并非所有语句的性能开销都相同
在 Kernighan 和 Ritchie 的《C 程序设计语言》一书中,所有语句的性能开销都一样。一个 函数调用可能包含任意复杂的计算。但一个赋值语句通常只是将保存在一个寄存器中的内 容变为另外一个内容保存在另一个寄存器中。因此,以下赋值语句
int i,j;
...
i = j;
会从 j 中复制 2 或 4 字节到 i 中。所声明的变量类型可能是 int、float 或 struct big struct *,但是赋值语句所做的工作量是一样的。
不过现在,这已经不再是正确的了。在 C++ 中,将一个 int 赋值给另外一个 int 的工作量 与相应的 C 语言赋值语句的工作量是完全一样的。但是,一个赋值语句,如 BigInstance i = OtherObject; 会复制整个对象的结构。更值得注意的是,这类赋值语句会调用 BigInstance 的构造函数,而其中可能隐藏了不确定的复杂性。当一个表达式被传递给一 个函数的形参时,也会调用构造函数。当函数返回值时也是一样的。而且,由于算数操作 符和比较操作符也可以被重载,所以 A=B*C; 可能是 n 维矩阵相乘,if (x<y)...可能比较 的是具有任意复杂度的有向图中的两条路径。对优化而言,这一点的意义是某些语句隐藏 了大量的计算,但从这些语句的外表上看不出它的性能开销会有多大。
先学习 C++ 的开发人员不会对此感到惊讶。但是对那些先学习 C 的开发人员来说,他们 的直觉可能会将他们引向灾难性的歧途。
2.3.2 语句并非按顺序执行
C++ 程序表现得仿佛它们是按顺序执行的,完全遵守了 C++ 流程控制语句的控制。上句话中的含糊其辞的“仿佛”正是许多编译器进行优化的基础,也是现代计算机硬件的许多技 巧的基础。
当然,在底层,编译器能够而且有时也确实会对语句进行重新排序以改善性能。但是编译 器知道在测试一个变量或是将其赋值给另外一个变量之前,必须先确定它包含了所有的最新计算结果。现代处理器也可能会选择乱序执行指令,不过它们包含了可以确保在随后读 取同一个内存地址之前,一定会先向该地址写入值的逻辑。甚至微处理器的内存控制逻辑 可能会选择延迟写入内存以优化内存总线的使用。但是内存控制器知道哪次写值正在从执行单元穿越高速缓存飞往主内存的“航班”中,而且确保如果随后读取同一个地址时会使 用这个“航班”中的值。
并发会让情况变得复杂。C++ 程序在编译时不知道是否会有其他线程并发运行。C++ 编译器不知道哪个变量——如果有的话——会在线程间共享。当程序中包含共享数据的并发线 程时,编译器对语句的重排序和延迟写入主内存会导致计算结果与按顺序执行语句的计算 结果不同。开发人员必须向多线程程序中显式地加入同步代码来确保可预测的行为的一致性。当并发线程共享数据时,同步代码降低了并发量。
转载请注明:xuhss » C++ 性能优化篇一《优化概述》