程序执行中的多个流
任何运行于现代操作系统中的程序都会与同时运行的其他程序、检查磁盘或者新的 Java 和
Flash 版本的定期维护进程以及控制网络接口、磁盘、声音设备、加速器、温度计和其他
外设的操作系统的各个部分共享计算机。每个程序都会与其他程序竞争计算机资源。
程序不会过多在意这些事情。它只是会运行得稍微慢一点而已。不过有一个例外,那就是
当许多程序一齐开始运行,互相竞争内存和磁盘时。为了性能调优,如果一个程序必须在
启动时执行或是在负载高峰期时执行,那么在测量性能时也必须带上负载。
在 2016 年早期,台式计算机有多达 16 个处理器核心。手机和平板电脑中的微处理器也有
多达 8 个核心。但是,快速地浏览下 Windows 的任务管理器、Linux 的进程状态输出结果
和 Android 的任务列表就可以发现,微处理器所执行的软件进程远比这个数量大,而且绝
大多数进程都有多个线程在执行。操作系统会执行一个线程一段很短的时间,然后将上下
文切换至其他线程或进程。对程序而言,就仿佛执行一条语句花费了一纳秒,但执行下一
条语句花费了 60 毫秒
切换上下文究竟是什么意思呢?如果操作系统正在将一个线程切换至同一个程序的另外一
个线程,这表示要为即将暂停的线程保存处理器中的寄存器,然后为即将被继续执行的线
程加载之前保存过的寄存器。现代处理器中的寄存器包含数百字节的数据。当新线程继续
执行时,它的数据可能并不在高速缓存中,所以当加载新的上下文到高速缓存中时,会有
一个缓慢的初始化阶段。因此,切换线程上下文的成本很高。
当操作系统从一个程序切换至另外一个程序时,这个过程的开销会更加昂贵。所有脏的高
速缓存页面(页面被入了数据,但还没有反映到主内存中)都必须被刷新至物理内存中。
所有的处理器寄存器都需要被保存。然后,内存管理器中的“物理地址到虚拟地址”的内
存页寄存器也需要被保存。接着,新线程的“物理地址到虚拟地址”的内存页寄存器和处
理器寄存器被载入。最后就可以继续执行了。但是这时高速缓存是空的,因此在高速缓存
被填充满之前,还有一段缓慢且需要激烈地竞争内存的初始化阶段。
当一个程序必须等某个事件发生时,它甚至可能会在这个事件发生后继续等待,直至操作
系统让处理器为继续执行程序做好准备。这会导致当程序运行于其他程序的上下文中,竞
争计算机资源时,程序的运行时间变得更长和更加难以确定。
为了能够达到更好的性能,一个多核处理器的执行单元及相关的高速缓存,与其他的执行
单元及相关的高速缓存都是或多或少互相独立的。不过,所有的执行单元都共享同样的主
内存。执行单元必须竞争使用那些将可以它们链接至主内存的硬件,使得在拥有多个执行
单元的计算机中,冯 • 诺依曼瓶颈的限制变得更加明显。
当执行单元写值时,这个值会首先进入高速缓存内存。不过最终,这个值将被写入至主内
存中,这样其他所有的执行单元就都可以看见这个值了。但是,这些执行单元在访问主内
存时存在着竞争,所以可能在执行单元改变了一个值,然后又执行几百个指令后,主内存
中的值才会被更新。
因此,如果一台计算机有多个执行单元,那么一个执行单元可能需要在很长一段时间后才
能看见另一个执行单元所写的数据被反映至主内存中,而且主内存发生改变的顺序可能与
指令的执行顺序不一样。受到不可预测的时间因素的干扰,执行单元看到的共享内存字中
的值可能是旧的,也可能是被更新后的值。这时,必须使用特殊的同步指令来确保运行于
不同执行单元间的线程看到的内存中的值是一致的。对优化而言,这意味着访问线程间的
共享数据比访问非共享数据要慢得多。