3.4 预读机制

简介: <div class="bct fc05 fc11 nbw-blog ztag"><div> <p style="TEXT-INDENT: 21pt;"><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times new roman'; mso-hansi-font-family: 'Times new roman';">随着处理器

随着处理器制造工艺的提高,处理器主频越来越高,存储器和外部设备的访问速度虽然也得到极大的提升,但是依然不与处理器主频的提升速度成正比,从而处理器的运行速度和外部设备的访问速度之间的差距越来越大,存储器瓶颈问题愈发严重。虽然Cache的使用有效缓解了存储器瓶颈问题,但是仅使用Cache远远不够。

因为不管Cache的命中率有多高,总有发生CacheMiss的情况。一旦Cache行出现Miss,处理器必须启动存储器周期,将需要的数据从存储器重新填入Cache中,这在某种程度上增加了存储器访问的开销。

使用预读机制可以在一定程度上降低Cache行失效所带来的影响。处理器系统可以使用的预读机制,包括指令预读、数据预读、外部设备的预读队列和操作系统提供的预读策略。本章将简要介绍指令预读,并重点介绍CPU如何对主存储器和外部设备进行数据预读。并以此为基础,详细说明PCI总线使用的预读机制。

3.4.1 指令预读

指令预读是指在CPU执行某段程序时,根据程序的执行情况,提前将指令从主存储器预读到指令Cache中,从而当CPU需要执行这段程序时,不需要从主存储器而是从指令Cache中获取指令,从而极大缩短了CPU获取指令的延时。

在一段程序中,存在大量的分支预测指令,因而在某种程度上增加了指令预读的难度,因为有时预读到Cache中的指令,并不会被迅速执行。因此如何判断程序的执行路径是指令预读首先需要解决的问题。

CPU中通常设置了分支预测单元(Branch Predictor),在分支指令执行之前,分支预测单元需要预判分支指令的执行路径,从而进行指令预取。但是分支预测单元并不会每次都能正确判断分支指令的执行路径,这为指令预读制造了不小的麻烦,在这个背景下许多分支预测策略应运而生。

这些分支预测策略主要分为静态预测和动态预测两种方法。静态预测方法的主要实现原理是利用Profiling工具,静态分析程序的架构,并为编译器提供一些反馈信息,从而对程序的分支指令进行优化。如在PowerPC处理器的转移指令中存在一个“at”字段,该字段可以向CPU提供该转移指令是否Taken[1]的静态信息。在PowerPC处理器中,条件转移指令“bc”表示Taken;而“bc-”表示Not Taken

CPU的分支预测单元在分析转移指令时可以预先得知该指令的转移结果。目前在多数CPU中提供了动态预测机制,而且动态预测的结果较为准确。因此在实现中,许多PowerPC内核并不支持静态预测机制,如E500内核并不支持这种机制。

CPU使用的动态预测机制是本节研究的重点。而在不同的处理器中,分支预测单元使用的动态预测算法并不相同。在一些功能较弱的处理器,如8b/16b微控制器中,分支指令的动态预测机制较为简单。在这些处理器中,分支预测单元常使用以下几种方法动态预测分支指令的执行。

(1)      分支预测单元每一次都将转移指令预测为Taken,采用这种方法无疑非常简单,而且命中率在50%以上,因为无条件转移指令都是Taken,当然使用这种方法的缺点也是显而易见的。

(2)      分支预测单元将向上跳转的指令预测为Taken,而将向下跳转的指令预测为Not Taken。分支预测单元使用的这种预测方式与多数程序的执行风格类似,但是这种实现方式并不理想。

(3)      一条转移指令被预测为Taken,而之后这条转移指令的预测值与上一次转移指令的执行结果相同。

当采用以上几种方法时,分支预测单元的硬件实现代价较低,但是使用这些算法时,预测成功的概率较低。因此在高性能处理器中,如PowerPCx86处理器并不会采用以上这3种方法实现分支预测单元。

目前在高性能处理器中,常使用BTB(Branch Target Buffer) 管理分支预测指令。在BTB中含有多个Entry,这些Entry由转移指令的地址低位进行索引,而这个EntryTag字段存放转移指令的地址高位。BTB的功能相当于存放转移指令的Cache,其状态机转换也与Cache类似。当分支预测单元第一次分析一条分支指令时,将在BTB中为该指令分配一个Entry,同时也可能会淘汰BTB中的Entry。目前多数处理器使用LRU (Least recently used)算法淘汰BTB中的Entry

BTB的每个Entry中存在一个Saturating Counter。该计数器也被称为Bimodal Predictor,由两位组成,可以表示4种状态,为0b11时为“Strongly Taken” 0b10时为“Weakly Taken”;为0b01时为“Weakly not Taken”;为0b00时为“Strongly not Taken”。

CPU第一次预测一条转移指令的执行时,其结果为Strongly Taken,此时CPU将在BTB中为该指令申请一个Entry,并置该EntrySaturating Counter0b11。此后该指令将按照Saturating Counter的值,预测执行,如果预测结果与实际执行结果不同时,将Saturating Counter的值减1,直到其值为0b00;如果相同时,将Saturating Counter的值加1,直到其值为0b11。目前Power E500内核和Pentium处理器使用这种算法进行分支预测。

使用Saturating Counter方法在处理转移指令的执行结果为1111011111或者0000001000时的效果较好,即执行结果大多数为0或者1时的预测结果较好。然而如果一条转移指令的执行结果具有某种规律,如为010101010101或者001001001001时,使用Saturating Counter并不会取得理想的预测效果。

在程序的执行过程中,一条转移指令在执行过程中出现这样有规律的结果较为常见,因为程序就是按照某种规则书写的,按照某种规则完成指定的任务。为此Two-Level分支预测方法应运而生。

Two-Level分支预测方法使用了两种数据结构,一种是BHR(Branch History Register);而另一种是PHT(Pattern History Table)。其中BHRk位组成,用来记录每一条转移指令的历史状态,而PHT表含有2kEntry组成,而每一个Entry由两位Saturating Counter组成。BHRPHT的关系如3?10所示。

3.4 预读机制 - maoxingbing - 毛毛虫的爹

假设分支预测单元在使用Two-Level分支预测方法时,设置了一个PBHT(Per-address Branch History Table)存放不同指令所对应的BHR。在PBHT表中所有BHR的初始值为全1,而在PHT表中所有Entry的初始值值为0b11BHRPBHT表中的使用方法与替换机制与Cache类似。

当分支预测单元分析预测转移指令B的执行时,将首先从PBHT中获得与转移指令B对应的BHR,此时BHR为全1,因此CPU将从PHT的第11…11Entry中获得预测结果0b11,即Strongly Taken。转移指令B执行完毕后,将实际执行结果Rc更新到BHR寄存器中,并同时更新PHT中对应的Entry

CPU再次预测转移指令B的执行时,仍将根据BHR索引PHT表,并从对应Entry中获得预测结果。而当指令B再次执行完毕后,将继续更新BHRPHT表中对应的Entry。当转移指令的执行结果具有某种规律(Pattern)时,使用这种方法可以有效提高预测精度。如果转移指令B的实际执行结果为001001001….001,而且k等于4时,CPU将以0010-0100-1001这样的循环访问BHR,因此CPU将分别从PHT表中的第001001001001Entry中获得准确的预测结果。

由以上描述可以发现,Two-Level分支预测法具有学习功能,并可以根据转移指令的历史记录产生的模式,在PHT表中查找预测结果。该算法由T.Y. Yeh and Y.N. Patt1991年提出,并在高性能处理器中得到了大规模应用。

Two-Level分支预测法具有许多变种。目前x86处理器主要使用“Local Branch Prediction”和“Global Branch Prediction”两种算法。

在“Local Branch Prediction”算法中,每一个BHR使用不同的PHT表,Pentium IIPentium III处理器使用这种算法。该算法的主要问题是当PBHT表的Entry数目增加时,PHT表将以指数速度增长,而且不能利用其它转移指令的历史信息进行分支预测。而在“Global Branch Prediction”算法中,所有BHR共享PHT表,Pentium MPentium CoreCore 2处理器使用这种算法。

在高性能处理器中,分支预测单元对一些特殊的分支指令如“Loop”和“Indirect跳转指令”设置了“Loop Prediction”和“Indirect Prediction”部件优化这两种分支指令的预测。此外分支预测单元,还设置了RSB(Return Stack Buffer),当CPU调用一个函数时,RSB将记录该函数的返回地址,当函数返回时,将从RSB中获得返回地址,而不必从堆栈中获得返回地址,从而提高了函数返回的效率。

目前在高性能处理器中,动态分支预测的主要实现机制是CPU通过学习以往历史信息,并进行预测,因而Neural branch predictors机制被引入,并取得了较为理想的效果,本节对这种分支预测技术不做进一步说明。目前指令的动态分支预测技术较为成熟,在高性能计算机中,分支预测的成功概率在95%~98%之间,而且很难进一步提高。

3.4.2 数据预读

数据预读是指在处理器进行运算时,提前通知存储器系统将运算过程中需要的数据准备好,而当处理器需要这些数据时,可以直接从这些预读缓冲(通常指Cache)中获得这些数据。[Steven P. Vanderwiel and David J. Lilja] 总结了最近出现的各类数据预读机制,下文将以3?11为例进一步探讨这些数据预读机制。

3.4 预读机制 - maoxingbing - 毛毛虫的爹

3?11列举了三个实例说明数据预读的作用。其中实例a没有使用预读机制;实例b是一个采用预读机制的理想情况;而实例c是一个采用预读机制的次理想情况。我们假设处理器执行某个任务需要经历四个阶段,每个阶段都由处理器执行运算指令和存储指令组成。而处理器一次存储器访问需要5个时钟周期。其中每一个阶段的定义如下所示。

(1)      处理器执行4个时钟周期后需要访问存储器。

(2)      处理器执行6个时钟周期后需要访问存储器。

(3)      处理器执行8个时钟周期后需要访问存储器。

(4)      处理器执行4个时钟周期后完成。

实例a由于没有使用预读机制,因此在运算过程中需要使用存储器中的数据时,不可避免的出现Cache Miss。实例a执行上述任务的过程如下。

(1)      执行第一阶段任务的4个时钟周期,之后访问存储器,此时将发生Cache Miss

(2)      Cache Miss需要使用一个时钟周期[2],然后在第5个时钟周期启动存储器读操作。

(3)      在第10个周期,处理器从存储器获得数据,继续执行第二阶段任务的6个时钟周期,之后访问存储器,此时也将发生Cache Miss

(4)      处理器在第17~22时钟周期从存储器读取数据,并在第22个时钟周期,继续执行第三阶段任务的8个时钟周期,之后访问存储器,此时也将发生Cache Miss

(5)      处理器在第31~36时钟周期从存储器读取数据,并在第36个时钟周期,继续执行第四阶段任务的4个时钟周期,完成整个任务的执行。

采用这种机制执行上述任务共需40个时钟周期。而使用预读机制,可以有效缩短整个执行过程,如3?11中的实例b所示。在实例b中在执行过程中,都会提前进行预读操作,虽然这些预读操作也会占用一个时钟周期,但是这些预读操作是值得的。合理使用这些数据预读,完成同样的任务CPU仅需要28个时钟周期,从而极大提高了程序的执行效率,其执行过程如下。

(1)      首先使用预读指令对即将使用的存储器进行预读[3],然后执行第一阶段任务的4个时钟周期。当处理器进行存储器读时,数据已经准备好,处理器将在Cache中获得这个数据然后继续执行[4]

(2)      处理器在执行第二阶段的任务时,先执行2个时钟周期之后进行预读操作,最后执行剩余的4个时钟周期。当处理器进行存储器读时,数据已经准备好,处理器将在Cache中获得这个数据然后继续执行。

(3)      处理器执行第三阶段的任务时,先执行4个时钟周期之后进行预读操作,最后执行剩余的4个时钟周期。当处理器进行存储器读时,数据已经准备好,处理器将在Cache中获得这个数据然后继续执行。

(4)      处理器执行第四阶段的任务,执行完4个时钟周期后,完成整个任务的执行。

当然这种情况是非常理想的,处理器在执行整个任务时,从始至终是连贯的,处理器执行和存储器访存完全并行,然而这种理想情况并不多见。

首先在一个任务的执行过程中,并不易确定最佳的预读时机;其次采用预读所获得数据并不一定能够被及时利用,因为在程序执行过程中可能会出现各种各样的分支选择,有时预读的数据并没有被及时使用。

3?11所示的实例c中,预读机制没有完全发挥作用,所以处理器在执行任务时,Cache Miss时有发生,从而减低了整个任务的执行效率。即便这样,实例c也比完全没有使用预读的实例a的任务执行效率还是要高一些。在实例c中,执行完毕3?11中所示的任务共需要34个时钟周期。

但是在某些特殊情况下,采用预读机制有可能会降低效率。首先在一个较为复杂的应用中,很有可能预读的数据没有被充分利用,一个程序可能会按照不同的分支执行,而执行每一个分支所使用的数据并不相同。其次预读的数据即使是有效的,这些预读的数据也会污染整个Cache资源,在大规模并行任务的执行过程中,有可能引发Cache颠簸,从而极大地降低系统效率。

什么时候采用预读机制,关系到处理器系统结构的每一个环节,需要结合软硬件资源统筹考虑,并不能一概而论。处理器提供了必备的软件和硬件资源用以实现预读,而如何“合理”使用预读机制是系统程序员考虑的一个细节问题。数据预读可以使用软件预读或者硬件预读两种方式实现,下文将详细介绍这两种实现方式。

3.4.3 软件预读

软件预读机制由来已久,首先实现预读指令的处理器是Motorola88110处理器,这颗处理器首先实现了“touch load”指令,这条指令是PowerPC处理器dcbt指令[5]的雏形。88110处理器是Motorola第一颗RISC处理器,具有里程碑意义,这颗处理器从内核到外部总线的设计都具有许多亮点。这颗处理器是MotorolaPowerPC架构做出的巨大贡献,PowerPC架构中著名的60X总线也源于88110处理器。

后来绝大多数处理器都采用这类指令进行软件预读,Inteli486处理器中提出了Dummy Read指令,这条指令也是后来x86处理器中PREFETCHh指令[6]的雏形。

这些软件预读指令都有一个共同的特点,就是在处理器真正需要数据之前,向存储器发出预读请求,这个预读请求[7]不需要等待数据真正到达存储器之后,就可以执行完毕。从而处理器可以继续执行其他指令,以实现存储器访问与处理器运算同步进行,从而提高了程序的整体执行效率。由此可见,处理器采用软件预读可以有效提高程序的执行效率。我们考虑源代码3?1所示的实例。

 

源代码3?1没有采用软件预读机制的程序

int ip, a[N], b[N];

 

for (i = 0; i < N; i++)

    ip = ip + a[i]*b[i];

 

这个例子在对数组进行操作时被经常使用,这段源代码的作用是将int类型的数组a和数组b的每一项进行相乘,然后赋值给ip,其中数组ab都是Cache行对界的。源代码3?1中的程序并没有使用预读机制进行优化,因此这段程序在执行时会因为a[i]b[i]中的数据不在处理器的Cache中,而必须启动存储器读操作。因此在这段程序在执行过程中,必须要等待存储器中的数据后才能继续,从而降低了程序的执行效率。为此我们将程序进行改动,如源代码3?2所示。

源代码3?2 采用软件预读机制的程序

int ip, a[N], b[N];

for (i = 0; i < N; i++) {

    fetch(&a[i+1]);

    fetch(&b[i+1]);

    ip = ip + a[i]*b[i];

}

 

以上程序对变量ip赋值之前,首先预读数组ab,当对变量ip赋值时,数组ab中的数据已经在Cache中,因而不需要进行再次进行存储器操作,从而在一定程度上提高了代码的执行效率。以上代码仍然并不完美,首先ipa[0]b[0]并没有被预读,其次在一个处理器,预读是以Cache行为单位进行的,因此对a[0]a[1]进行预读时都是对同一个Cache行进行预读[8],从而这段代码对同一个Cache行进行了多次预读,从而影响了执行效率。为此我们将程序再次进行改动,如源代码3?3所示。

 

源代码3?3软件预读机制的改进程序

int ip, a[N], b[N];

fetch(&ip);

fetch(&a[0]);

fetch(&b[0]);

for (i = 0; i < N-4; i+=4) {

    fetch(&a[i+4]);

    fetch(&b[i+4]);

    ip = ip + a[i]*b[i];

    ip = ip + a[i+1]*b[i+1];

    ip = ip + a[i+2]*b[i+2];

    ip = ip + a[i+3]*b[i+3];

}

for (; i < N; i++)

    ip = ip + a[i]*b[i];

 

对于以上这个例子,采用这种预读方法可以有效提高执行效率,对此有兴趣的读者可以对以上几个程序进行简单的对比测试。但是提醒读者注意,有些较为先进的编译器,可以自动的加入这些预读语句,程序员可以不手工加入这些预读指令。实际上源代码3?3中的程序还可以进一步优化。这段程序的最终优化如源代码3?4所示。

 

源代码3?4软件预读机制的改进程序

int ip, a[N], b[N];

fetch( &ip);

for (i = 0; i < 12; i += 4){

    fetch( &a[i]);

    fetch( &b[i]);

}

for (i = 0; i < N-12; i += 4){

    fetch( &a[i+12]);

    fetch( &b[i+12]);

    ip = ip + a[i] *b[i];

    ip = ip + a[i+1]*b[i+1];

    ip = ip + a[i+2]*b[i+2];

    ip = ip + a[i+3]*b[i+3];

}

for ( ; i < N; i++)

    ip = ip + a[i]*b[i];

 

因为我们还可以对ip、数据ab进行充分预读之后;再一边预读数据,一边计算ip的值;最后计算ip的最终结果。使用这种方法可以使数据预读和计算充分并行,从而优化了整个任务的执行时间。

由以上程序可以发现,采用软件预读机制可以有效地对矩阵运算进行优化,因为矩阵运算进行数据访问时非常有规律,便于程序员或编译器进行优化,但是并不是所有程序都能如此方便地使用软件预读机制。此外预读指令本身也需要占用一个机器周期,在某些情况下,采用硬件预读机制更为合理。

3.4.4 硬件预读

采用硬件预读的优点是不需要软件进行干预,也不需要浪费一条预读指令来进行预读。但硬件预读的缺点是预读结果有时并不准确,有时预读的数据并不是程序执行所需要的。在许多处理器中这种硬件预读通常与指令预读协调工作。硬件预读机制的历史比软件预读更为久远,在IBM 370/168处理器系统中就已经支持硬件预读机制。

大多数硬件预读仅支持存储器到Cache的预读,并在程序执行过程中,利用数据的局部性原理进行硬件预读。其中最为简单的硬件预读机制是OBL(One Block Lookahead)机制,采用这种机制,当程序对数据块b进行读取出现Cache Miss时,将数据块b从存储器更新到Cache中,同时对数据块b+1也进行预读并将其放入Cache中;如果数据块b+1已经在Cache中,将不进行预读。

这种OBL机制有很多问题,一个程序可能只使用数据块b中的数据,而不使用数据块b+1中的数据,在这种情况下,采用OBL预读机制没有任何意义。而且使用这种预读机制时,每次预读都可能伴随着Cache Miss,这将极大地影响效率。有时预读的数据块b+1会将Cache中可能有用的数据替换出去,从而造成Cache污染。有时仅预读数据块b+1可能并不足够,有可能程序下一个使用的数据块来自数据块b+2

为了解决OBL机制存在的问题,有许多新的预读方法涌现出来,如“tagged预读机制”。采用这种机制,将设置一个“tag位”,处理器访问数据块b时,如果数据块b没有在Cache中命中,则将数据块b从存储器更新到Cache中,同时对数据块b+1进行预读并将其放入Cache中;如果数据块b已经在Cache中,但是这个数据块b首次被处理器使用,此时也将数据块b+1预读到Cache中;如果数据块b已经在Cache中,但是这个数据块b已经被处理器使用过,此时不将数据块b+1预读到Cache中。

这种“tagged预读机制”还有许多衍生机制,比如可以将数据块b+1b+2都预读到Cache中,还可以根据程序的执行信息,将数据块b-1b-2预读到Cache中。

但是这些方法都无法避免因为预读而造成的Cache污染问题,于是Stream buffer机制被引入。采用该机制,处理器可以将预读的数据块放入Stream Buffer中,如果处理器使用的数据没有在Cache中,则首先在Stream Buffer中查找,采用这种方法可以消除预读对Cache的污染,但是增加了系统设计的复杂性。

与软件预读机制相比,硬件预读机制可以根据程序执行的实际情况进行预读操作,是一种动态预读方法;而软件预读机制需要对程序进行静态分析,并由编译器自动或者由程序员手工加入软件预读指令来实现。

3.4.5 PCI总线的预读机制

在一个处理器系统中,预读的目标设备并不仅限于存储器,程序员还可以根据实际需要对外部设备进行预读。但并不是所有的外部设备都支持预读,只有“well-behavior”存储器支持预读。处理器使用的内部存储器,如基于SDRAMDDR-SDRAM或者SRAM的主存储器是“well-behavior”存储器,有些外部设备也是“well-behavior”存储器。这些well-behavior存储器具有以下特点。

(1)      对这些存储器设备进行读操作时不会改变存储器的内容。显然主存储器具有这种性质。如果一个主存储器的一个数据为0,那么读取这个数据100次也不会将这个结果变为1。但是在外部设备中,一些使用存储器映像寻址的寄存器具有读清除的功能。比如某些中断状态寄存器[9]。当设备含有未处理的中断请求时,该寄存器的中断状态位为1,对此寄存器进行读操作时,硬件将自动地把该中断位清零,这类采用存储映像寻址的寄存器就不是well-behavior存储器。

(2)      对“well-behavior”存储器的多次读操作,可以合并为一次读操作。如向这个设备的地址nn+4n+8n+12地址处进行四个双字的读操作,可以合并为对n地址的一次突发读操作(大小为4个双字)

(3)      对“well-behavior”存储器的多次写操作,可以合并为一次写操作。如向这个设备的地址nn+4n+8n+12地址处进行四个双字的写操作,可以合并为对n地址的一次突发写操作。对于主存储器,进行这种操作不会产生副作用,但是对于有些外部设备,不能进行这种操作。

(4)      对“well-behavior”的存储器写操作,可以合并为一次写操作。向这个设备的地址nn+1n+2n+3地址处进行四个单字的写操作,可以合并为对n地址的一次DW写操作。对主存储器进行这种操作不会出现错误,但是对于有些外部设备,不能进行这种操作。

如果外部设备满足以上四个条件,该外部设备被称为“well-behavior”。PCI配置空间的BAR寄存器中有一个“Prefectchable”位,该位为1时表示这个BAR寄存器所对应的存储器空间支持预读。PCI总线的预读机制需要HOST主桥、PCI桥和PCI设备的共同参与。在PCI总线中,预读机制需要分两种情况进行讨论,一个是HOST处理器通过HOST主桥和PCI桥访问最终的PCI设备;另一个是PCI设备使用DMA机制访问存储器。

PCI总线预读机制的拓扑结构如3?12所示。

3.4 预读机制 - maoxingbing - 毛毛虫的爹

由上图所示,HOST处理器预读PCI设备时,需要经过HOST主桥,并可能通过多级PCI桥,最终到达PCI设备,在这个数据传送路径上,有的PCI桥支持预读,有的不支持预读。而PCI设备对主存储器进行预读时也将经过多级PCI桥。PCI设备除了可以对主存储器进行预读之外,还可以预读其他PCI设备,但是这种情况在实际应用中极少出现,本节仅介绍PCI设备预读主存储器这种情况。

1 HOST处理器预读PCI设备

PCI设备的BAR寄存器可以设置预读位,首先支持预读的BAR寄存器空间必须是一个Well-behavior的存储器空间,其次PCI设备必须能够接收来自PCI桥和HOST主桥的MRM(Memory Read Multiple)MRL(Memory Read Line)总线事务。

如果PCI设备支持预读,那么当处理器对这个PCI设备进行读操作时,可以通过PCI桥启动预读机制(PCI桥也需要支持预读),使用MRMMRL总线事务,对PCI设备进行预读,并将预读的数据暂时存放在PCI桥的预读缓冲中。

之后当PCI主设备继续读取PCI设备的BAR空间时,如果访问的数据在PCI桥的预读缓冲中,PCI桥可以不对PCI设备发起存储器读总线事务,而是直接从预读缓冲中获取数据,并将其传递给PCI主设备。当PCI主设备完成读总线事务后,PCI桥必须丢弃预读的数据以保证数据的完整性。此外当PCI桥预读的地址空间超越了PCI设备可预读BAR空间边界时,PCI设备需要“disconnect”该总线事务。

如果PCI桥支持“可预读”的存储器空间,而且其下挂接的PCI设备BAR空间也支持预读时,系统软件需要从PCI桥“可预读”的存储器空间中为该PCI设备分配空间。此时PCI桥可以将从PCI设备预读的数据暂存在PCI桥的预读缓冲中。

PCI总线规定,如果下游PCI桥地址空间支持预读,则其上游PCI桥地址空间可以支持也可以不支持预读机制。如3?12所示,如果PCIB管理的PCI子树使用了可预读空间时,PCIA可以不支持可预读空间,此时PCIA只能使用存储器读总线事务读取PCI设备,而PCIB可以将这个存储器读总线事务转换为MRL或者MRM总线事务,预读PCI设备的BAR空间(如果PCI设备的BAR空间支持预读),并将预读的数据保存在PCIB的数据缓冲中。

但是PCI总线不允许PCIA从其“可预读”的地址空间中,为PCIB的“不可预读”区域预留空间,因为这种情况将影响数据的完整性。

大多数HOST主桥并不支持对PCI设备的预读,这些HOST主桥并不能向PCI设备发出MRL或者MRM总线事务。由于在许多处理器系统中,PCI设备是直接挂接到HOST主桥上的,如果连HOST主桥也不支持这种预读,即便PCI设备支持了预读机制也没有实际作用。而且如果PCI设备支持预读机制,硬件上需要增加额外的开销,这也是多数PCI设备不支持预读机制的原因。

尽管如此本节仍需要对HOST处理器预读PCI设备进行探讨。假设在3?12所示的处理器系统中,HOST主桥和PCIA不支持预读,而PCIB支持预读,而且处理器的Cache行长度为32B(0x20)

如果HOST处理器对PCI设备的0x8000-0000~0x8000-0003这段地址空间进行读操作时。HOST主桥将使用存储器读总线事务读取PCI设备的“0x8000-0000~0x8000-0003这段地址空间”,这个存储器读请求首先到达PCIA,并由PCIA转发给PCIB

PCIB发现“0x8000-0000~0x8000-0003这段地址空间”属于自己的可预读存储器区域,即该地址区域在该桥的Prefetchable Memory Base定义的范围内,则将该存储器读请求转换为MRL总线事务,并使用该总线事务从PCI设备[10]中读取0x8000-0000~0x8000-001F这段数据,并将该数据存放到PCIB的预读缓冲中。MRL总线事务将从需要访问的PCI设备的起始地址开始,一直读到当前Cache行边界。

之后当HOST处理器读取0x8000-0004~0x8000-001F这段PCI总线地址空间的数据时,将从PCIB的预读缓冲中直接获取数据,而不必对PCI设备进行读取。

2 PCI设备读取存储器

PCI设备预读存储器地址空间时,需要使用MRL或者MRM总线事务。与MRL总线周期不同,MRM总线事务将从需要访问的存储器起始地址开始,一直读到下一个Cache行边界为止。

对于一个Cache行长度为32B(0x20)的处理器系统,如果一个PCI设备对主存储器的0x1000-0000~0x1000-0007这段存储器地址空间进行读操作时,由于这段空间没有跨越Cache行边界,此时PCI设备将使用MRL总线事务对0x1000-0000~0x1000-001F这段地址区域发起存储器读请求。

如果一个PCI设备对主存储器的0x1000-001C~0x1000-0024这段存储器地址空间进行读操作时,由于这段空间跨越了Cache行边界,此时PCI设备将使用MRM总线事务对0x1000-001C~0x1000-002F这段地址空间发起存储器读请求。

3?12所示的例子中,PCI设备读取0x1000-001C~0x1000-0024这段存储器地址空间时,首先将使用MRM总线事务发起读请求,该请求将通过PCIBA最终到达HOST主桥。HOST主桥[11]将从主存储器中读取0x1000-001C~0x1000-002F这段地址空间的数据。如果PCIA也支持下游总线到上游总线的预读,这段数据将传递给PCIA;如果PCIAB都支持这种预读,这段数据将到达PCIB的预读缓冲。

如果PCIAB都不支持预读,0x1000-0024~0x1000-002F这段数据将缓存在HOST主桥中,HOST主桥仅将0x1000-001C~0x1000-0024这段数据通过PCIAB传递给PCI设备。之后当PCI设备需要读取0x1000-0024~0x1000-002F这段数据时,该设备将根据不同情况,从HOST主桥、PCIA或者B中获取数据而不必读取主存储器。值得注意的是,PCI设备在完成一次数据传送后,暂存在HOST主桥中的预读数据将被清除。PCI设备采用这种预读方式,可以极大提高访问主存储器的效率。

PCI总线规范有一个缺陷,就是目标设备并不知道源设备究竟需要读取或者写入多少个数据。例如PCI设备使用DMA读方式从存储器中读取4KB大小的数据时,只能通过PCI突发读方式,一次读取一个或者多个Cache行。

假定PCI总线一次突发读写只能读取32B大小的数据,此时PCI设备读取4KB大小的数据,需要使用128次突发周期才能完成全部数据传送。而HOST主桥只能一个一个的处理这些突发传送,从而存储器控制器并不能准确预知何时PCI设备将停止读取数据。在这种情况下,合理地使用预读机制可以有效地提高PCI总线的数据传送效率。

我们首先假定PCI设备一次只能读取一个Cache行大小的数据,然后释放总线,之后再读取一个Cache行大小的数据。如果使用预读机制,虽然PCI设备在一个总线周期内只能获得一个Cache行大小的数据,但是HOST主桥仍然可以从存储器获得2Cache行以上的数据,并将这个数据暂存在HOST主桥的缓冲中,之后PCI设备再发起突发周期时,HOST主桥可以不从存储器,而是从缓冲中直接将数据传递给PCI设备,从而降低了PCI设备对存储器访问的次数,提高了整个处理器系统的效率。

以上描述仅是实现PCI总线预读的一个例子,而且仅仅是理论上的探讨。实际上绝大多数半导体厂商都没有公开HOST主桥预读存储器系统的细节,在多数处理器中,HOST主桥以Cache行为单位读取主存储器的内容,而且为了支持PCI设备的预读功能HOST主桥需要设置必要的缓冲部件,这些缓冲的管理策略较为复杂。

目前PCI总线已经逐渐退出历史舞台,进一步深入研究PCI桥和HOST主桥,意义并不重大,不过读者依然可以通过学习PCI体系结构,获得处理器系统中有关外部设备的必要知识,并以此为基础,学习PCIe体系结构。

3.5 小结

本章重点介绍了PCI总线的数据交换。其中最重要的内容是与Cache相关的PCI总线事务和预读机制。虽然与Cache相关的PCI总线事务并不多见,但是理解这些内容对于理解PCI和处理器体系结构,非常重要。



[1] 为简便起见,下文将转移指令成功进行转移称为“Taken”;而将不进行转移称为“Not Taken”。

[2] 假定从访问Cache到发现Cache Miss需要一个时钟周期。

[3] PowerPC处理器使用dcbt指令,而x86处理器使用PREFETCHh指令,实现这种软件预读。

[4] 假定从Cache中获得数据需要一个时钟周期。

[5] dcbt指令是PowerPC处理器的一条存储器预读指令,该指令可以将内存中的数据预读到L1或者L2 Cache中。

[6] PREFETCHh指令是x86处理器的一条存储器预读指令。

[7] 预读指令在一个时钟周期内就可以执行完毕。

[8] 假定这个处理器系统的Cache行长度为4个双字,即128位。

[9] 假设中断状态寄存器支持读清除功能。

[10] 此时PCI设备的这段区域一定是可预读的存储器区域。

[11] 假设HOST主桥读取存储器时支持预读,多数HOST主桥都支持这种预读。

相关文章
|
4月前
|
编译器 调度 C++
协程问题之机制保障中提到的早值班机制和稳定性周会机制分别是什么
协程问题之机制保障中提到的早值班机制和稳定性周会机制分别是什么
|
1月前
|
消息中间件
确认机制(Acknowledgements)
确认机制(Acknowledgements)
|
3月前
|
Kubernetes 安全 调度
在k8S中, PodSecurityPolicy机制是什么?
在k8S中, PodSecurityPolicy机制是什么?
|
6月前
|
算法 Linux 调度
Linux进程调度机制
Linux进程调度机制
111 0
|
存储 缓存 Kubernetes
Titus 网关中的缓存一致性机制
Titus 网关中的缓存一致性机制
209 0
Titus 网关中的缓存一致性机制
|
存储 缓存 安全
C#的并发机制优秀在哪?
笔者上次用C#写.Net代码差不多还是10多年以前,由于当时Java已经颇具王者风范,Net几乎被打得溃不成军。因此当时笔者对于这个.Net的项目态度比较敷衍了事,没有对其中一些优秀机制有很深的了解,在去年写《C和Java没那么香了,高并发时代谁能称王》时都没给.Net以一席之地,不过最近恰好机缘巧合,我又接手了一个Windows方面的项目,这也让我有机会重新审视一下自己关于.Net框架的相关知识。 项目原型要实现的功能并不复杂,主要就是记录移动存储设备中文件拷出的记录,而且需要尽可能少的占用系统资源,而在开发过程中的一个现象令我颇我惊异,在使用Invoke方法记录文件拷出情况时,程序执行效率
C#的并发机制优秀在哪?
|
Java Spring
如何在业务逻辑当中优雅引入重试机制
如何在业务逻辑当中优雅引入重试机制
如何在业务逻辑当中优雅引入重试机制
|
存储 移动开发 算法
也谈Android签名机制
1. 前言 关于Android的签名机制,在一个月前就看过了,当时还写了下流程,感觉没有太大的技术含量就没有记录。最近在看APK安装过程,突然又想起安装过程包含了APK的验证,关于APK的验证无非就是签名的逆过程。
1442 0
|
前端开发 JavaScript .NET