赞
踩
计算机先驱准确预测到程序员会希望拥有无限数量的快速存储器。满足这一愿望的一种经济型解决方法是存储器层次结构。基于局部性原理下和“在给定实现工艺和功耗预算下,硬件越小,速度越快”的指导原则,产生了存储器层次结构,这些层次由速度和容量各不相同的存储组成。
因为快速存储器非常昂贵,所以存储器层次结构被分为几个级别--离处理器越近,容量越小,速度越快,每字节的成本越高。存储器的终极目标是提供一种存储器系统,每字节的成本几乎与最便宜的存储器级别相同,而速度几乎与最快的存储器级别相同。在大多数情况下,低层级存储器中的数据是上一级存储器中数据的超集,这一性质被称为包含性质,则层次结构的最低级别必须具有包含性质。
尽管处理器请求速率和存储器访问速率的差距多年来显著增大,但由于单处理器的性能没有大幅提升,导致处理器与DRAM之间的差距的增速放缓。
因为高端处理器有多个核,所以带宽需求大于单核处理器。intel core i7 6700有4个核,每个核每周期会产生2次访问存储器的请求,时钟频率为4.2GHz。每秒最多生成328(4.2*2*4)亿次64位数据访存,总峰值带宽为409.6GB/s,这一难以置信的高带宽是通过以下方式实现的:缓存的多端口和流水线;利用三级缓存。
传统上,存储器层次结构的设计人员把重点放在优化存储器的平均访问时间上,这一时间是由缓存命中时间、缺失率和缺失代价决定的。但最近功耗已经成为设计人员的主要考虑事项,包括不执行操作时候的漏电功耗(静态功耗)和执行读写时候的有效功耗(动态功耗),在PMD的处理器中,缓存功耗可能占到总功耗的25%~50%。
存储器层次结构基础:快速回顾
若缓存缺失,出于效率原因,会一次从下一级存储结构提取多个字,这称为块(行)。这样做还有另外一个原因:局部性原理。每个缓存块都包含一个标签tag,用来指明它与那个存储器地址相对应。
在设计时需要考虑一个非常重要的决策:那些块可以放在缓存中。最常见的是组相联,其中组是指缓存中的一组块。一个块首先被映射到一个组上,然后将这个块放在这个组中的任意位置。要查找一个块,首先将这个块的地址映射到这个组,然后在搜索这个组(通常是并行搜索)。这个组是根据数据地址选择的:
块地址 MOD(缓存中的组数)
如果组中有n个块,则缓存的布局称为n路组相连。组相连的端点有自己的名字。直接映射缓存的每组中只有一个块(所以块总是放在同一个位置),全相连缓存只有一个组(所有块可以放在任何地方)。
向缓存中写入数据难一些,比如缓存副本和存储器怎样才能保持一致?主要有2种策略。一种是写直达缓存,当它更新缓存中的条目时(若存在),会同时将数据写入主存储器中,并对其进行更新。另一种是写回缓存,仅更新缓存中的副本。在要替换这个块时,再将它复制回存储器。这两种写入策略都可以使用写缓冲区。
衡量不同缓存组织方式优劣的一个指标是缺失率。缺失率是指那些未能找到预期目标的缓存访问所占的比例,即未找到目标的访问数目除以总访问数目。
为了深入理解造成高缺失率的原因,3C模型将所有缺失情景分为以下3种情况:
(1)强制缺失:对数据块的第一次访问肯定不会在缓存中,所以必须将这个块放入缓存中。即使拥有无限大的缓存,也会造成强制缺失。
(2)容量缺失:如果缓存不能包含程序运行期间所需要的全部块,就会因为有些块先被丢弃之后然后再被调入而导致容量缺失(除了强制缺失之外)。
(3)冲突缺失:如果块放置策略不是全相连的,并且多个块映射到一个块的组中,对不同块的访问混杂在一起,那么一个块可能会被丢弃,之后再被调入,从而发生冲突缺失(除强制缺失和容量缺失之外)。
多线程和多核增加了缓存的复杂性,即增大了发生容量缺失的可能性,又因为缓存刷新而增加了第4种C缺失--一致性缺失。
然而,缺失率可能因为多个原因而产生误导。因此,一些设计人员喜欢测量每条指令的缺失次数,而不是每次存储器访问的缺失次数。(通常采用整数,而不是分数,比如每千条指令的缺失数)
缺失次数/指令数 = 缺失率*存储器访问次数/指令数
以上指标的问题在于,它们都没有考虑缺失代价。存储器平均访问时间:
存储器平均访问时间 = 命中时间+缺失率*缺失代价
命中时间是指在缓存中命中目标的时间,缺失代价是将块从存储器读取到缓存所需要的时间(即缓存缺失的开销)。存储器平均访问时间仍然是一个间接的性能测量指标,尽管它比缺失率好一些,但不能替换执行时间。支持推测执行的处理器可以在缺失期间执行其他指令,从而降低实际缺失代价。使用多线程也允许处理器容忍一些缺失,而不会被强制转入空闲状态。
附录B中缓存优化方法:
(1)增大缓存块以降低缺失率。降低缺失率的最简单办法是利用空间局部性,并增大块的大小。使用较大的块可以减少强制缺失,但也增加了缺失代价。因为较大的块意味着较少的标签,所以略微降低静态功耗。较大的块还会增加容量缺失或冲突缺失,特别是当缓存块的容量较小时。
(2)增大缓存以降低缺失率。要减少容量缺失,一种显而易见的方法是增大缓存容量。缺点包括延长缓存命中时间,增加成本和静态/动态功耗。
(3)提高相连度以降低缺失率。提高相连度可以减少冲突缺失。较大的相连度是以延长命中时间和增大功耗为代价的。
(4)采用多级缓存以降低缺失代价。是加快缓存命中速度,跟上处理器的高速时钟频率,还是加大缓存,以缩小处理器访问和主存储器访问之间的差距?第一级缓存可以小到足以匹配快速的时钟周期,而第二级或第三级缓存大到足以捕获许多本来要对主存储器的访问。为了着重减少第二级缓存缺失,其采用了更大的块、更大的容量和更高的相连度。与单个总缓存相比,多级缓存更节能。对于二级缓存而言,平均存储器访问时间重新定义为:
L1命中时间+L1缺失率*(L2命中时间+L2缺失率*L2缺失代价)
(5)为读缺失指定高于写操作的优先级,以降低缺失代价。写缓冲区是实现这一优化的理想之选。因为读缺失请求有可能命中写缓冲区正在写入最新值的位置,会产生存储器上的写后读风险。一种解决方法是在读缺失时先检查写缓冲区的内容。如果没有冲突,且存储器可用,则在写操作之前发送读取请求会降低缺失代价。
(6)在索引缓存期间避免虚实转换,以缩短时间。一种常用优化方法是使用页内偏移地址(虚拟地址和物理地址中相同的部分)来索引缓存。这种虚拟地址索引/物理地址标签方法增加了系统复杂度以及对L1缓存大小与结构的限制,但从关键路径中消除了变换旁路缓冲区访问这一收益大于损失。
针对不同的用户运行大量并发进程,存储器带宽变得更加重要。因为处理器需要更大的缓存和更激进的存储器系统来增加带宽。
PMD必须同时考虑性能和能耗,因为能耗决定了电池寿命。
使用SRAM可以满足最小化缓存访问时间的需求。然而,当发生缓存缺失时,需要尽可能快地将数据从主存储器中取出,而这需要高带宽存储器。其实现方式有三:
1. 将组成主存储器的许多DRAM芯片分配到多个存储体中实现;
2. 增加存储器总线宽度;
3. 结合使用上述方式;
随着突发传输存储器的引入(广泛应用于闪存和DRAM中),存储器延迟采用两种度量方法--访问时间和周期时间。访问时间是从发出读取请求到收到所需字之间的时间,周期时间是指对存储器发出的两次不相关请求之间的最短时间间隔。
自1975年以来,几乎所有计算机都将DRAM用作主存储器,将SRAM用作缓存,并在CPU的处理器芯片中集成一到三级缓存。
DRAM电路的动态本质要求在读取数据之后将其写回,因此在访问时间和周期时间之间存在差异,并需要刷新。SRAM不需要刷新,所以访问时间和周期时间非常接近。SRAM通常使用6个晶体管保存1位数据,以防止在读取信息时对信息造成干扰。在待机模式下,SRAM只需要很少的功耗来维持电荷。
早些时候,大多数桌面系统和服务器系统使用单独SRAM芯片作为其主缓存、第二级或第三级缓存;如今,这三级缓存都集成在处理器芯片上。
片上缓存SRAM的数据位宽通常与缓存的块大小相匹配,每个块对应的标签都与其并行存储。这样就可以在单个时钟周期内读取或写入整个块。在将缺失后获取的数据写入缓存时,或在写回一个必须从缓存中清除的块时,此功能特别有用。缓存的访问时间(忽略在组相连缓存中的命中检测和选择)与缓存中的块数成正比,而能耗则依赖于缓存中的比特数(静态功耗),也依赖于块数(动态功耗)。因为存储器更小一些,所以组相连缩短了对存储器的初始访问时间,但是增加了命中检测和块选择的时间。
在早期DRAM的容量增大时,由于封装需要提供所有必要的地址线,所以封装成本较高。解决方法是复用地址线,从而将地址管脚数量减半。现在行选通期间发送一半地址,然后在列选通期间发送另外一半地址。
现代DRAM是以存储器为单位进行组织的,DDR4有16个bank。每一bank由一系列行构成。发送Active命令会打开一个bank的一行,并将该行载入行缓冲区。将行放入缓存区后,就可以采用两种方式进行传输:一种是根据DRAM的宽度采用连续列地址传送(在DRR4中,这一宽度为4位、8位或16位),另一种是指定块传送方式,并给出起始地址。Prechage命令会关闭bank和行,并为新的访问做准备。每个命令和块传送过程都以一个时钟进行同步。
为了在每个芯片中容纳更多的位,DRAM仅使用1个晶体管来存储1位数据。这有两层含义。用来检测电荷的传感线必须进行预充电,使其设定为介于逻辑0和逻辑1之间的“中间状态”,这样,只需在单元中存储很少量的电荷就可以使信号放大器检测到逻辑0或1。在读取时,将一行放入行缓冲器中,列选通CAS信号可以在这里选择从DRAM中读取该行的一部分。因为对数据行的读取会破坏其中的信息,所以当不再需要该行时,必须将其写回。这一写回过程以重叠方式进行,但在早期DRAM中,这意味着读取一行并访问该行的一部分之后,还需要等待一定的时间才能读取一个新行。
此外,为了防止单元中的电荷泄露(假设既没有读取它,也没有写入它)而导致信息丢失,必须定期“刷新”每个位。只需对一行进行读取并将其写回,就可以同时刷新该行中的所有位。因此,存储器系统中的每个DRAM必须在特定时间窗口内(比如64ms)访问每一行。存储器控制器包括定期刷新DRAM的硬件。
这一要求意味着存储器系统偶尔会不可用,因为它要发出一个信号,告诉每个芯片进行刷新。刷新时间等于一次行激活和一次预充电的时间,预充电也会将该行写回(由于不需要进行列选择操作,所以写回时间大约是获取数据时间的2/3)。由于DRAM中的存储器矩阵在概念上是方形的,所以一次刷新的步骤通常是DRAM容量的平方根。DRAM设计人员努力将刷新时间保持在总时间的5%以下。
DRAM控制器(通常位于处理器芯片上)会利用SDRAM尝试优化访问过程,尽可能避免打开新行和使用块传输。
DRAM现在的性能提升速度非常慢,主要是因为行访问时间未能大幅缩短,它是由很多问题决定的,比如功耗限制、单个存储器单元的充电容量(以及存储器单元的大小)。
尽管非常早期的DRAM中有一个缓冲区,可以对单个行进行多次列访问,不需要启动新的行访问过程,但它们采用了一种异步接口,这意味着每次进行列访问和传输时,都额外需要一些时间与控制器进行同步。20世纪90年代中期,设计人员向DRAM接口中增加了一个时钟信号,这样重复进行的传输就不在需要额外的同步时间,这就是同步DRAM(SDRAM)。除了缩减时间开销之外,SDRAM还允许添加一种突发式传输模式,在这种模式中,可以进行多次传输而无需指定新的列地址,既一次可以多次多位传输。增加这种突发式传输之后,随机访问一串数据与连续访问一块数据的带宽存在显著差异。
为了克服在DRAM密度增大时从存储器获得更多带宽的问题,人们加大了DRAM的宽度,起初,他们提供一种4位传输;2017年,DDR2、DDR3和DDR4采用了4、8或16位总线。
21世纪早期又推出另外一种创新:双倍数据速率DDR,它使DRAM在存储器时钟周期的上升和下降通道沿都能传输数据,从而使峰值数据传输速率翻倍。
最后,SDRAM引入了bank,用于帮助功耗管理、缩短访问时间,并允许对不同存储器进行相互交织、重叠的访问。对不同bank的访问可以相互重叠,每个bank都有自己的行缓冲区。在一个DRAM中创建多个bank实际上又为该地址增加了一个段,现在的地址由bank编号、行地址和列地址组成。在发出一个新bank的地址时,必须打开这个bank,从而增加了延迟时间。bank和行缓冲区完全由现代存储器控制接口处理,所以当后续地址指定的是一个已打开的bank的相同行时,只需发送列地址,从而快速进行访问。
要发起一次新的访问过程,SDRAM控制器发送一个存储器编号和一个行号(在SDRAM中称为激活,之前称为RAS行选通)。这个命令打开该行,并将数据读入一个缓冲区中。然后会发送一个列地址,SDRAM可以传送一个或多个数据项,具体取决于是单个请求还是突发请求。在访问新行之前,必须对bank进行预充电。如果该行位于同一bank内,则会感觉到预充电导致的延迟;但如果这个新行位于另一个bank内,那么行的关闭和bank的预充电可以与新行的访问重叠进行。
从DDR1到DDR3,访问时间改进了大约3倍,每年约7%。DDR4的功耗和带宽相对于DDR3均有改进,但访问时间相差无几。
随着DDR的推出,存储器设计者开始越来越多地关注带宽,这是因为访问时间已经很难再缩短了。采用更宽的DRAM、突发式传输、将数据率翻倍,都为存储带宽的快速提升做出了贡献。
降低SDRAM中的功耗:
1.动态存储芯片中的功耗由静态(或待机)功耗和读写期间消耗的动态功耗构成,这2者都取决于工作电压。电压降低,功耗降低。bank的增加也降低了功耗,这是因为每次仅读取一个bank的中的行。
2.最新的SDRAM都支持一种断电模式,通知DRAM忽略时钟进入低功耗模式。从低功耗模式返回正常模式所需的确切延迟取决于SDRAM,一般为200个时钟周期。
GDRAM是一种特殊的DRAM,以SDRAM设计为基础,但为满足GPU的高带宽需求进行了定制。GDDR5以DDR3为基础,与DRAM芯片有以下几点不同:
(1)GDDR接口更宽,为32位,而早期DDR为4、8或16位。
(2)GDDR数据管脚上的最大时钟频率更高。为了在提高传输速率的同时不引起发送出错,GDRAM通常直接与GPU相连,焊接在电路板上,而DRAM通常放在可扩展的DIMM阵列上。
使得GDDR中的每个DRAM的带宽是DDR3 DRAM的2~5倍。
DRAM在2017年的最新创新是一种封装创新,而不是电路创新。它将多个DRAM以一种堆叠或相依方式嵌入在同一个处理器封装内部。将DRAM和处理器放在同一个封装内,可以降低访问延迟(通过缩短DRAM与处理器之间的延迟),从而允许在处理器和DRAM之间建立更多、更快地连接,进而提升带宽,因此被称为HBM。
该技术的一个版本是将DRAM晶片直接放到CPU晶片上,使用焊料凸块技术来连接它们。只要管理好发热,就可以采用这种方式堆叠多个DRAM晶片。另一种方法是仅堆叠DRAM,然后使用一个完成连接功能的基地(中介层)将它们与CPU连接到单个封装内。
HBM很可能会成为GDDR5在高端GPU中的后继者。
闪存通常是只读的,但可擦除。闪存的另一个重要特性是在没有供电的情况下保存其内容。本书重点研究NAND闪存,它比NOR闪存密度更高,更适合大规模非易失性存储器。其缺点在于访问是顺序的,写入更慢。
闪存使用的体系结构与标准的DRAM有很大不同,性质也有所不同。最重要区别如下:
(1)对闪存的读取是顺序的,而且会读取一整页,该页可能是512字节、2KB或4KB。因此,NAND闪存在访问一个随机地址的第一个字节时,其延迟较长(大约25us),但在提供一个数据页的其余部分时,速度能达到40MB/s。闪存速度大约是DDR的1/50,但比磁盘快300~500倍。
(2)在重写缓存之前,必须将其擦除(闪存中闪字就是“快速擦除”的意思),并且是以块而不是字节或单词的形式将其擦除。这一要求意味着,当必须将数据写入闪存时,必须将整个块组装起来,要么作为新数据,要么将要写入的数据与块的其余内容合并。
(3)闪存是非易失性的,在未进行读写时候,功耗非常低(在待机模式会低于一半,在完全非激活状态下可以为零)。
(4)闪存限制了任何给定块的写入次数,通常为100000次。通过确保写入块在存储器中的均匀分布,系统可以最大限度地延长闪存系统的寿命。这种技术被称为写入均衡,由闪存控制器处理。
(5)高密度NAND闪存比SDRAM便宜,但比磁盘贵。
与DRAM一样,闪存芯片也包含冗余块,允许少量的缺陷;块的重映射在闪存芯片中处理。闪存控制器处理页面传输,提供页面缓存,并允许写入均衡。
通常使用一种小型的发热元件,使块状基地的状态在静态和非静态之间变化,这两种状态拥有不同的电阻特性。通过感测一个x,y交叉点的电阻就可以完成读取,它也因此有另一个名字--忆阻器,而写入过程则是通过施加电流来改变材料的相态而完成的。由于不存在有源器件,所以与NAND闪存相比,有可能做到成本更低而密度更大。
预计该技术的写入持久性远优于NAND闪存,而且不再需要再写入之前擦除整个页,所以其写入性能可能会比NAND高10倍。读取延迟性能也可能优于闪存2~3倍。
大型缓存和主存储器显著增加了制造过程和操作过程中动态发生错误的可能性。而电路变化引起的可重复的错误称为硬错误或永久性故障。所有DRAM、闪存和大多数SRAM在制造中都留有备用行,因此通过编程用备用行替换有缺陷的行可以解决少量的制造错误。动态错误是指在电路不改变的前提下存储单元内容发生改变的情况,被称为软错误(瞬态错误)。
动态错误可以使用奇偶验证法检测,可以使用ECC检测和纠正。因为指令缓存是只读的,所以用奇偶验证法就够了。在更大型的数据缓存和主存储器中,则使用ECC技术来检测和纠正错误。奇偶验证只需要占用一个数据位就可以检测一系列数据位中的一个错误。由于无法使用奇偶验证校验更多位的错误,所以必须限制用奇偶验证提供保护的位数。典型的比例是每8个数据位使用一个奇偶验证位。ECC可以检测2个错误并纠正一个错误,代价是每64位数据占用8位的开销。
Chipkill在本质上类似于磁盘中使用RAID方法,它分散数据和ECC信息,以便在单个存储芯片完全失效时,可以从其他存储芯片中重构丢失数据。例如IBM假定10000个处理器的服务器,在3年的运行中出现不可恢复的错误数目如下:
1. 仅采用奇偶验证位--大约90000个;
2. 仅采用ECC---大约3500个;
3. Chipkill--大约每2个月一个不可恢复;
前面的存储器平均访问时间提供3种优化指标:命中时间、缺失率和缺失代价。根据最近发展趋势,添加缓存带宽和功耗2个指标。将以下10种方法分为以下5类:
(1)缩短命中时间:小而简单的L1缓存和路预测,通常还能降低功耗。
(2)增加缓存带宽:缓存访问流水化、多体缓存和非阻塞缓存,会对功耗有不同的影响。
(3)降低缺失代价:关键字优先,合并写缓冲区。对功耗影响很小。
(4)降低缺失率:编译器优化。针对编译时的各种优化肯定可以降低功耗。
(5)并行执行降低缺失代价或缺失率:硬件预取和编译器预取。通常会增加功耗,主要是提前取出了未用到的数据。
提高时钟频率和降低功耗的双重压力推动了对L1缓存大小的限制。类似的,使用较低级别的相连度可以缩短命中时间、降低功耗。
缓存命中过程的关键计时路径由3个步骤组成:
1. 使用地址中的索引确定标签存储器的地址;
2. 将读取的标签值和地址进行比较;
3. 如果缓存为组相连,则设置多路选择器以选择正确的的数据项。
直接映射的缓存可以将标签检查和数据传输重叠,有效缩短命中时间。此外,在采用低相连度时,由于减少了必须访问的缓存行,所以通常还可以降低功耗。
尽管随着新一代微处理器的出现,片上缓存的总量已经大幅增加,但由于大容量L1缓存对时钟频率的影响,L1缓存大小的近期涨幅很小。在选择相连度时,另一个考虑因素是消除地址别名的可能性。
CACTI程序对CMOS微处理器上各种缓存结构的访问时间和能耗的估计误差在10%以内。对于一个给定最小工艺尺寸,CACTI根据缓存大小、相连度、读写端口和其他更复杂的参数来预估缓存的命中时间。
在选择缓存大小和相联度时,能耗也是一个考虑因素。在128KB和256KB缓存中,当从直接映射变到2路组相连,高相连度的能耗范围从大于2倍到忽略不计。
随着能耗变得至关重要,设计师开始关注减少访问所需能耗的方法。除了相连度之外,另一个决定缓存访问所需能耗的关键因素是缓存中块的大小,因为它决定了访问的行的数量。设计师通过增加块大小(保持总缓存大小不变)来减少行数,但是这会增加缺失率,对于较小的L1尤其如此。
另一种方法是将缓存分为多个存储体,这样一次访问只激活缓存的一部分,既包含所需块的那个存储体。多体缓存主要用于增加缓存的带宽,也会降低能耗,因为访问的缓存更少了。许多多核芯片上的L3在逻辑上是统一的, 但物理上是分散的,实际上就相当于一个多体缓存。
在近期的设计中,有3种其他因素导致了在L1使用更高的相连度。
1. 许多处理器在访问缓存时至少需要两个时钟周期,因此命中时间较长可能不会产生很大的影响。
2. 为了将TLB排除在关键路径之外(TLB带来的延迟可能要大于高相联度导致的延迟),几乎所有L1缓存都应当是虚拟地址索引的。这就将缓存的大小限制在页面大小与相联度的乘积。因为只有页内的位才能用于索引。
3. 在引入多线程之后,冲突缺失会增加,从而使提高相连度更有吸引力。
该方法既可以减少冲突缺失,同时又能保持直接映射缓存命中速度的方法。在路预测技术中,缓存中另外保存了一些位,用于预测下一次缓存访问中的路。
在一个缓存的每个块中添加块预测位。根据这些位来选定下一次缓存中尝试命中那些块。如果预测正确,则缓存访问延迟就等于这一快速命中时间。如果命中错误,则尝试其他块,改变路预测器,并且延迟会增加一个时钟周期。对于速度非常快的处理器,要将时延控制在一个周期是非常具有挑战性,而这对于降低路预测失误代价非常关键。
还有一种扩展形式的路预测,它使用路预测位(本质就是附加地址位)来判断实际访问的缓存块,也可以用来降低功耗。这种方法也可称为路选择,当路预测正确时,它可以节省功耗,但在路预测错误时则会显著增加时间,这是因为需要重复进行访问,而不仅是重复标签匹配和选择过程。这种优化方法只有在低功耗处理器中才有意义。路选择方法的一个重要缺点就是它增大了实现缓存流水化的难度。然而,随着能耗问题所受关注度增加,适时对缓存做低功耗处理的方案越来越有意义。
这类优化方法通过实现缓存访问的流水化,或者通过拓宽多体缓存,实现在每个时钟周期内进行多次访问,从而提高缓存的带宽。这种优化方法主要面向L1,这里的访问带宽限制了指令的吞吐率。L2和L3缓存中也会使用多个存储体,但主要是作为功耗管理技术。
L1缓存实现流水化后,可采用更高的时钟频率,但代价是会增加延迟。指令缓存访问的流水化增加了流水段的数目,增加了分支预测错误的代价。相应的,数据缓存的流水化增加了从发出载入指令到使用数据之间的时钟周期数。即使只是为了分开访问和命中检测这种简单情况,所有处理器都会使用某一级缓存流水化方法,而许多高速处理器则会采用三级或更多级缓存流水化方法。
指令缓存的流水化要比数据缓存容易一些,因为处理器可以依赖于高性能的分支预测来减轻延迟造成的影响。为了在每个时钟周期内处理多个数据缓存访问,可以将缓存划分为独立的存储体,每个存储体为一次独立的访问提供支持。分体方式最初用于提高主存储器的性能,现在也能用于DRAM芯片和缓存中。
当访问请求均匀分布在缓存组之间,分体方式的效果最佳,所以将地址映射到存储体的方式会影响存储器系统的行为。一种简单有效的映射方式是将缓存块地址按顺序分散在这些存储体中,称之为顺序交错。
多体缓存在L2或L3缓存也有应用,但原因不同。L2缓存中有多个存储体时,如果这些存储体没有冲突,那么可以同时处理多次L1缓存缺失--这是支持第四种优化方式非阻塞式缓存的关键能力。
对于允许乱序执行的流水化计算机,其处理器不必因为一次数据缓存的缺失而停顿。非阻塞式缓存允许数据缓存在一次缺失期间继续提供缓存命中。该方法并没有真正忽略处理器的请求,但降低了实际的缺失代价。
对于非阻塞式缓存进行性能评估时,真正的难度在于一次缓存缺失并不一定会使处理器停顿,很难判断一次缺失造成的影响,因此也就很难计算存储器平均访问时间。实际缺失代价并不等于这些缺失之和,而是等于处理器停顿的非重叠时间。非阻塞缓存的优势非常复杂,因为它取决于存在多次缺失时的缺失代价、存储器访问模式以及处理器在处理单次缺失时能够执行多少条指令。
通常,乱序处理器能够隐藏在L2缓存命中但在L1数据缓存中缺失的大部分缺失代价,但无法隐藏更低层次缓存中缺失的大部分代价。在决定支持多个未处理器缺失时,需要考虑多种因素,如下所述:
1. 缺失流中的时间与空间局部性,它决定了一次缺失能否触发对低级缓存或对存储器的新访问操作。
2. 对访问请求做出回应的存储器或缓存的带宽。
3. 为了允许最低级别的缓存中出现更多的未处理缺失,需要在较高级别上支持至少同等数量的缺失,这是因为这些缺失必须在最高级别的缓存上启动。
4. 存储器系统的延迟。
实现非阻塞缓存,出现了两种类型的挑战:
1. 仲裁命中与缺失之间的冲突:命中很可能会与低一级存储器返回的缺失发生冲突。如果允许存在多个尚未解决的缺失,那么缺失之间很可能会发生冲突。解决的方法是,首先未命中赋予比缺失更高的优先级,其次是在出现互相冲突的缺失时对其进行排序。
2. 跟踪尚未解决的缺失,以便知道何时可以处理载入或存储操作。这是因为我们需要跟踪多个尚未解决的缺失。如果L2是非阻塞式的,那么向L1返回缺失的顺序就未必与它们最初的发生顺序一致了。缓存访问时间不一致的多核系统及其他多处理器系统,也可能会引入这一复杂性。
在返回一个缺失时,处理器必须知道那个载入或存储导致了这一缺失,这样指令才能进行下去;它还必须知道应当将数据放到缓存中的那个位置(以及针对这个块的标签设置),通常称为缺失状态处理寄存器,如果允许存在n个尚未解决的缺失,就会有n个对应的寄存器。其中每一个都保存了关于某一个缺失应当进入缓存中的什么位置,以及这一缺失的任意标签位置的取值等信息,还包括了关于那个载入或存储指令导致了这一个缺失的信息。存储器系统在返回数据时使用该标签,从而使缓存系统能够将数据和标签信息传送给适当的缓存块,并向生成这一缺失的载入或存储操作发出“通知”,告诉它数据现在已经可用了,它可以恢复执行了。非阻塞式缓存显然需要额外的逻辑处理,从而需要一点能耗。但很难评估它们的开销,因为它们可能会缩短停顿时间,从而降低执行时间和相应的能耗。
由于缓存缺失不再具有原子性(因为请求和响应式分离的,可能会在多个请求之间发生交错),所以存在出现死锁的可能性。
该技术的基础是处理器通常一次仅需要缓存块中的一个字,于是无须等待整个块载入完成,就可以发送请求的字并重新执行处理器。具体策略为:
1. 关键字优先:首先从存储器中请求缺失的字,在其到达缓存之后立即发送给处理器,让处理器能够在载入块中的其他的字时继续执行;
2. 提前重新执行:以正常顺序提取字,但只要块中的被请求的字到达缓存,就立即将其发送给处理器,让处理器继续执行。
一般来说,这些技术只对使用大缓存块的设计有利。和非阻塞式缓存一样,其缺失代价也不好计算。在采用关键字优先策略时,如果存在第二次请求,则实际缺失代价等于从本次访问开始到第二部分内容到达之前的非重叠时间。关键字优先和提前重新执行的好处取决于块的大小以及对块中尚未获取的部分进行另一个访问的可能性。
1. 如果写缓冲区为空,则数据和整个地址被写到缓冲区中,从处理器的角度来看,写操作已经完成;在写缓冲区准备将字写入存储器时,处理器继续自己的工作。
2. 如果缓冲区中包含其他经过修改的块,则可以检查它们的地址,看看新数据的地址是否匹配写缓冲区中某个条目的有效地址。如果匹配,则将新数据与这个条目合并在一起,被称为写合并。
3. 如果缓冲区已满,而且没有匹配的地址,则缓存(和处理器)必须一直等到缓冲区中拥有空白条目为止。由于多字写入的速度通常快于每次只写入一个字的写操作,所以这种优化方法可以更高效地使用存储器。
这种优化方法还可能减少因为写缓冲区已满而导致的停顿。
注意,输入/输出设备寄存器经常被映射到物理地址空间。这些IO地址不允许写合并,因为单独IO寄存器不能像存储器中的字数组那样操作。
处理器与主存储器之间的性能差距越拉越大,促使编译器开发人员深入研究存储器的层次结构,以判断能否在程序编译时通过各种优化技术来提高性能。研究包括2个方面:指令缓存缺失的性能改进和数据缓存缺失的性能改进。
如果在嵌套循环中以非连续顺序访问存储器中的数据,只要交换一下这些循环的嵌套顺序,就可以使程序代码按照数据的存储顺序来访问它们,使得缓存块中的数据在被替换之前,得到最大限度的使用,即这一技术可以通过改善空间局部性来减少缺失。
通过改善时间局部性来减少缓存缺失。若在每个循环迭代都使用了行与列,所以按行或按列存储数组并不能解决问题。这种正交访问(行列访问都存在)方式意味着进行循环交换之类的转换操作之后,仍然有很大的改进空间。
分块算法并不是对一个数组的整行或整列进行操作,而是对其子矩阵(或称块)进行操作。其目的是在缓存中载入的数据被替换之前,最大限度地利用它。
容量缺失的数目显然取决于N和缓存的大小,在最差的情况需要O(N^3)次内存访问。为了确保正在访问的元素能够放在缓存中,将源代码改为计算一个B*B的子矩阵。如果仅观察容量缺失,从存储器中访问的总字数为O(N^3/B)。这是因为左矩阵收益于空间局部性,而右矩阵收益于时间局部性。
尽管我们的目标是减少缓存缺失,但分块方法可用于帮助实现寄存器分配。通过设定一个较小的分块大小,使块能够保存在寄存器中,可最大限度地减少程序中的载入与存储指令的数量。
通过将执行过程与访存过程重叠,非阻塞式缓存能有效地降低缺失代价。另一种方法是在处理器真正需要某个数据之前,预先获取它们。
指令预取经常在缓存外部的硬件中完成。通常,处理器在一次缺失时提取2个块:被请求的块和下一个块。被请求的块放在它返回时的指令缓存中,预取的块被放在指令流缓冲区中。若被请求的块存在于指令流缓冲区中,则取消该缓存请求,从流缓冲区中读取这个块,并发出下一条预取请求。
预取操作需要利用空闲的存储带宽,但如果它干扰了其他关键路径缺失内容的访问,反而会导致性能下降。如果预取的数据并未被用到或者替换了有用数据,预取操作会对功耗产生负面影响。
硬件预取之外的另一种方法是:编译器插入预取指令。共有2种预取:
1. 寄存器预取将数据值载入下一个寄存器。
2. 缓存预取仅将数据载入缓存,而不是载入寄存器。
这2种预取都可能触发异常,既虚拟地址错误异常和保护冲突异常。普通的载入指令可视为“故障性寄存器预取指令”。如果一次预取可能导致异常,那么就把它转入空操作,空操作不会触发缺页错误。
最有效的预取对程序来说是“语义上不可见的”:它不会改变寄存器和存储器的内容,也不会导致虚拟存储器错误。
只有当处理器在预取数据时能够继续工作的情况下,预取才有意义。
编译器预取的目标也是将执行过程与数据预取过程重叠。循环是重要的预取优化目标,如果缺失代价很小,编译器只需将循环展开一两次,在执行时调度这些预取操作。如果缺失代价很大,它会使用软件流水线或者将循环展开多次,以预先提取数据,供后续迭代使用。
不过,发出预取指令会带来指令开销,所以编译器必须确保这些开销不会大于收益。
HBM封装技术中封装的容量,难以满足服务器中大多数通用处理器对存储器的需求,所以人们建议使用与计算芯片封装在一起的DRAM来构建大容量的L4缓存。128MB到1GB的HBM技术出现,L4缓存的容量要远大于目前的片上L3缓存容量。如此之大的DRAM缓存会带来一个问题,缓存标签放在那里?这取决于标签的数量,假定块大小为64B,那么1GB的L4缓存需要96MB的标签,远多于CPU上缓存的静态存储器数量。将块大小增大至4KB,会使标签数量急剧缩减至256K项。尽管如此,这样大的块会有2个重要问题:
1. 如果许多块的内容都不会用到,缓存的使用效率可能会比较低下,被称为碎片化问题,它出现在虚拟存储器中。
2. 由于数据块比较大,所以DRAM缓存中保存的不同数据块的数目就要少很多了,这样会导致更多的缺失,尤其是冲突缺失和一致性缺失。
第1个问题的部分解决方法是增加子块。子块允许一个缓存行中只有部分数据有效,当发生缺失时,可以只获取其中有效的子块。但对于第2个问题,子块无能为力。
使用较小的数据块,标签存储是一个主要缺陷。一个可能有效的解决方法是直接把HBM作为L4缓存的标签存储到HBM中。看似不可行的原因是每次访问一次L4缓存都需要访问2次DRAM:一次用于标签,一次用于数据本身。一种更聪明解决方法--LH是:将标签和数据放在HBM SDRAM中的同一行。尽管打开这个行(还有最后关闭这个行)需要大量时间,但访问同一行的不同部分所带来的CAS延迟,大约是访问一个新行所需时间的1/3。因此,我们可以先访问这个块的标签部分,如果命中,则使用一次列访问来选择正确的字。
alloy cache熔合缓存的改进方法可以缩短命中时间。熔合缓存将标签和数据融在一起,并使用一种直接映射的缓存结构。这一改进通过直接对HBM缓存进行索引,并对标签和数据进行突发传输,使L4缓存访问时间缩短到一个HBM周期。与LH方法对比。熔合缓存将命中时间缩短至不到1/2,代价则是缺失率增加到1.1~1.2倍。
不过这2种方法在每次缺失都需要2次完整的DRAM访问:一次用于获得初始标签,接下来一次用于访问主存储器。如果可以加快缺失检测的速度,就可能缩短缺失时间。有2种方法:
1. 使用一个位图来跟踪缓存中的这些块(不是跟踪位置,而是跟踪它是否存在);
2. 使用一个访存预测器,通过历史预测技术来预测可能出现的缺失;
熔合缓存方法的性能超过LH方案,甚至超过了目前还难以实现的SRAM标签,这是因为缺失预测期的访问时间短,预测结果好,从而缩短了预测缺失的时间,从而降低了缺失代价。
用于改善命中时间、带宽、缺失代价以及缺失率的技术通常会影响存储器平均访问时间公式的其他部分,还会影响存储器层次结构的复杂度。
技术 | 命中 时间 | 带宽 | 缺失 代价 | 缺 失率 | 功耗 | 硬 件 成本/ 复 杂 度 | 注释 |
小而简单 的缓存 | + | - | + | 0 | 应用广泛 | ||
路预测缓存 | + | + | 1 | 在Pentium4中使用 | |||
流水化和 分体缓存 | - | + | 1 | 应用广泛 | |||
非阻塞缓存 | + | + | 3 | 应用广泛 | |||
关键字优和 提前重新执行 | + | 2 | 应用广泛 | ||||
合并写 缓冲区 | + | 1 | 与写直达一起广泛应用 | ||||
以编译器技术减少缓存缺失 | + | 0 | 软件是一个挑战,但许多编译器能处理常见的线性代数的计算 | ||||
指令和数据 的硬件预取 | + | + | - | 2 (指令) 3 (数据) | 大多提供预取指令;现代高端处理器还会再硬件中实现自动预取 | ||
编译器控制的预取 | + | + | 3 | 需要非阻塞缓存;可能存在指令开销;在许多CPU中得到应用; | |||
将HBM作为额外缓存 | +/- | - | + | + | 3 | 依赖新的封装技术。效果主要取决于命中率的改进。 |
VMM有3个基本特征:
1. VMM为程序提供了一种与原机器基本相同的运行环境。
2. 在这种环境中运行的程序在最糟糕的情况下速度也只有略有降低。
3. VMM可以完全控制系统资源。
虚拟存储器使得我们可以将物理存储器(可以是磁盘或固态)作为辅助存储的缓存。虚拟存储器在存储器层次结构的两个级别之间移动页面,就像缓存在两个级别之间移动块一样。类似地,TLB作为页表的缓存,从而消除了每次变换地址时进行存储器访问的需要。虚拟存储器还提供了共享一个物理存储器但具有独立虚拟地址空间的进程之间的隔离。
本节将重点讨论共享同一处理器的进程之间的保护和隐私方法的其他问题。
多道程序设计(几个同时运行的程序共享一台计算机的资源)需要在各个程序之间提供保护和共享,从而产生了进程的概念。打个比方,进程就是程序呼吸的空气、生存的空间---一个正在运行的程序加上继续运行它所需的全部状态。在任意时刻,必须能够从一个进程切换到另一个进程,被称为进程切换或上下文切换。
操作系统和体系结构联合起来就能使进程共享资源而不会相互干扰,则体系结构至少要做到以下几点:
1. 提供至少2种模式,指出正在运行的进程是用户进程还是操作系统进程。
2. 提供用户进程可以使用但不能写入的处理器状态的一部分。这些状态包括用户/管理模式位、异常启用/禁用位和存储器访问权限信息。
3. 提供处理器从用户模式转为管理模式及反向转换的机制。前者通过系统调用完成。
3. 提供限制存储器访问的机制,这样上下文切换时不需要将进程切换到磁盘就能保护该进程的存储器状态。
由于只有操作系统才能更新页表,所以分页机制提供了全面的访问保护。
分页存储器意味着每次存储器访问在逻辑上都花费至少2倍的时间,一次存储器访问用于获取物理地址,第二次访问用于获取数据。这种操作成本太高了。解决方案是依靠数据局部性原理带来的虚拟地址变换的局部性。这种特殊的地址变换缓存被称为变换旁路缓冲区TLB。
TLB条目类似于缓存条目,其中的标签保存虚拟地址的一部分,数据部分保存物理页地址、保护字段、有效位,通常还有一个使用位和脏位。
如今,不实施保护的代价比过去大得多,再加上存在上述问题,人们不得不去寻找一种代码比完整操作系统少得多的保护模型,比如虚拟机。
虚拟机在20世纪60年代被提出,在20世纪的80~90年代的单用户计算机领域被忽略,但近来得到广泛关注,原因如下:
1. 隔离与安全在现代系统中的重要性提高了;
2. 标准操作系统的安全性和可靠性出现了问题;
3. 许多不相关的用户(比如一个数据中心或云中的用户)会共享同一计算机;
4. 处理器原始速度的飞速增长,使虚拟机的开销更容易被人接受;
最广义的虚拟机定义基本包括了所有提供标准软件接口的仿真方法,如JAVA VM。最常见的情况是,VM支持的ISA与底层硬件相同。
为虚拟机提供支持的软件称为虚拟机监视器VMM,是虚拟机技术的核心。底层硬件平台称为宿主机,其资源在客户VM之间共享。VMM决定了如何将虚拟资源映射到物理资源:物理资源可以分时共享、划分,甚至可以在软件内模拟。VMM比传统操作系统小很多。
处理器虚拟化的成本取决于工作负载。用户级别的处理器操作密集型程序的虚拟化开销为零,这是因为很少会出现调用操作系统。与之相对,IO密集型工作负载通常也是操作系统密集型的,会执行许多系统调用(以满足IO需求)和特权指令(频繁使用会导致高昂的虚拟化开销)。这一开销的大小取决于必须由VMM模拟的指令数和模拟这些指令的缓慢程度。如果工作负载也是IO密集型的,那么由于处理器经常要等待IO,所以处理器虚拟化的成本可以完全被较低的处理器利用率所掩盖。
除VM提供保护的功能之外,VM还有2个具有重大商业价值的优点。
1. 软件管理:VM提供了一种抽象,可以运行整个软件栈。一种典型部署是一部分VM运行遗留操作系统,大量VM运行当前稳定的操作系统版本,而一少部分用于测试下一个操作系统版本。
2. 硬件管理:需要多台服务器的原因之一是:希望每个应用程序都能在独立的计算机上与其兼容的操作系统一起运行,这种隔离可以提高系统的可靠性。VM使得这些分享软件栈能够独立运行,却共享硬件,从而减少了服务器的数量。另一个例子是,大多数较新的VMM支持将正在运行的VM迁移到另一台计算机上,以负载均衡或从发生故障的硬件中退出。
VM监视器向客户提供一个软件接口,必须使不同客户软件的状态相互隔离,还必须保护自己以免受客户软件的破坏。定性需求包括:
1. 客户软件在VM上的运行情况应当与原始硬件上完全相同,当然,与性能相关的行为或者因为多个VM共享固定资源所造成的局限除外。
2. 客户软件应当不能直接修改实际系统资源的分配。
如果在设计ISA期间就为VM做了做了规划,就很容易减少VMM必须执行的指令数,缩短模拟这些指令所需的时间。
直到最近才开始考虑将VM用于桌面系统和基于PC的服务器应用程序,所以大多数指令集在设计时没有考虑虚拟化问题。x86体系结构的最新特性试图弥补早期的缺陷,而RISC-V明确地包含了对虚拟机的支持。
由于每个VM的每个客户操作系统都管理自己的页表集,所以虚拟存储器的虚拟化就成为另一项挑战。客户操作系统通过它的页表将虚拟存储器映射到实际存储器,VMM页表将客户的实际存储器映射到物理存储器。
VMM没有再为所有存储器访问进行多一层的中间访问,而是维护了一个影子页表,直接从客户端虚拟地址空间映射到硬件的物理地址空间。
体系结构中最后一个要虚拟化的部分是IO,到目前为止,这是系统虚拟化中最困难一部分,原因在于连接到计算机的IO设备数在增加,而且这些IO设备的类型也更加多样。另外一个难点在多个VM之间共享实际设备,还有一个难题需要支持不同的设备驱动程序,这一点在同一VM系统上支持不同客户操作系统时尤为困难。
最近,处理器设计师们已经引入指令集扩展来更高效支持虚拟化。性能提升的两个主要领域是对页表和TLB的处理,以及IO,特别是处理中断和DMA。虚拟存储器性能的提升主要是通过避免不必要的TLB刷新和使用嵌套页表机制,而不是一套完整的影子列表。为提升IO性能,增加了一些体系结构扩展,以允许设备直接使用DMA移动数据,而且运行客户操作系统直接处理设备中断和命令。
尽管人们对细粒度保护技术已经讨论了几十年,但一直没有得到多少推动,一方面是由于其开销很高,另一方面是因为其他一些效率更高,侵入性更低的解决方法已经为人们所接受。
在历史上,IBM大型机硬件和VMM通过以下3个步骤来提高虚拟机的性能。
1. 降低处理器虚拟化的成本;
2. 降低由于虚拟化而造成的中断开销成本;
3. 将中断传送给正确的VM,而不调用VMM,以中断成本。
许多采用乱序执行的处理器,甚至是一些拥有深度流水线的处理器,使用一种独立的取指单元将取指分离出来。通常,取指单元访问指令缓存,获取一个完整的块,然后对其译码,变为一条条单独的指令,这种技术在指令长度变化时尤其有用。
在支持推测的存储器系统中有两个问题:保护和性能。使用推测时,处理器可能会生成一些存储器访问;如果这些指令是错误推测的结果,那么这些存储器访问可能永远都不会用到。这些访问在执行时可能会产生保护异常。显然,这种故障应当仅在实际命令时才发生。
由于支持推测执行的处理器可能会访问指令与数据两种缓存,而接下来又没有用到这些访问的结果,所以推测可能会提高缓存缺失率。但通过预取操作,这些推测实际可能会降低总的缺失代价。
在超标量处理器中,最大的挑战之一是提供指令带宽。
数据可以同时存在于存储器和缓存中。只要处理器是唯一修改或读取数据的组件,并且缓存存在于处理器和存储器之间,处理器几乎没有看到旧副本的危险。不过多个处理器和IO设备增大了副本不一致及读取错误副本的可能性。
io缓存一致性问题可以表述如下:它干扰了处理器,可能会导致处理器因为等待io而停顿。输入还可能会用某些不会马上用到的新数据来取代缓存中某些信息,从而对缓存造成干扰。
在带有缓存的计算机中,io系统的目标应当是防止数据过期的问题,同时尽可能减轻干扰。因此,许多系统喜欢让io直接作用于主存储器,把主存储器当做一个io缓冲区。
如今通常只会在L1数据缓存中使用写直达方式,L2缓存中使用的方式就是写回方式。
软件解决方案是保证输入缓冲区的所有数据块都不在缓存中。硬件解决方案则是在输入时检查IO地址,查看它们是否在缓存中。如果在缓存中找到了io地址的匹配项,则使缓存失效,以避免过期数据。这些方法也能用于带有写回缓存的输出操作。
实际有3处陷阱:一是试图通过使用小的执行流来预测大容量缓存的性能。二是程序的局部性特性在整个程序运行期间不是恒定的。三是程序的局部性特征可能随输入的变化而变化。
缓存有助于缩短平均存储器延迟,但缓存可能无法为应用程序中必须转到主存储器的访问提供高存储带宽。
之前的架构师没有认真确保所有读写硬件资源相关信息的指令的特权指令,给体系结构的VMM带来了困难。以x86的io为例,io虚拟化的难点在于,它既支持存储器映射io,也拥有独立io指令,但更难在于PC拥有数目庞大、种类繁多的设备和设备驱动程序。传统VM提出了一种解决方法是将实际设备驱动程序直接载入VMM。
存储器层次结构的可能性追溯到20世纪40~50年代。虚拟存储器出现在20世纪60年代,缓存出现在70年代。
导致存储器层次结构的设计发生重大变化的一个趋势是,DRAM密度的增长速度和访问时间的缩短速度都在持续变缓。DRAM中使用的槽形电容器设计限制了它的扩展能力。诸如堆叠存储器之类的封装技术很有可能成为提升带宽和缩短延迟的主要方式。
闪存相对于DRAM的潜在优势也正是它的致命弱点:它必须采用相当慢的批擦除重写周期。
相变材料相对闪存的几点优势:不再需要速度缓存的“擦除-写入”周期,寿命也要长的多。这项技术很可能最终会取代在大容量存储领域占据主导地位50多年的磁盘技术。
近年来,存储墙可能会严重限制处理器性能。所幸,多级缓存的扩展(从2级到4级)、更高级的填充和预取方案、编译器和程序员对局部性重要程度的更深入理解、DRAM带宽的显著增加,这些都阻碍了存储墙的到来。访问时间对L1大小的约束(受时钟周期的限制)以及能耗原因对L2和L3大小的限制,都提出了新的挑战。更积极的使用预取操作,就是尝试克服因为不能加大L2和L3所带来的影响。片外L4缓存的重要性很可能会提高,因为与片上缓存相比,它们受到的能耗限制较少。
除了依赖多级缓存的机制之外,引入乱序流水线,允许存在多个待解决的缺失,就可以使现有的指令级并行机制隐藏缓存系统中的存储器延迟。而多线程及更多线程级并行机制的引入则更进一步隐藏延迟的影响。
一个频繁被提及的想法是由程序员控制的便笺式存储器或其他可见存储器。不过其从未成为通用处理器的主流,有以下原因:
1. 它们引入了具有不同行为特性的地址空间,打破了存储器模型;
2. 与基于编译器或依托程序员的缓存优化的方式(比如预取的不同),便笺式存储器的地址转换必须完全处理从主存储器地址空间到便笺式存储器的重新映射。
对于能够使用这些存储器的领域特定的软件系统,便笺式存储器的性能收益非常高。因此,HBM技术很可能会被用于大型通用计算机的缓存。
随着摩尔定律和登纳德定律终结,便笺式存储器和类矢量寄存器组可能会得到更多应用。此外,计算机体系结构和相关软件的创新将共同提升性能和效率,这将是延续过去50年性能持续改进的关键。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。