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

Mhhao

关注

阅读 9

2022-04-17

目录

概述

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

计算机的真相

        

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

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

流水线停滞  

程序执行中的多个流

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

C++也会说谎

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

语句并非按顺序执行


概述

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

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

计算机的真相

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

        

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

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

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

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

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

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

流水线停滞  

        如果指令B需要指令A的计算结果,那么在计算出指令A的处理结果前是无法执行指令B的计算的。这会导致在指令执行过程中发生流水线停滞(pipeline stall)——一个短暂的暂停,因为两条指令无法完全同时执行。如果指令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是一个矩阵,那他们的执行复杂度也是相差甚大。

语句并非按顺序执行

精彩评论(0)

0 0 举报