《C++性能优化指南》 linux版代码及原理解读 第二章

目录

概述

C++所相信的计算机谎言

计算机的真相

        

某些内存访问会比其他的更慢

内存容量是有限的,但对于程序来说是无限的

流水线停滞  

程序执行中的多个流

调用操作系统的开销是昂贵的

C++也会说谎

并非所有语句的性能开销都相同

语句并非按顺序执行


概述

本章节主要通过讲解部分计算机硬件的基本知识背景,让读者知道很多时候的计算机的表现并不如结果看起来的那样简单,甚至也不像某些书籍中教导的那样运行。

所有这些被广泛使用的计算机都会执行存储在内存中的指令。指令所操作的数据也是存储在内存中的。内存被分为许多小的字(word),这些字由若干位(bit)组成。其中一小部分宝贵的内存字是寄存器(register),它们的名字被直接定义在机器指令中。其他绝大多数内存字则都是以数值型的地址(address)命名的。每台计算机中都有一个特殊的寄存器保存着下一条待执行的指令的地址。如果将内存看作一本书,那么执行地址(execution address)就相当于指向要阅读的下一个单词的手指。执行单元(execution unit,也被称为处理器、核心、CPU、运算器等其他名字)从内存中读取指令流,然后执行它们。指令会告诉执行单元要从内存中读取(加载,取得)什么数据,如何处理数据,以及将什么结果写入(存储、保存)到内存中。计算机是由遵守物理定律的设备组成的。内存地址读取数据和向内存地址写入数据是需要花费时间的,指令对数据进行操作也是需要花费时间的。

C++所相信的计算机谎言

• C++程序只需要表现得好像语句是按照顺序执行的。C++编译器和计算机自身只要能够确保每次计算的含义都不会改变,就可以改变执行顺序使程序运行得更快。(其实代码的执行顺序不一定是按照用户编写的顺序执行的,比如代码 a += 1 ; b = 10; 有时候可能b变量的赋值会比a的要靠前执行。)

• 自C++11开始,C++不再认为只有一个执行地址。C++标准库现在支持启动和终止线程以及同步线程间的内存访问。在C++11之前,程序员对C++编译器隐瞒了他们的线程,有时候这会导致难以调试。

• 某些内存地址可能是设备寄存器,而不是普通内存。这些地址的值可能会在同一个线程对该地址的两次连续读的间隔发生变化,这表示硬件发生了变化。在C++中用volatile关键字定义这些地址。声明一个volatile变量会要求编译器在每次使用该变量时都获取它的一份新的副本,而不用通过将该变量的值保存在一个寄存器中并复用它来优化程序。另外,也可以声明指向volatile内存的指针。

• C++11提供了一个名为std::atomic<>的特性,可以让内存在一段短暂的时间内表现得仿佛是字节的简单线性存储一样,这样可以远离所有现代处理器的复杂性,包括多线程执行、多层高速缓存等。有些开发人员误以为这与volatile是一样的,其实他们错了。

计算机的真相

        实际而言,计算机的内存硬件的处理速度和指令的执行速度相差很大,往往能相差几个数量级。一般来说,计算机会通过缓存来弥补这个不足,比如桌面级处理器可以一次获取64字节的数据,像一些超级计算机可以一次获取512甚至更多,注意每次内存的操作都是64 byte对齐的。当我们需要考虑内存的获取的时候,我们也要明白如果计算机每次取的内存数据是对齐的,比如每次都是按照64倍数的地址开始获取数据,然后每次获取64字节的数据,进行处理,但是有时候我们编写的程序会导致我们希望取出来的数据横跨在两个物理内存字上面,这就导致CPU需要执行两次操作,这种成为非对齐的内存访问(unaligned memory access)。这种优化的意义也是显而易见的,我们可以通过C++编译器或者手动选择对齐,让我们的数据能进行64 byte对齐,但是这样也会导致一些问题,比如数据和数据之间会有一些无法使用到的内存,而这部分只是为了能让数据对齐。

        

某些内存访问会比其他的更慢

        计算机的CPU一般都具有多级缓存,越靠近CPU的缓存,容量越小,但是速度越快。每层缓存之间的速度可能相差一个或者几个数量级。CPU获取数据都是通过缓存来获取的。在桌面级处理器中,通过一级高速缓存、二级高速缓存、三级高速缓存、主内存和磁盘上的虚拟内存页访问内存的时间开销范围可以跨越五个数量级。

        当需要获取不在缓存中的数据时,缓存中的部分数据需要被替换成需要的数据,但并不是缓存中所有的数据都会被清空。而哪一部分会被替换出去,一般会按照最近最少使用的数据进行丢弃,这意味着访问那些被频繁地访问过的存储位置的速度会比访问不那么频繁地被访问的存储位置更快。这就是CPU的空间局部性原则。

        通过这些,我们可以推测出CPU执行指令时有以下几个特点:

  1. 一个包含循环处理的代码块的执行速度可能会更快。(这是因为组成循环处理的指令会被频繁地执行,而且互相紧挨着,因此更容易留在高速缓存中。)同理一个包含if语句或者函数调用这种导致执行发生跳转的代码执行的会更慢。
  2. 访问连续的数据结构的速度会比访问不连续的数据结构快。因为不连续的数据结构需要频繁的触发缓存未命中时间将所需要的数据添加到缓存中。

内存容量是有限的,但对于程序来说是无限的

        计算机中的内存容量并非是无限的。为了维持内存容量无限的假象,操作系统可以如同使用高速缓存一样使用物理内存,将没有放入物理内存中的数据作为文件存储在磁盘上。这种机制被称为虚拟内存(virtual memory)。虚拟内存制造出了拥有充足的物理内存的假象。

        从磁盘上获取一个内存块需要花费数十毫秒,对现代计算机来说,这几乎是一个恒定值。

        高速缓存和虚拟内存带来的一个影响是,由于高速缓存的存在,在进行性能测试时,一个函数运行于整个程序的上下文中时的执行速度可能是运行于测试套件中时的万分之一。当运行于整个程序的上下文中时,函数和它的数据不太可能存储至缓存中,而在测试套件的上下文中,它们则通常会被缓存起来。这个影响放大了减少内存或磁盘使用量带来的优化收益,而减小代码体积的优化收益则没有任何变化。

        第二个影响则是,如果一个大程序访问许多离散的内存地址,那么可能没有足够的高速缓存来保存程序刚刚使用的数据。这会导致一种性能衰退,称为页抖动(page thrashing)。 当在微处理器内部的高速缓存中发生页抖动时,性能会降低;当在操作系统的虚拟缓存文件中发生页抖动时,性能会下降为原来的1/1000。过 去,计算机的物理内存很少,页抖动更加普遍。不过,如今,这个问题仍然会发生。

流水线停滞  

        如果指令B需要指令A的计算结果,那么在计算出指令A的处理结果前是无法执行指令B的计算的。这会导致在指令执行过程中发生流水线停滞(pipeline stall)——一个短暂的暂停,因为两条指令无法完全同时执行。如果指令A需要从内存中获取值,然后进行运算得到线程B所需的值,那么流水线停滞时间会特别长。流水线停滞会拖累高性能微处理器。

        

        一个会导致流水线停滞的原因是计算机需要作决定。大多数情况下,在执行完一条指令后,处理器都会获取下一个内存地址中的指令继续执行。这时,多数情况下,下一条指令已经被保存在高速缓存中了。一旦流水线的第一道工序变为可用状态,指令就可以连续地进入到流水线中。如果我们执行了一条条件分支语句之后,程序执行会有两种可能,下一条语句或者分支指令。而具体的执行哪一个是按照条件的执行结果确定的,在等待计算结果的过程中,以及决定出下一条指令的地址并取出,流水线都会停滞。

        还有一种条件分支是控制转义, 跳转指令或跳转子例程指令会将执行地址变为一个新的值。在执行跳转指令一段时间后,执行地址才会被更新。在这之前是无法从内存中读取“下一条”指令并将其放入到流水线中的。新的执行地址中的内存字不太可能会存储在高速缓存中。在更新执行地址和加载新的“下一条”指令到流水线中的过程中,会发生流水线停滞。       

程序执行中的多个流

        当操作系统从一个程序切换至另外一个程序时,这个过程的开销会更加昂贵。所有脏的高速缓存页面(页面被入了数据,但还没有反映到主内存中)都必须被刷新至物理内存中。所有的处理器寄存器都需要被保存。然后,内存管理器中的“物理地址到虚拟地址”的内存页寄存器也需要被保存。接着,新线程的“物理地址到虚拟地址”的内存页寄存器和处理器寄存器被载入。最后就可以继续执行了。但是这时高速缓存是空的,因此在高速缓存被填充满之前,还有一段缓慢且需要激烈地竞争内存的初始化阶段。

         计算机不止有一个指令地址,他可以同时执行多个指令,这也是它执行指令为什么这么快的原因。

调用操作系统的开销是昂贵的

        除了最小的处理器外,其他处理器都有硬件可以确保程序之间是互相隔离的。这样,程序A不能读写和执行属于程序B的物理内存。这个硬件还会保护操作系统内核不会被程序覆写。另一方面,操作系统内核需要能够访问所有程序的内存,这样程序就可以通过系统调用访问操作系统。有些操作系统还允许程序发送访问共享内存的请求。许多系统调用的发生方式和共享内存的分布方式是多样和神秘的。对优化而言,这意味着系统调用的开销是昂贵的,是单线程程序中的函数调用开销的数百倍。

C++也会说谎

        C++为了用户的编程简单,所以在内部隐藏了大量的细节,但是这也导致有些时候程序的表现可能不像预期的那样运行。

并非所有语句的性能开销都相同

        在以前来说,赋值操作的性能开销是一样的,就是将一个寄存器中的数值保存到另一个寄存器当中。但是随着语法的发展,新特性的加入,这些也不是一成不变的。

        比如以下代码

int i , j ;
// ........process......
i = j ;

         像这种内置的数据类型的赋值,就是将数据对应的内容复制。但是假如是以下的代码:

class T;//declaration 
T t1 = T();

        在这个代码中,同样是赋值语句,但是内部所执行的操作就与上述的有很大不同。这其中甚至会调用到类的构造函数、析构甚至更多。

        同样,相同的操作

template <typename T>
T a , b , c;
a = b * c;

        如果T是int,或者如果T是一个矩阵,那他们的执行复杂度也是相差甚大。

语句并非按顺序执行

        

        C++程序表现得仿佛它们是按顺序执行的,完全遵守了C++流程控制语句的控制。上句话的含糊其辞的“仿佛”正是许多编译器进行优化的基础,也是现代计算机硬件的许多技巧的基础。当然,在底层,编译器能够而且有时也确实会对语句进行重新排序以改善性能。但是编译器知道在测试一个变量或是将其赋值给另外一个变量之前,必须先确定它包含了所有的最新计算结果。现代处理器也可能会选择乱序执行指令,不过它们包含了可以确保在随后读取同一个内存地址之前,一定会先向该地址写入值的逻辑。甚至微处理器的内存控制逻辑可能会选择延迟写入内存以优化内存总线的使用。但是内存控制器知道哪次写值正在从执行单元穿越高速缓存飞往主内存的“航班”中,而且确保如果随后读取同一个地址时会使用这个“航班”中的值。

        并发会让情况变得复杂。C++程序在编译时不知道是否会有其他线程并发运行。C++编译器不知道哪个变量——如果有的话——会在线程间共享。当程序中包含共享数据的并发线程时,编译器对语句的重排序和延迟写入主内存会导致计算结果与按顺序执行语句的计算结果不同。开发人员必须向多线程程序中显式地加入同步代码来确保可预测的行为的一致性。当并发线程共享数据时,同步代码降低了并发量。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++性能优化 指南(强列推荐) chm Part I: Everything But the Code Chapter 1. Optimizing: What Is It All About? Performance Footprint Summary Chapter 2. Creating a New System System Requirements System Design Issues The Development Process Data Processing Methods Summary Chapter 3. Modifying an Existing System Identifying What to Modify Beginning Your Optimization Analyzing Target Areas Performing the Optimizations Summary Part II: Getting Our Hands Dirty Chapter 4. Tools and Languages Tools You Cannot Do Without Optimizing with Help from the Compiler The Language for the Job Summary Chapter 5. Measuring Time and Complexity The Marriage of Theory and Practice System Influences Summary Chapter 6. The Standard C/C++ Variables Variable Base Types Grouping Base Types Summary Chapter 7. Basic Programming Statements Selectors Loops Summary Chapter 8. Functions Invoking Functions Passing Data to Functions Early Returns Functions as Class Methods Summary Chapter 9. Efficient Memory Management Memory Fragmentation Memory Management Resizable Data Structures Summary Chapter 10. Blocks of Data Comparing Blocks of Data The Theory of Sorting Data Sorting Techniques Summary Chapter 11. Storage Structures Arrays Linked Lists Hash Tables Binary Trees Red/Black Trees Summary Chapter 12. Optimizing IO Efficient Screen Output Efficient Binary File IO Efficient Text File IO Summary Chapter 13. Optimizing Your Code Further Arithmetic Operations Operating System–Based Optimizations Summary Part III: Tips and Pitfalls Chapter 14. Tips Tricks Preparing for the Future Chapter 15. Pitfalls Algorithmic Pitfalls Typos that Compile Other Pitfalls
性能优化指南是指在软件或系统开发过程中,通过一系列的优化措施来提升程序的执行效率和系统的性能表现。这样可以使得软件和系统更加高效地运行,提供更好的用户体验。 性能优化的目标在于减少程序的资源消耗、提高响应速度和降低延迟。为了达到这些目标,我们可以从多个方面入手: 1. 代码优化:通过优化算法、减少冗余代码、精简逻辑等方式,改善代码的执行效率。同时,要避免频繁的内存分配和释放操作,使用更高效的数据结构和算法。 2. 并发优化:对于一些多线程或并行计算的场景,可以采用合适的并发模型和数据共享机制,避免竞争条件和死锁,提高并发性能。 3. 数据库优化:优化数据库的设计和索引,合理使用查询语句和事务,避免频繁的数据库访问和大量的数据传输,提高数据库的读写效率。 4. 网络优化:优化网络通信协议和数据传输方式,减少数据传输量和网络延迟,提高网络通信的效率。 5. 缓存优化:通过合理设置缓存机制,减少重复计算和数据查询的开销,提高系统的响应速度。 6. 资源管理优化:合理管理系统资源,及时释放未使用的资源,减少资源的浪费。 7. 日志优化:精简和优化日志输出,避免不必要的日志记录,减轻系统负担。 8. 前端优化:优化前端页面的加载速度和渲染性能,减少页面的请求次数和数据量,提高用户体验。 综上所述,性能优化是一个综合性的工作,要从代码、并发、数据库、网络、缓存、资源管理、日志和前端等多个方面入手。通过适当的优化策略和手段,可以提升软件和系统的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值