第2章 现代处理器特性

程序的最终性能由运行它的处理器实现,只有了解目标处理器的特性,才能写出高效的代码。

现代处理器利用了指令级并行技术,同一时刻存在多条指令执行,并且处理器执行的顺序无须和汇编代码给出的指令顺序完全一致,编译器和处理器只需要保证最终结果一致,这类处理器称为“乱序执行处理器”。而严格按照顺序依次执行一条指令,只有前一条执行完成才开始执行下一条指令的处理器,称为“按序处理器”。

处理器的处理速度远快于内存读写速度,为了减少访问数据时的延迟,现代主流处理器主要采用了这两种方式:
        1. 利用程序的局部性特点:采用了一系列小而快的缓存保存正在访问和将要被访问的数据,以近似于内存的价格获得类似于缓存的速度。以减少延迟,是目前主流的CPU所采用的。
        2. 利用并行性在一个控制流由于高延迟的操作而阻塞时,执行另一个控制流。尽量保证运算单元一直在忙碌,以提高硬件的吞吐量,这种方法主要由GPU所采用。

这两种方法没有天然的壁垒,无论是CPU还是GPU都采用了这两种方法,区别只是更侧重哪一种。

现代乱序执行多核向量处理器具有许多和代码性能优化相关的特点:
        1. 指令级并行:主要有流水线,多发射,VLIW,乱序执行,分支预测等技术;
        2. 向量化:主要有SIMT和SIMD技术;
        3. 线程级并行:多核支持的线程级并行是目前处理器性能提升的主要手段;

缓存层次结构:包括缓存组织,缓存特点以及NUMA

2.1 指令级并行

指令级并行要求同时执行的指令之间没有数据,资源依赖和控制依赖。

通过指令级并行,处理器可用调整指令在该处理器上的执行顺序,能够处理某些在编译阶段无法知道的相关关系(如涉及内存引用时);在指令集兼容的条件下,能够允许一个流水线机器上编译的指令,在另一个流水线上也能有效运行。

指令级并行能够利用处理器上的不同组件同时工作,如果程序拥有类型丰富的运算,指令级并行能使处理器性能迅速提高。

2.1.1 指令流水线

现代处理器将指令操作划分为许多不同的阶段,每个阶段由某个单元执行,这样存在多个操作在处理器的多个单元上像多个水流一样向前流动,这称为“流水线执行”,而每个水流就成为流水线。流水线pipeline是一串操作的集合,其中前一个操作的输出是下一个操作的输入。流水线执行允许在同一时钟周期内重叠执行多个指令。经典5阶段流水线示意图:取指令、指令译码、数据加载、操作和写回。

为了充分利用流水线的好处,一些处理器将一些复杂的指令划分为许多更小的指令以增加流水线的长度。流水线系统中存在许多正在执行且还没有执行完的指令,现代处理器能够允许上百条流水线指令同时执行,而每条指令的延迟可能长达几个甚至几十个时钟,最终的结果是某些指令的吞吐量达到每时钟周期几个。

如果每时钟周期超过一条指令的退休,称之为超标量

通常一条指令的计算和另一条指令的访存同时进行,这样能够更好地利用流水线的好处。

为了更好地利用指令流水线,现代处理器通常会增加指令流水线的长度,但是代码中指令级并行是有限度的,一旦达到此限制,再增加流水线长度就不会有好处了。ARM A9的指令流水线长度为8,而A15为13。

带有长流水线的处理器想要达到最佳性能,需要程序给出高度可预测的控制流。代码主要在紧凑循环中执行的程序,可以提供恰当的控制流,比如大型矩阵或者在向量中做算术计算的程序。

2.1.2 乱序执行

乱序执行是指后一条指令比前一条指令先开始执行,但要求这两条指令没有数据及控制依赖。

编译器引入了相应的功能,主要有指令重排序和寄存器重命名

通常还需要软件开发人员以处理器和编译器友好的方式编写代码,以充分挖掘应用具有的并行性,利用处理器的乱序执行功能。

乱序执行需要在执行指令前知道指令之间的依赖关系。

现代处理器对乱序执行有不同程度的支持,比如x86桌面移动和服务器处理器都具有重排序缓冲区(ReOrderBuffer, ROB),并且具有远多于逻辑寄存器数量的物理寄存器以支持寄存器重命名。

乱序执行会重排指令的执行顺序,这要求处理器的发射能力大于其执行能力(相同或不同指令的所有吞吐量)如果处理器的发射能力和执行能力一致,那么ROB中就不会有指令等待重新排列执行顺序(这句话不对劲)。

由于处理器执行不同指令的速度并不相同,因此其发射能力并不一定比执行最快的指令的吞吐量大,比如驻留X86 CPU一个周期能够处理4条整数加法指令,但是其指令发射能力也是一周期4条。

2.1.3 指令多发射

从乱序执行的角度来说,处理器的发射能力最好要大于指令执行能力。只要是具有多个执行单元的处理器,无论是否支持乱序执行,都要求其指令发射能力大于执行能力。

如Nvidia Kepler GPU上,SMX一个周期可以发射8条指令,但是SMX本身却最多消耗6条乘加指令。

许多处理器支持一个周期发射2条或多条指令,但是多条指令要满足一些条件,比如有些处理器要求没有依赖关系,有些处理器只允许访存指令和计算指令同时发射,而Intel Xeon PHI处理器两个周期可以为一个线程发射两条指令,但是这两条指令要没有背靠背的依赖。

指令多发射增加了硬件的复杂度,提高了处理器的指令级并行能力。

2.1.4 分支预测

当处理器遇到分支指令(判断指令,如if)的时候,有两种选择:
        1. 直接执行下一条指令,如果分支是循环的判断条件,则这很可能造成流水线中断;
        2. 选择某条分支执行,一旦选择错误,处理器就需要抛弃已经执行的结果,且从正确的分支开始执行。

如果程序带有许多循环,且循环计数都比较小,或者面对对象的程序中带有许多虚函数的对象,此时处理器很难或者完全不可能预测某个分支的走向。由于此时程序的控制流不可预测,因此处理器常常猜错。处理器需要频繁等待流水线被清空或等待新指令填充,这将大幅度降低处理器的性能

关于分支预测,不同的处理器展现出完全不同的态度,比如x86就激进地优化其分支预测的性能,而ARM和GPU则要保守的多。

2.1.5 VLIW

乱序执行和分支预测增加了硬件的复杂性。乱序执行处理器增加了用于调度指令和检测依赖的硬件资源。

VLIW的并行指令执行是基于编译时已经确定好的调度。由于决定同时执行的工作是由编译器来完成的,处理器不在需要调度硬件。结果VLIW处理器相比其他多数的超标量处理器提供了更加强大的处理能力且更少的硬件复杂性。

VLIW对于一些向量操作非常有效,并且能够组合某些不相关的指令以同时执行,但是VLIW对编译器提出了过高的要求,故实际性能通常并不是很理想。

早期的AMD GPU大量使用VLIW,现在已经全面转向使用SIMT。

2.2 向量化并行

大多数编译器提供了内置函数来避免直接使用汇编指令来编写向量化代码,如X86的SSE和AVX,ARM的NEON,它们都是SIMD模式。而主流的GPU则采用SIMT。

向量化主要是一条向量指令操作向量寄存器的多个元素,这是一种数据并行模式。

2.2.1 SIMD

SIMD是指一条指令作用在多个数据上面。目前Intel X86提供了SSE/AVX指令,向量寄存器长度分别为128位、256位。利用SSE/AVX指令可显著提高处理器能力,但是由于SSE/AVX指令缺乏灵活性和性能的可扩展性,开发人员需要较长的时间才能熟练使用它们。

目前在X86上有多种办法可以使用SIMD指令,比如汇编语言、内置函数和OpenMP4.0。

从编程难度来看,使用汇编语言最难,使用OpenMP4.0最容易,但是大多数时候使用内置函数也可接近或者手工使用汇编优化的的性能。

在处理器支持AVX指令集的条件下,使用SSE/AVX指令编写的程序在Intel和AMD的X86处理器之间可移植。

使用SIMD指令要求开发人员非常熟悉指令的类型、吞吐量和延迟。

因为不同的处理器对SIMD指令的支持程度不同,这不但表现在指令类型很不相同,还表现在同一指令在不同的架构处理器上的延迟和吞吐量可能也不相同,或者某些指令存在某些未公开的性能缺陷。

2.2.2 SIMT

SIMT也是数据并行的一个子集,但是去掉了一些SIMD的一些限制,具有先天的优势。SIMT优势主要有以下几个方面:
        1. SIMT指定了逻辑向量宽度,而隐藏了物理向量宽度,软件开发人员根据逻辑向量宽度来编写代码,这提高了代码性能的可移植性。比如AMD GCN,其逻辑向量宽度为256字节,而物理向量宽度为64字节。
        2. SIMT无需显式地编写向量化代码,其向量化代码逻辑隐藏在多核代码中。而X86多核向量处理器则需要同时使用向量指令和多线程这两种编码方式才能完全发挥性能。
        3. OpenCL和CUDA比内置函数要简单直接,更易于调试、验证和维护。

2.3 线程级并行

线程级并行将处理器内部的并行由指令级和向量级上升到线程级,旨在通过线程级的并行来增加指令吞吐量,提高处理器的资源利用率。线程级并行的思想是:
        1) 在多核处理上,使用多个线程使得多个核心同时执行多条流水线
        2) 在单核处理器上,当某一个线程由于等待数据到达而空闲时,可以立刻导入其他就绪线程来运行(超线程)。

除了提高处理能力和吞吐量,线程级并行也经常用来提高用户的使用体验

2.3.1 内核线程和用户线程

运行在用户空间的线程称为用户线程,同理运行在内核空间的线程称为内核线程。用户线程由库管理,无需操作系统支持,因此其创建,调度无需干扰操作系统的运行,故消耗少。

内核线程的创建、调度和销毁都由操作系统负责,操作系统了解内核线程的运行情况。

当一个用户线程由于资源分配而阻塞时,操作系统无法切换。

由于操作系统内核和物理硬件的限制,一个进程支持的线程数量是有限的,在Linux下可以通过sysconf函数查询得到,通常其值不小于64。

很明显的是,用户线程的缺点正好是内核线程的优点,而内核线程的缺点又正好用户线程的优点,如果能够合理的将用户线程映射到内核线程上,就有可能结合两者优点。

因此,现代库和操作系统将用户线程映射到内核线程。存在4中映射方式:一对多,一对一,多对一,多对多。实际在核心执行的线程数量可能远少于声明或创建的线程数量。

pthread是用户线程库,但是Linux使用了内核级线程实现(一对一映射)。因此pthread线程和GCC的OpenMP线程都是内核线程。

2.3.2 多线程编程库

通常的做法是在串行语言的基础上增加了多线程库或通过编译制导语句给予编译器额外的并行信息,然后由编译器生成代码以达到并行目的。以下是常用的多线程库和编译制导语句:
        1. pthread目前在所有Linux可用。
        2. win32 thread,是windows系统内置的线程API。
        3. OpenMP,是一个开放的,基于编译制导语句的API,由于其与具体系统平台无关,提供比较好的可移植性。
        4. C/C++ 标准线程。
        5. OpenCL和CUDA,他们是基于GPU。

        OpenACC是一个基于加速器的编译制导标准,通常用于GPU、FPGA等并行编程,目前提供编译器的主要厂商有Cray和PGI。

2.3.3 多核上多线程并行要注意的问题

多核处理器上的所有核心共享内存,和有各自的一、二级缓存和寄存器。由于资源共享,在多核处理器上使用线程级并行需要注意的问题有以下几个:
        1. 线程过多:如果系统上的线程数量远远超过核心的数量,那么就会导致频繁的上下文切换,进而降低性能,如缓存污染。通常支持超线程的多核处理器能够使用的线程数最多是物理核心数的两倍。
        2. 数据竞争当多个线程读写同一共享数据时,便会产生竞争,需要同步和互斥处理。一方面,同步通常会导致线程之间的相互等待,潜在地降低了性能;另一方面,如果不使用同步,程序可能无法并行。互斥会导致锁总线,性能下降。

        3. 死锁:线程发生死锁时,处理器都在操作(一直在询问需要的资源是否可用)。但是,线程都在相互等待其他线程释放资源,谁也无法前进一步,处于一种‘’僵死‘’的状态。

        4. 饿死:当一个或多个线程永远没有机会被调度在处理器上执行,而陷入永远的等待状态。

        5. 伪共享:当多个线程读写的数据映射到同一条缓存线上时,如果一个线程更改了数据,那么其他线程对该数据的缓存将会失效,如果线程频繁地更改数据,硬件就需要不停地更新缓存线,这使性能从独享缓存的水平降低到共享缓存或内存的地步。

2.3.4 多线程程序在单核和多核上运行的不同

特点:
        1. :在单核上,多个线程执行锁或者临界区,实际上只有一个线程在执行临界区代码,而核心也只支持一个线程执行,因此不存在冲突。如果某个线程持有锁,那么只是其他线程不会被调度到CPU上执行,影响的只有持有和释放锁的时间,处理器却时刻在运行着。但是在多核上运行时,锁或临界区会导致其余处理器空闲而只允许一个处理器执行持有锁的那个线程,这是一个串行过程,会影响性能。
        2. 负载均衡:在单核上不用考虑负载均衡,因为各个线程轮流执行,当一个线程执行完时,便会执行另一个线程,不存在线程等待问题。而在多核上执行时,此时最终时间由运行时间最长的线程决定。
        3. 任务调度:单核上,任务调度完全是操作系统的工作,无需软件开发人员干预,通常有时间片轮转、优先级算法等。而在多核上运行时,软件开发人员要合理地在核心间分配任务,以尽量同时结束计算。
        4. 程序终止:在多线程环境中,何时终止程序就变得复杂,因为程序终止时需要确定各个线程都已经完成计算。幸运的是,多线程库通常都提供了对应的函数。

2.4 缓存

随着技术发展,内存容量变得越来越大,带宽也在增加(但是增加的幅度比处理器的吞吐量要小),但延迟并没有减少。处理器吞吐量与内存吞吐量和延迟的差异越来越大,这称为内存墙。现代处理器通过几种方式来减少这种差距实际产生的影响:
        1. 每次内存访问读取周围的多个数据,因为这些数据随后极有可能会被用到;
        2. 采用容量小但更快的存储器(称为缓存),如果访问的数据在缓存中,那么就无需去访问内存;
        3. 支持向量访问和同时处理多个访问请求,通过大量并行访问来掩盖延迟

局部性分为时间局部性和空间局部性。

        1. 时间局部性是指当前被访问到的数据随后有可能访问到;

        2. 空间局部性是指当前访问地址附近的地址可能随后被访问;

现代处理器通过在内存和核心之间增加缓存以利用局部性增强程序性能,这样可以用远低于缓存的价格换取缓存的速度。

现代处理器的带宽很高,比如Intel Haswell一级缓存的读带宽为每时钟周期每核心64字节,要发挥其峰值必须要使用向量指令。要克服内存的高延迟,则需要发起多个访问请求,让流水线始终满负荷运行。

软件开发人员应当意识到:对于性能限制在内存/缓存上的程序而言,缓存能够显著增加程序的实际性能,因此要编写缓存友好型的代码,同时在多核的条件下要注意避免伪共享问题导致的性能损失。

2.4.1 缓存层次结构

现代处理器采用了多层次的,容量不同的和性能不同的缓存,其中上一级缓存容量比下一级缓存小,但延迟更小,带宽更大。比如Intel Haswell CPU一级缓存大小为32KB,延迟为3个周期,吞吐量为每周期64字节;其二级缓存大小为256KB,延迟为11个周期,吞吐量为每周期64字节。

现代处理器至少具有二级缓存,某些高端的多核处理器具有三级缓存。通常采用上一级缓存缓存下一级缓存的访问的方式。如寄存器缓存一级缓存访问,一级缓存缓存二级缓存访问,二级缓存缓存三级缓存访问,三级缓存缓存内存访问。当然一些处理器有微小的不同,比如AMD A10 APU中的CPU的一级缓存就不缓存来自寄存器的写操作。

类比于金字塔的形状,某些数据可能同时被缓存在某核心的多个缓存层次上(也可能只缓存某个层次上),如同时被L1和L2缓存。在多核处理器上,某数据还可能被多个核心的缓存所缓存。

对于一次内存访问而言,如果访问的数据在缓存中,则称为缓存命中,如果不在,则称为缓存不命中,为了衡量缓存命中的概率,提出了缓存命中率的概念,其指程序执行过程中缓存命中的次数占总访存次数的百分比。

2.4.2 缓存一致性

由于处理器具有多级缓存,那么如何保证缓存中的数据和内存中的数据是一致性的,这由处理器的缓存一致性协议来保证。(多个处理器的副本或者多层缓存中的副本如何保持一致性)

当多个核心缓存了同一个内存地址的数据,那么一个核心更改了某个地址的数据,其他的核心就需要对该地址数据的缓存失效。一些简单的多核处理器缓存一致性使得缓存一致性的代价和处理器的数目成正比。

为了正确性,一旦某个核心更改了内存中的内容,硬件就必须要保证其他的核心能够读到更新后的数据。目前大多数硬件采用的策略或协议是MESI或基于MESI的变种;
        1. M代表更改(modifed),表示缓存中的数据已经更改,在未来的某个时刻将会写入内存;
        2. E代表独享(exclusive),表示缓存的数据只被当前的核心所缓存;
        3. S代表共享(shared),表示缓存的数据还被其他核心缓存;
        4. I代表无效(invalid),表示缓存中的数据已经失效,即其他核心更改了数据;

多核系统的存储器具有缓存一致性, 
        1. 现在的编译器和编程语言几乎都采用了弱一致性协议
        2. 多个控制流执行的先后顺序通常没有办法控制
        3. 缓存一致性并不保证顺序一致性

对于开发人员而言,缓存一致性是透明的,就如同缓存一样,因此无需过多关注。

缓存失效是一个长延迟的操作,不能完全流水线化,故很多处理器都提供了失效队列来保存缓存失效操作。伪共享本质也是一种缓存一致性问题

2.4.3 缓存缺失

通常有以下几种情况:

        1. 强制缺失,程序开始执行时,由于缓存里根本没有数据,因此无论请求任何数据,都会不命中。通常程序无需特别关注这种情况,但是在测试缓存性能、大小和结构时,要注意这点
        2. 容量缺失,如果缓存已经被完全占用,那么请求的数据如果不在缓存中,就需要将其中某个已缓存的数据x覆盖为新请求的数据。如果数据存在局部性,但是访问的数据的大小超过缓存的大小就会出现满不命中的情况(利用这一点来测试缓存的容量大小)

        3. 冲突缺失,新读取的数据会被放入缓存的某个地址a,a并不是任意的,而是由某些算法决定的(由于内存比缓存大得多,因此多个内存地址会被映射到同一个缓存地址)。如果a中原始数据在随后被访问,那么也不会命中(利用这一点来测试缓存的相连度)。冲突缺失和缓存的组织有关,比如缓存块的长度(利用访问某行时逐渐增加跨度来测试)、每组里面多少缓存块。

实际上,如果只是为了正确性,软件开发人员并不需要关注这几种不命中的情况是否发生。然而为了提高性能,软件开发人员就必须知道硬件缓存缺失的机制和原因。

2.4.4 写缓存

根据写是否命中来说,各有两种基本策略:

如果要写的数据已经在缓存中,即写命中时,有两种策略:
                1. 写回(write back),仅当一个缓存线需要被替换回内存时(缓存已满,或者其他线程需要访问该数据块),才将其内容写入内存或者下一级缓存。如果缓存命中,则总是不用更新内存。为了减少耗时的内存操作次数,缓存行通常还设有一个脏位(diety bit),用以标识该缓存行在被载入之后是否发生过更新。如果一个缓存行在被置换回内存之前从未被写入过,就不用写回内存。
                2. 写直达(write through),每当缓存接收到写数据指令,都直接将数据写回到内存或下一级缓存。如果此数据也在缓存,则必须同时更新缓存。

        无论是写回还是写直达,如果更新的地址被其他核心缓存,那么其他核心对此地址的缓存必须失效。写回的优点是节省了大量的写操作,内存带宽和功耗。而写直达比写回更易于实现,并且能更简单地维护数据一致性。

        无论是写回还是写直达都需要多个周期,不能完全流水线化,因此现代处理器核心通常都有一个缓冲区用于临时保护等待写回内存的数据,这称为写缓存。

        由于下一次循环需要使用前一次循环写入的数据,因此硬件必须检查写缓冲中的操作是否完成,这导致延迟增加,不能完全流水线化。

如果需要被写的数据不在缓存中,即写不命中,那么现代处理器会执行两种处理方法的一种:
        1. 写分配(write allocate):如果要写的数据没有被缓存,那么就在缓存中分配一条缓存行,这类似于大多数处理器对读的处理如果被写入的数据的局部性很好(在随后会被读),那么写分配就很适合。
        2. 写不分配(write- no-allocate):指如果要写的数据没有缓存,那么数据直接写入内存,而不占用缓存行,这有点类似于流式缓存。如果被写入的数据的局部性很差(在随后不会被再次使用),那么写不分配就很适合。

2.4.5 流式缓存

对于某些应用而言,代码只具有空间局部性而没有时间局部性,即某个数据只被访问一次,其后相邻的数据会被访问,但是其本身不会再次被访问。那么就能够将缓存留给更需要缓存的数据。实际上只需要硬件为读写分配几条长度和缓存行相同的缓冲区即可。某些现代X86处理器使用流式加载或流式存储概念。__mm_stream_load.

通常使用流式加载和流存储指令要求数据访问的步长为1。

2.4.6 硬件预取

为了利用空间局部性,同时也为了掩盖传输延迟,可以投机地在数据被用到之前就将其取入缓存。这一技术称为预取。本质上说,处理器每次加载一条缓存行即是一种预取,即预取这条缓存线上的其他数据。

预取可以通过硬件或软件控制典型的硬件预取指令会在缓存因失效而发生内存载入一个缓存线的同时,请求紧随其后的另一个缓存线。

但是如果程序的局部性不好,则硬件预取反而会降低性能。原因如下:

        1. 如果预取的的数据随后没有被访问,那么预取的数据就不被使用(完全浪费掉了,还不如不预取)。

        2. 如果预取过早的话,可能会出现冲突(预取的数据有可能会被替换出缓存)。

        3. 有可能导致满不命中,如果缓存已满,那么预取的数据需要换出缓存中数据,如果被换出的缓存数据在随后被访问,那么就增加了缓存访问次数。

X86和ARM处理器支持软件预取技术,如SSE何NEON的prefetch指令。

2.4.7 缓存结构

缓存以缓存线为基本单位读写,每条缓存线可保存L(现代机器上L一般是64个字节)个字节。

对于开发人员而言,缓存的总量和缓存线的大小相当重要,另外缓存的层次结构,缓存映射策略也需要了解。

目前内存的容量越来越大,根据机械定律,内存容量越大则其访问延迟就越长,为了弥补访问时间的损失,缓存会越来越大且层次越来越多。

1. 缓存线(缓存行)

        1. 缓存线是缓存中数据交互的最小单位,即读写数据时,每次会写一个缓存线的状态,不会存在读半个缓存线(不包括寄存器从一级缓存中读)的情况。
        2. 通常缓存线的长度是2的幂,主流CPU上缓存线长度为64B;主流GPU上,缓存线长度为128B。缓存线会映射到连续的地址,通常读取数据时,如果能够对齐到缓存线长度,可以有效减少访存的次数。
        3. 在缓存总量一定的条件下,增加缓存线长度会增加访问延迟,但是有可能减少访存次数以提高带宽。

2. 缓存组

        1. 为了减少读取多个映射到同一缓存的内存地址时造成的存储器访问“抖动”的影响(即冲突不命中),通常将多个缓存线组成一组,每个对齐到缓存线的内存地址可映射到一组中的某一条缓存线。
        2. 在读取时,具体读入组中那一条缓存线则由其替换策略决定,常见的替换策略有最近最少使用、随机粗略。通常组的大小的为8或16条缓存线。

2.4.8 映射策略

        当缓存被数据占满时,那些缓存的内容要被替换,从下一级缓存取出的数据要放入上一级缓存的数目地方等,这些行为都和映射策略相关。

        常见的映射策略是:直接相联、组相联和全相联。直接相联是指每个缓存组中只存在一条缓存线。组相联是指缓存组中的缓存线数量大于1,即一个内存地址可映射到缓存组中的多条缓存线。而全相联是指一个内存地址可映射到整个缓存。

2.5 虚拟存储器和TLB

虚拟存储器是对内存和IO设备的抽象,通过将内存中的数据切换到硬盘,它使得进程好像拥有了比整个内存容量大得多的内存空间。

在虚拟存储器的抽象下,软件开发人员无需了解存储器组织细节就可以编写高性能程序。

虚拟存储器使用虚拟地址寻址,而物理存储器使用物理地址寻址,因此硬件执行访存操作时需要将虚拟地址翻译成物理地址,这需要操作系统和存储器管理单元硬件配合来完成这一工作。由于从虚拟地址到物理地址转换非常耗时,处理器和操作系统主要使用了两个优化来减少转换次数。
        1. 分页机制:虚拟存储器和物理存储器被划分为大小相等的页,虚拟存储器和物理存储器的每次交换都是以页为单位,通常页的大小为4KB,64KB或4MB(Linux下页的默认大小即为4KB),这远大于缓存线的大小,处理器通过载入一页的方式(相当于一次转换可完成一页而不是一个数据)减少载入单元数据从虚拟地址到物理地址转换的消耗
       2. TLB(Translation lookside buffer),TLB用于缓存已经翻译的虚拟地址,通过利用页的局部性来减少翻译次数。由于虚拟地址到物理地址之间的转换非常耗时且TLB不命中的代价很大,因此TLB的映射策略通常设计为全相联。解决程序TLB不命中而导致性能问题的方法主要如下:
                        a. 增大页的大小,比如之前使用4KB,现在用64KB,甚至2MB大小的页;
                        b. 对于重复多次使用且局部性很好的多维数据,临时分配数据空间来保存数据的一部分,然后重复使用该临时数据空间,此时TLB只需要保存这一部分临时数据空间的地址即可。
                        c. 对数据进行分块优化。如果数据要多次使用,只是由于其大小超过TLB能够缓存的范围,即满不命中。此时可将数据分块,操作完一块后操作另一块。

2.6 NUMA技术

随着多路芯片中多核处理器数目的增多,内存带宽和多路芯片计算能力之间的差距越来越大。为了提高多路芯片中多核处理器之间通信的宽度,NUMA应运而生。

对Intel的处理器而言,多路处理器之间通过QPI总线通信;而AMD的处理器则通过HT总线通信。QPI和HT的带宽比内存带宽小,而延迟则大于访问内存的延迟,这是NUMA存在的根本原因。

对NUMA架构上的每个处理器来说,访问各个存储器的速度是不一样的。访问“靠近”处理器自身的存储器速度要比访问“远”的存储器快。

要利用NUMA的优点,控制流分配存储器时要保证分配的存储器离自身所在的处理器比较近,这需要保证两点:
        1. 控制流分配存储在离自己进的物理内存上。这可以通过在线程内使用malloc分配办到。但是如果控制流在运行时迁移到另一个物理核心上,这点就可能失效。这引出了下一点;

        2. 控制流不能在核心间迁移。为了使得处理器核心能够公平地得到任务,操作系统会在核心间迁移控制流,一旦出现线程迁移到其他处理器上的情况,则不满足NUMA。可以通过线程亲和性保证线程不会在核心间迁移,如gcc环境变量GOMP_CPU_AFFINITY使用(s-e:span)格式表示如何将线程固定在CPU上,其中s表示起始线程固定到的CPU编号,e表示最后一个CPU的编号,span表示相邻线程间隔的CPU编号距离。

如果主板支持多个socket,那么GPU之间,GPU和CPU之间也存在NUMA。

2.7 本章小结

现代处理器主要特性,主要但不限于如下内容:

        1. 现代处理器使用的指令级并行技术有:指令流水线、乱序执行、指令多发射、分支预测和VLIW。

        2. 现代处理器主要使用的两种向量化并行技术:SIMD和SIMT。

        3. 线程级并行基础知识:用户线程和内核线程,常见的多线程编程库。

        4. 关于现代处理器的存储系统,介绍了缓存层次结构、缓存一致性、缓存缺失的分类、缓存结构和缓存的映射策略,另外还介绍了虚拟存储器和TLB及NUMA技术。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值