<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>计算机系统 | Easton Man's Blog</title>
	<atom:link href="https://blog.eastonman.com/blog/category/tech/computer-system/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.eastonman.com</link>
	<description>临渊羡鱼，不如退而结网</description>
	<lastBuildDate>Sun, 31 Dec 2023 12:51:26 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.5</generator>

<image>
	<url>https://blog.eastonman.com/wp-content/uploads/2021/02/cropped-Logo-e1613298891313-32x32.png</url>
	<title>计算机系统 | Easton Man's Blog</title>
	<link>https://blog.eastonman.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>现代分支预测：从学术界到工业界</title>
		<link>https://blog.eastonman.com/blog/2023/12/modern-branch-prediction-from-academy-to-industry/</link>
					<comments>https://blog.eastonman.com/blog/2023/12/modern-branch-prediction-from-academy-to-industry/#comments</comments>
		
		<dc:creator><![CDATA[Easton Man]]></dc:creator>
		<pubDate>Sun, 31 Dec 2023 12:51:26 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[计算机系统]]></category>
		<category><![CDATA[Branch Prediction]]></category>
		<category><![CDATA[CPU]]></category>
		<guid isPermaLink="false">https://blog.eastonman.com/?p=1282</guid>

					<description><![CDATA[<p>预计阅读时间： 24 分钟 Branch Prediction Is Not A Solved Problem [&#8230;]</p>
The post <a href="https://blog.eastonman.com/blog/2023/12/modern-branch-prediction-from-academy-to-industry/">现代分支预测：从学术界到工业界</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></description>
										<content:encoded><![CDATA[<p class="wpwc-reading-time">预计阅读时间： 24 分钟</p>
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Branch Prediction Is Not A Solved Problem</p>
<cite>&#8211;Intel</cite></blockquote>



<p>我从 2022 年底开始在<a href="https://github.com/OpenXiangShan" target="_blank" rel="noopener" title="香山开源高性能处理器">香山开源高性能处理器</a>团队里实习，到现在已满一年了，一直主要负责前端取指模块中分支预测单元的改进。到了年底了，略有一点空闲时间来写一下目前为止在分支预测方面的见闻。</p>



<p>本文假定读者对数字电路设计和高性能 CPU 设计都有一定了解，知道什么是“分支预测”以及“分支预测”对于高性能 CPU 来说的重要意义，如果你完全不了解这部分内容，但又感兴趣的话，建议先去看一些相关的基础书籍和文章。</p>



<p>另外本文基本上仅探讨条件分支的预测，也即只进行 T/NT 的方向预测，其他类型的分支如果大家感兴趣我以后再写。</p>



<p>希望本文能被一些刚参加完龙芯杯，或者通过一生一芯学习的同学阅读。如果想要尝试做分支预测器方面的工作，或者到香山前端团队来实习，那么在雄心壮志开始准备大战预测算法提高准确率之前，请先停下来了解一下真正的高性能处理器设计对分支预测的需求。</p>



<h2 class="wp-block-heading">分支预测发展历史</h2>



<p>最古老的分支预测方法就是饱和计数器，这里假设读者已经充分了解什么是饱和计数器和用饱和计数器来预测分支的方法了。</p>



<p>饱和计数器只能作出偏向性的预测，并不能充分利用单个分支内部和分支之间的关联信息，因此很快就发展出了能够利用分支历史信息的预测器，例如 GShare 预测器:</p>



<figure class="wp-block-image aligncenter size-full"><img fetchpriority="high" decoding="async" width="567" height="309" src="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.01.12.png" alt="" class="wp-image-1293" srcset="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.01.12.png 567w, https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.01.12-300x163.png 300w" sizes="(max-width: 567px) 100vw, 567px" /><figcaption class="wp-element-caption">GShare预测器示意图<br>来自 <a href="https://courses.cs.washington.edu/courses/cse548/05wi/files/Patt-An-Analysis-of-Correlation-and-Predictability.pdf" target="_blank" rel="noopener" title="">An Analysis of Correlation and Predictability:<br>What Makes Two-Level Branch Predictors Work</a></figcaption></figure>



<p>GShare 预测器将分支的 PC 与一个全局历史寄存器 XOR 以后作为 index 来选择 PHT 中的一个饱和计数器，然后使用这个计数器给出预测方向，更新时同样也通过同样的方式索引到同一个饱和计数器并更新。</p>



<p>这种方式将不同全局分支历史下的同一条分支的预测分给了不同的饱和计数器，使得 GShare 预测器能够区分不同的历史。这样做带来的第一个好处是能够正确预测循环退出了。</p>



<p>另一个好处是，分支之间的相关性可以得到有效利用，下图是常见的分支间相关性的例子</p>



<figure class="wp-block-image aligncenter size-full"><img decoding="async" width="549" height="423" src="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.05.05-edited.png" alt="" class="wp-image-1297" srcset="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.05.05-edited.png 549w, https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.05.05-edited-300x231.png 300w" sizes="(max-width: 549px) 100vw, 549px" /><figcaption class="wp-element-caption">分支间相关的例子，来源同上面的图</figcaption></figure>



<p>以上的两个例子恰好对应局部分支历史和全局分支历史。循环退出的预测只需要单个 PC 的历史就可以解决，而分支间相关的例子则必须要全局的分支历史。可见不同的分支需要的历史是不同的，由此发展出了锦标赛预测器，由局部历史和全局历史的两个预测器分别独立预测，然后再由一个饱和计数器构成的选择结构选择使用哪个子预测器提供的方向预测。著名的 Alpha 21264 处理器就是使用了锦标赛预测器。</p>



<figure class="wp-block-image aligncenter size-full"><img decoding="async" width="704" height="414" src="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.24.28.png" alt="" class="wp-image-1300" srcset="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.24.28.png 704w, https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.24.28-300x176.png 300w" sizes="(max-width: 704px) 100vw, 704px" /><figcaption class="wp-element-caption">Alpha 21264 的分支预测器，来自其论文</figcaption></figure>



<p>Alpha 21264 是经典的超标量乱序处理器，它的论文值得大家一看 <a href="https://ieeexplore.ieee.org/document/755465" target="_blank" rel="noopener" title="">The Alpha 21264 microprocessor</a></p>



<h2 class="wp-block-heading">现代分支预测算法</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Branch prediction research is basically about separating easier and more difficult branches, and using a simple predictor for the easy branches and a complex predictor for the difficult ones.</p>
<cite>&#8212; Onur Mutlu (Computer Architecture and Digital Design, ETHZ)</cite></blockquote>



<p>现代的分支预测器算法只有一种——TAGE，几乎所有的学术研究和商业高性能处理器都使用 TAGE 或者 TAGE 的变种。TAGE 预测器是最早在 2006 年由 Andre Seznec 提出的，提出当时即获得当届分支预测锦标赛冠军。然后接下来直到 2016 年的所有分支预测锦标赛都由TAGE预测器的变种获得。</p>



<p>我在 2022 年参加龙芯杯比赛复现 TAGE 预测器的时候，对 Mutlu 教授在课程中的这句话感受颇深。分支本身的预测难度不尽相同， 如果使用复杂的分支预测器预测简单的分支，那么大概率会浪费存储空间。TAGE 预测器的设计和这一个思想不谋而合，它使用多个不同历史长度的子预测器，并且使用一套机制自动地在这些子预测器中分配表项。这样能够做到使用短历史预测简单分支，长历史预测复杂分支，同时也能够避免存储空间的浪费。</p>



<p>以下是 TAGE 预测器的框图</p>



<figure class="wp-block-image aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="810" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12-1024x810.png" alt="" class="wp-image-1316" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12-1024x810.png 1024w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12-300x237.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12-768x607.png 768w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12-1536x1215.png 1536w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-09.44.12.png 1808w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>对预测机制细节感兴趣的读者可以参考原论文 <a href="https://jilp.org/vol8/v8paper1.pdf" target="_blank" rel="noopener" title="">A case for (partially) <strong>TA</strong>gged <strong>GE</strong>ometric history length branch prediction</a></p>



<h2 class="wp-block-heading">解耦合前端与覆盖重定向</h2>



<p>像 TAG E这种复杂程度的预测器是没有可能当拍就能得到预测结果的，本文撰写的时期（2023年）附近的半导体工艺对于 SRAM 微缩已经遇到了困难，目前 SRAM 访问就需要接近一个时钟周期或者一个完整的时钟周期了。另外考虑到每个 PC 都访问复杂预测器在功耗上的开销，商业处理器也很少做非常激进的省拍策略（如提前流水等）。</p>



<p>故 TAGE 预测器的返回结果到下一次的预测之间就出现了空拍，这是难以接受的，对性能的影响非常大。所以很自然的就出现多级预测器——覆盖重定向这种策略。这种设计一般使用简单的预测器进行 zero-bubble 的预测，然后再使用复杂预测器对简单预测器的结果进行验证，如果预测不一致，就会发出一个前端重定向，重新开始一个预测流水。这样的做法可以在简单预测器准确率较高的情况下，省去复杂预测器的访问延迟。</p>



<p>下图是一个 SiFive P870 的设计，可以明显得看出具有 Cascading BP 的的设计。</p>



<figure class="wp-block-image aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="406" src="https://blog.eastonman.com/wp-content/uploads/2023/12/image-1024x406.png" alt="" class="wp-image-1318" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/image-1024x406.png 1024w, https://blog.eastonman.com/wp-content/uploads/2023/12/image-300x119.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/image-768x305.png 768w, https://blog.eastonman.com/wp-content/uploads/2023/12/image-1536x610.png 1536w, https://blog.eastonman.com/wp-content/uploads/2023/12/image.png 1920w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>覆盖重定向的设计带来了新的问题——预测流水和取指流水，也即 ICache 流水，的速度不匹配。预测流水出现前端重定向的时候，ICache 可能仍能取指，ICache 出现缺失的时候，BP 就处于阻塞状态。为了避免相互影响，现代高性能处理器普遍采用了解耦合前端（或称为分离式前端），只有少量的处理器设计采用传统的设计。</p>



<p>下图是高通在 HPCA 2019 上发表的论文 <em><a href="https://ieeexplore.ieee.org/document/8675212" target="_blank" rel="noopener" title="Elastic Instruction Fetching">Elastic Instruction Fetching</a></em> 中的配图</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="521" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35-1024x521.png" alt="" class="wp-image-1323" style="width:694px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35-1024x521.png 1024w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35-300x153.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35-768x390.png 768w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35-1536x781.png 1536w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-10.18.35.png 1546w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">学术与产业落地</h2>



<p>我曾经一度认为商业公司秘密发展了新的分支预测算法，而且不予公开，并且感到十分不高兴。</p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" width="524" height="128" src="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.32.07.png" alt="" class="wp-image-1305" srcset="https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.32.07.png 524w, https://blog.eastonman.com/wp-content/uploads/2023/10/Screenshot-2023-10-10-at-20.32.07-300x73.png 300w" sizes="(max-width: 524px) 100vw, 524px" /><figcaption class="wp-element-caption">年轻的我.jpg</figcaption></figure>



<p>后来我很快认识到工业界中使用的分支预测算法非常的保守，甚至大部分的商业处理器都只使用了 TAGE 预测器或者 TAGE-L 等价的预测器。</p>



<p>香山处理器的分支预测器也从雁栖湖的 TAGE-SC-L 变成了南湖和昆明湖的 TAGE-SC，然后甚至正在考虑去掉 SC。</p>



<p>这样的情况可能和很多人的认识相悖，尤其是刚参加完龙芯杯或者一生一芯学习的同学。也和一些刚毕业的研究生或者长时间从事学术研究的人的认识有冲突。分支预测准确率对于高性能处理器的影响是非常巨大的，为什么这些在相同存储下能够取得更好预测准确率的算法没有被工业界采用？</p>



<p>接下来我们就讨论一下工业界对于分支预测算法更关注什么？</p>



<h2 class="wp-block-heading">工业界关注什么？</h2>



<p>首先，工业界和学术界的区别在哪里？学术界相信大家都很好理解，只要一个算法在相同的存储空间下能够取得更好的准确率，就可以认为这是一个更好的算法。但是在商业高性能处理器的设计中，还必须要考虑实际的延迟，以及性能、功耗、面积的平衡。</p>



<h3 class="wp-block-heading">更新延迟</h3>



<p>首先是一个非常明显的问题，部分学术界的研究也已经涉及到了，那就是在真实处理器中，分支预测器的更新只能在后端执行完毕以后才能拿到准确的分支执行情况。现代的预测算法都依赖分支的历史，如果历史不准确，将会极大地影响预测的准确率。然而下一条分支的预测开始时，上一条分支还远没有执行完毕，现代处理器中 in-flight 的分支数量普遍可达数十条。那么下一条分支如何拿到前一条分支的方向用于历史的计算？</p>



<p>目前普遍采用的做法是推测更新历史，也就是在预测后立即更新分支历史供下一条分支使用，然后在分支误预测发生时对历史进行恢复。这个做法能够较好的解决历史不及时的问题，但是 Pattern History Table（PHT）的更新依然可以是可以探讨的设计。是在分支执行完就更新？还是等到 commit 以后才更新？总体来说越早更新 warmup 越快，越晚更新更新得越准确。</p>



<p>推测更新历史的做法就引起了下一个问题——为什么现代处理器的分支历史都是全局历史？前文刚刚讨论了局部历史和全局历史对分支预测有不同的贡献，给任何预测器加入局部历史几乎都可以进一步提高预测准确率，那么为什么没有人用？只有部分处理器使用了专门的 Loop 预测器（Loop iteration count 就是一种特殊的局部历史）。这就涉及局部历史推测更新后的恢复困难问题。</p>



<h3 class="wp-block-heading">历史维护</h3>



<p>局部历史维护的困难可以参考 MICRO 2019&#8217;的论文 <em><a href="https://dl.acm.org/doi/10.1145/3352460.3358315" target="_blank" rel="noopener" title="Towards the adoption of Local Branch Predictors in Modern Out-of-Order Superscalar Processors ">Towards the adoption of Local Branch Predictors in Modern Out-of-Order Superscalar Processors </a></em>。</p>



<p>因为全局历史整个处理器只需要维护一份（或少量几份用于推测更新-恢复用），一般都直接使用寄存器来存储全局历史。但是局部历史是需要按照分支来维护，即便是只维护部分活跃的分支，也需要较大的表，因此几乎必须使用 SRAM 来存储。</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="1020" height="892" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-11.02.03.png" alt="" class="wp-image-1334" style="width:464px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-11.02.03.png 1020w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-11.02.03-300x262.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-11.02.03-768x672.png 768w" sizes="(max-width: 1020px) 100vw, 1020px" /></figure>



<p>上图是按照分支预测锦标赛中软件实现的 Loop 预测器设计的对应硬件的框图，可以看到需要一个 Branch History Table（BHT）用于记录目前的循环计数。</p>



<p>使用 SRAM 存储带来的问题就是，恢复历史时无法在一拍内恢复或者甚至无法在短时间内恢复。目前已有的几种解决办法是</p>



<ul class="wp-block-list">
<li>维护 in-flight queue，预测时同时查询 queue 获得新的历史，SRAM 内仅存储 commit 后的分支信息，不需要恢复 SRAM，对预测时序和功耗影响大。</li>



<li>维护 recover queue，SRAM 内推测更新历史，恢复时通过 walk queue 来恢复 SRAM 内的信息，对恢复速度要求高。</li>



<li>设计机制，让 SRAM 内历史不准确的时候不参与预测，可以较慢恢复 SRAM 内的信息。</li>
</ul>



<p>更多设计细节的评估和对比可以参考上面的论文。</p>



<p>这种维护的困难使得局部历史在现代处理器的分支预测器中很少见到。</p>



<h3 class="wp-block-heading">Path History</h3>



<p>无论是哪一种分支历史，由于推测更新的维护方式，都必须先出现在 BTB 中，才能进入历史。然而，正常程序中会出现大量从不跳转的分支（如 debug 或错误处理），没有人愿意为了存储这些分支耗费多几倍的 BTB 存储开销。现代处理器普遍只有曾经跳转过的分支才会存储进入 BTB 中，这给维护 Branch History 带来了困难，如果有新分支突然 taken，那么它附近的分支由于历史的变化，基本需要重新 warmup。有的处理器为了让有限的历史寄存器放入更多有意义的信息，可能会对分支做进一步的过滤，只有 T/NT 均出现至少一次的分支才会进入历史，这样需要重新 warmup 的概率也就上升了。</p>



<p>因此工业界更倾向于使用 Path History。Path History就是将跳转的 Target 经过哈希计算进入历史中，这种历史天然地不受不跳转的分支影响。通过论文观察到 ARM 和 Intel 都使用了 Path History 来代替 Branch History。</p>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="818" height="136" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.55.28.png" alt="" class="wp-image-1339" style="width:517px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.55.28.png 818w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.55.28-300x50.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.55.28-768x128.png 768w" sizes="(max-width: 818px) 100vw, 818px" /><figcaption class="wp-element-caption">ARM的PHR</figcaption></figure>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="852" height="246" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.17.png" alt="" class="wp-image-1340" style="width:513px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.17.png 852w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.17-300x87.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.17-768x222.png 768w" sizes="(max-width: 852px) 100vw, 852px" /><figcaption class="wp-element-caption">Intel PHR 中 footprint 的哈希</figcaption></figure>



<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="826" height="224" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.22.png" alt="" class="wp-image-1341" style="width:546px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.22.png 826w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.22-300x81.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-16.56.22-768x208.png 768w" sizes="(max-width: 826px) 100vw, 826px" /><figcaption class="wp-element-caption">Intel 的 PHR 历史维护</figcaption></figure>



<h3 class="wp-block-heading">主频 &amp; 预测延迟</h3>



<p>如果说前面的几个方面只是增加了复杂分支预测算法落地的难度，那么预测延迟就是阻碍复杂算法落地的直接障碍。举一个例子，TAGE-SC-L 预测器中的 Statistical Corrector（SC）是为了纠正 TAGE 对于一些偏向性大的分支预测不好的部件，同时 SC 还可以作为 TAGE 的补充，如果 TAGE 由于哈希或者别的 corner case 出现失效，那么 SC 可以在一定程度上挽救问题。</p>



<p>但是 SC 需要使用 TAGE 的结果作为输入，还需要做所有表项的加法规约，因此在现代处理器的主频下，延迟比 TAGE 多1-2拍。这样多出来的延迟可能就把将存储分给 SC 的优势给抵消了，如果 SRAM 时序允许的话，给 TAGE 增加额外的 10% 的空间可能能取得更好的效果。SC 增加的延迟在学术上是欠评估的，因为 SC 提出的场景是分支预测锦标赛，该比赛并不要求模拟实际的预测延迟，因此复杂的预测算法更有可能因为合理的存储空间分配而获得更好的效果。</p>



<p>根据我的观察，SC 目前在大的商业处理器上应用很少，可能是加入这个部件增加的功耗、面积、验证成本等已经超过了它带来的误预测率降低的好处了。</p>



<h3 class="wp-block-heading">Multi-branch ahead</h3>



<p>根据统计，桌面应用的平均基本块长度约在5条指令附近（4-10），因此在设计宽译码、宽发射的高性能处理器时，如果每周期只预测一条分支，那么供指能力是不足的。现代高性能处理器普遍具备每周期预测两条指令的能力，但是根据设计能力和目标的不同，一般对两条指令的性质会有一些限制。最常见的是要求本周期的第一条指令不能跳转，这种限制大大降低了设计难度，因为在这样的限制下，两个基本块是连续的，每周期只会产生一个新的 PC，不需要做大量额外的设计就可以支持。</p>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="551" src="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24-1024x551.png" alt="" class="wp-image-1344" style="width:552px;height:auto" srcset="https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24-1024x551.png 1024w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24-300x161.png 300w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24-768x413.png 768w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24-1536x827.png 1536w, https://blog.eastonman.com/wp-content/uploads/2023/12/Screenshot-2023-12-31-at-17.21.24.png 1758w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">平均基本块长度和平均跳转长度的对比（注意单位是Bytes）</figcaption></figure>



<p>从上图中可以看到，基本上仅支持这种限制较严格的 2 Branch per cycle 就可以提升每周期的平均供指数量。香山处理器昆明湖目前也仅支持这种情况，实际在 SPEC CPU 2006 上可以做到平均每周期6-7条指令的供指能力。</p>



<p>近年来也有处理器能够支持 2-Taken per cycle的情况，如 ARM 和 Intel 的最新产品，但同时也有对两条分支的类型或者支持的数量做限制。</p>



<h3 class="wp-block-heading">BTB设计</h3>



<p>BTB 设计也是重要的一环，目前的设计方向有以下几个：</p>



<ul class="wp-block-list">
<li>多级 BTB</li>



<li>下级 BTB 与 Cache 共用存储</li>



<li>BTB 压缩</li>



<li>L0 BTB / 快速预测器的设计</li>



<li>L1 BTB 的类型</li>
</ul>



<p>其中除了 L1 BTB 的类型，其他的都比较好理解。那么为什么需要单独讨论 L1 BTB 的类型呢？因为 L0 BTB 大多使用寄存器，排布灵活，L2 BTB 有压缩等设计，类型也可以灵活。唯独 L1 BTB 在预测的关键路径上，不能使用复杂排布，又需要足够大的容量，因此需要使用 SRAM，对读写有较为严格的限制。因此 L1 BTB 的表项设计和索引方式是前端设计中的重点，这里的设计也会大幅影响 TAGE 预测器和整个前端的设计。</p>



<p>今年的 MICRO 2023&#8217;恰好有一篇BTB的类似综述文的文章 <em><a href="https://dl.acm.org/doi/10.1145/3613424.3623774" target="_blank" rel="noopener" title="Branch Target Buffer Organizations">Branch Target Buffer Organizations</a></em> ，感兴趣的读者可以找来看看。其中有对 L1 BTB 的三种常见类型的描述和对比</p>



<ul class="wp-block-list">
<li>I-BTB：紧耦合前端，使用 ICache+预译码充当大容量 BTB。采用这样设计的有苹果和 SiFive。这种设计的优点是不需要为 BTB 使用额外的存储，缺点是紧耦合前端供指能力稍弱，另外 ICache 存储分支的效率是较低的，超过 ICache 容量之后，将无法使用分支信息指导指令预取。</li>



<li>R-BTB：使用减少的空间来存储一定范围内的分支（如 64B），在范围内支持一定数量（如8条）分支，BTB 使用对齐的 PC 索引，通过 offset 来确定具体的分支。优点是使用独立的 BTB 和解耦前端后，前端对分支的处理能力大幅加强，IBM 的 z15 处理器设计中，BTB 的总容量甚至可以 cover L2 Cache 的大小。缺点是当分支密度高的时候无法全部覆盖。</li>



<li>B-BTB：每个表项内存储一个基本块（或一个 Fetch Stream）的信息（下称取指块），取指块内可以有多个分支，有最长的取指块大小，使用 PC 低位索引，高位做 Tag。这种做法解决了 R-BTB 对分支密度的限制，能够处理绝大多数的情况。缺点是一个分支可能出现在多个表项内，会浪费一定的存储空间。</li>
</ul>



<p>香山南湖和昆明湖的设计都是使用 B-BTB 的设计，取指块中允许两个 Branch Slot，第一个仅允许条件分支，第二个允许所有类型的分支。然后预测时如果第一个分支预测不跳转，那么可以按照离第二个分支的距离来供指。</p>



<h3 class="wp-block-heading">功耗</h3>



<p>功耗这一块其实我知之甚少，因为香山还没有开始做低功耗设计（狗头保命），但其实有许多大方向是低功耗设计共性的。首先，因为分支具有难度的差别，还有类型的差别，复杂的预测器和 ITTAGE 这类的间接跳转预测器是没有必要每个PC每个周期都访问的。另外 TAGE 中，每个历史表对于程序片段中的有用程度也是不一样的，所以通过统计的方法，是可以在保留大部分性能的情况下省去很多 SRAM 的功耗。</p>



<p>另外 SRAM 与 SRAM 天差地别，如果设计时给 SRAM 留了时序裕度，选用稍慢的 SRAM 可能可以大幅降低功耗。</p>



<h2 class="wp-block-heading">一定要预测吗？</h2>



<p>最后想谈一下从体系结构的角度，分支一定要预测吗？近年来分支预测算法发展陷入停滞，原因是 TAGE 预测器的算法已经足够优秀，能够在存储耗尽之前充分地挖掘一条分支的“可预测性”，后续的改进也基本针对 corner case 的优化和存储替换算法、置信度等的一些小优化。</p>



<p>那么，剩下的分支误预测怎么办，还有办法吗？此时就需要跳出固有的思维框架，不再仅思考纯硬件的做法，就会发现有新的天地。实际计算机的设计是一个系统工程，需要每一层紧密配合才能达到最好的性能。此处我们介绍两个已经实用的方法，有没有别的新的方法留待读者探究。</p>



<h3 class="wp-block-heading">谓词化指令</h3>



<p>谓词化指令其实就是赫赫有名的条件执行指令，例如 cmov 指令就是著名的谓词化指令。设计这种指令的初衷是很多难预测的分支是和程序数据紧密相关的，甚至直接依赖于数据，那么如果将这些分支转化为 cmov 或类似的条件转送/执行指令，就可以只出现数据流的变化，而不出现控制流的变化。</p>



<p>谓词化指令在 x86 体系和 ARM 体系中都有大量的使用，RISC-V 在 2023 年也通过了 Zicond 扩展。</p>



<h3 class="wp-block-heading">软硬结合</h3>



<p>这里讲的软硬结合大致是指 hint 类型的指令，苹果已有专利说明其设计的处理器中，有针对 macOS/iOS 进程间调用返回的长跳转做 hint 和指令预取的优化，具体的优化只能翻专利了。</p>



<h2 class="wp-block-heading">结语</h2>



<p>其实还有更多的具体设计细节，这里由于篇幅考虑（<s>实际是我偷懒</s>）就不详细展开了，在这里提一个列表共大家思考</p>



<ul class="wp-block-list">
<li>单端口 SRAM 的读写冲突处理</li>



<li>预测器 ctr 降低更新延迟的影响，加快 warmup 的方法</li>



<li>B-BTB 中的多个分支对于方向预测器容量需求不对称的问题</li>



<li>预测器中 SRAM 分 bank 的方法</li>



<li>TAGE 折叠历史的维护方法</li>



<li>各种预测器和 BTB 的索引哈希</li>
</ul>



<p>现在看了这么多设计考虑，你还认为预测准确率是分支预测器唯一的评价指标吗？</p>



<h2 class="wp-block-heading">参考文献</h2>



<ul class="wp-block-list">
<li><a href="https://ieeexplore.ieee.org/document/755465" target="_blank" rel="noopener" title="">The Alpha 21264 microprocessor</a></li>



<li><a href="https://jilp.org/vol8/v8paper1.pdf" target="_blank" rel="noopener" title="">A case for (partially) <strong>TA</strong>gged <strong>GE</strong>ometric history length branch prediction</a></li>



<li><a href="https://ieeexplore.ieee.org/document/8675212" target="_blank" rel="noopener" title="Elastic Instruction Fetching">Elastic Instruction Fetching</a></li>



<li><a href="https://dl.acm.org/doi/10.1145/3352460.3358315" target="_blank" rel="noopener" title="Towards the adoption of Local Branch Predictors in Modern Out-of-Order Superscalar Processors ">Towards the adoption of Local Branch Predictors in Modern Out-of-Order Superscalar Processors</a></li>



<li>Branch Target Buffer Organizations</li>



<li>Half&amp;Half: Demystifying Intel’s Directional Branch Predictors for Fast, Secure Partitioned Execution</li>



<li>Re-establishing Fetch-Directed Instruction Prefetching: An Industry Perspective</li>



<li>Rebalancing the core front-end through HPC code analysis</li>



<li><a href="https://xiangshan-doc.readthedocs.io/zh-cn/latest/frontend/bp/" target="_blank" rel="noopener" title="香山处理器分支预测文档">香山处理器分支预测文档</a></li>
</ul>The post <a href="https://blog.eastonman.com/blog/2023/12/modern-branch-prediction-from-academy-to-industry/">现代分支预测：从学术界到工业界</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></content:encoded>
					
					<wfw:commentRss>https://blog.eastonman.com/blog/2023/12/modern-branch-prediction-from-academy-to-industry/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>NSCSCC2022 龙芯杯参赛总结</title>
		<link>https://blog.eastonman.com/blog/2022/08/nscscc2022/</link>
					<comments>https://blog.eastonman.com/blog/2022/08/nscscc2022/#comments</comments>
		
		<dc:creator><![CDATA[Easton Man]]></dc:creator>
		<pubDate>Thu, 25 Aug 2022 03:32:37 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[计算机系统]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[NSCSCC]]></category>
		<guid isPermaLink="false">https://blog.eastonman.com/?p=1081</guid>

					<description><![CDATA[<p>我参加2022年龙芯杯的参赛总结</p>
The post <a href="https://blog.eastonman.com/blog/2022/08/nscscc2022/">NSCSCC2022 龙芯杯参赛总结</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></description>
										<content:encoded><![CDATA[<p class="wpwc-reading-time">预计阅读时间： 21 分钟</p>
<p>一转眼已经是八月下旬，暑假只剩一个星期了，<s>当然去年也是这样</s>。上一次写内容还是在年初，基本上半年以来的课外时间基本都投入在龙芯杯和相关的事情上了。龙芯杯就如字面上的意思是由龙芯中科公司和高等学校计算机教育研究委员会共同主办的一个赛事，赛事的主要内容是使用RTL设计一个完整的CPU，并在FPGA上进行验证和展示。2022年是龙芯杯的第六年，今年的比赛也和以往有所不同，新增了LoongArch挑战赛，我就是来吃螃蟹的人（<s>踩大坑</s>）</p>



<h2 class="wp-block-heading">新增赛道</h2>



<p>如上面所讲，2022年龙芯杯最大的不同就是除了往年的MIPS指令集的个人赛和团体赛，新增了LoongArch挑战赛赛道。这个新的赛道的赛制，日程和评价方式都和MIPS的赛道有相当大的区别，区别体现在：</p>



<ul class="wp-block-list"><li><strong>指令集区别</strong>：这个当然是最主要的区别，LoongArch挑战赛使用的是LA32 Reduced指令集，而以往的个人赛和团体赛都使用MIPS指令集。</li></ul>



<ul class="wp-block-list"><li><strong>日程区别</strong>：团体赛和个人赛分为初赛和决赛，2022年初赛在8月5日截止，决赛提交在8月19日截止，线上答题在8月19日，决赛答辩在8月20日。而LoongArch赛道没有区分初赛和决赛（<s>因为本来就没几支队伍</s>），性能测试的提交截止日期是8月14日，没有答辩，ppt展示视频的提交时间是8月18日.</li><li><strong>决赛门槛区别</strong>：MIPS团体赛的决赛门槛是性能分数，决赛队伍数量一般是较为恒定的，只要性能排名能够达到一定水平，就可以进入决赛。而LoongArch挑战赛虽然没有实质性的决赛，但是仍有一个门槛，就是<strong>启动Linux</strong>。</li><li><strong>评价方式区别</strong>：MIPS的团体赛最终评分是50%性能，50%系统展示和答辩，LoongArch则是70%性能，30%系统展示和答辩。</li><li><strong>官方提供的开发工具区别</strong>：MIPS团队赛只有提供功能测试、系统测试等一系列测试，而LoongArch提供了差分测试框架和随机验证的指令流，相比而言LoongArch的仿真验证手段更为先进。</li><li>一些细节要求的区别：比如LoongArch挑战赛性能测试样例不同，性能测试不允许修改SoC，也不允许使用自己编译的Linux。以及LoongArch允许研究生参赛。</li></ul>



<p>当然基本上参加比赛要完成内容是相近的，无非就是设计一个CPU，大部分本科生队伍都是遵循传统的5级流水线，在此基础之上增加流水级、增加顺序双发射、提高频率等等。能力强的队伍或者往届学长很强的队伍可以依照已有的框架进行部分的微架构创新，基本就是这样。</p>



<p>LoongArch指令集恰好也是在2022年大踏步进入开源社区，Linux、QEMU、GCC等诸多重要开源软件都在2022年加入了LoongArch的支持。于是接下来讲一讲龙芯这个LoongArch指令集与MIPS有什么不同。</p>



<p>首先是最明显的（<s>大快人心</s>）的变化就是去除了<strong>延迟槽</strong>。延迟槽是MIPS设计时对那个年代普遍具有的5级流水微架构做的<strong>过度设计</strong>，这种与微架构绑定的设计显然没有跟上飞速发展的硬件设计水平，在2022年看来，延迟槽这个东西属于无用且严重增加设计复杂度的指令集设计。LoongArch没有沿用这个诟病已久的特性我觉得是理所当然的。</p>



<p>其次是删除了协处理器这个概念，特权资源与RISC-V类似使用特权寄存器（CSR）来提供支持，以及浮点运算也不在由协处理器完成。虽然参加龙芯杯不需要实现硬件浮点部件，且在大部分RISC的实现中，浮点部件因为面积巨大，逻辑复杂，通常也是由专门的一个深流水部件完成，与主流水线是解耦的关系，但是我觉得这个仍然是一个好的设计，同样是去除了MIPS中不必要的微架构绑定，为更复杂的设计提供了预留的空间。</p>



<p>再次是对MIPS中的立即数编码做了修改，由统一16位修改为长20/短12的分离的立即数格式。这样做的好处在RISC-V上就能得到充分的体现，常用指令使用短立即数增大了指令的编码空间，长跳、立即数加载等指令使用专门的20位立即数，避免了使用短立即数带来的代码体积增加的问题。</p>



<p>除了上面这些与MIPS相比的优点，LoongArch与RISC-V相比也有相当大的不同。首先是LoongArch设计之初就考虑了大量扩展，相比于RISC-V的极简主义极致模块化，每个扩展缓慢发展，LA的设计更加偏向于商用和实用主义，其中不乏大量的tradeoff和edge case，指令格式的类型也比RISC-V要多很多。</p>



<h2 class="wp-block-heading">队伍情况</h2>



<p>原本以为LoongArch这么先进的基础设施和先进的指令集，以往的强校都会将有实力的队伍投入LA挑战赛中，结果实际上虽然LA有实力强劲的队伍，也有拿出了复杂的乱序设计的队伍，但是传统强校依然基本将重心放在MIPS的团队赛。我猜测（盲猜，不对请大佬指正）是因为这些学校都有传承了数届的基础设施，MIPS的开发流程也能够做到十分先进高效，往年也有优秀的学长的设计可以参考，做到什么样的程度能拿到什么样的名次都十分清楚，因此不太愿意放弃成熟的基础设施和开发流程转而来新的赛道踩坑。</p>



<p>相比之下虽然我校也曾经在龙芯杯取得过一等奖第二名这样的成绩，然而我校却严重缺乏MIPS的基础设施和学长的经验传承，体现在以下几个方面：</p>



<ul class="wp-block-list"><li>没有差分测试框架。参加过多年的强校都有往届学长移植的差分测试框架，甚至有完善的CI自动化测试基础设施，这些基础设施我校一概没有。</li><li>没有学长成功启动过Linux。由于MIPS赛道的决赛门槛并不是支持操作系统，决赛评分中支持Linux也只是保证了一个高的起评分，因此在没有差分测试的情况下，我校的往届学长往往选择做出一个不错的性能，然后靠较为高级的外设和互动来拿到系统展示的分数。没有适配系统的经验对于想要启动Linux来说是非常致命的。</li><li>没有成熟的SoC和外设适配代码和流程。在操作系统上适配外设除了CPU以外SoC上也需要做很多的工作，包括地址空间分配，中断号分配，时钟分配等等。因此我校的系统展示基本仅限于裸机程序的展示，而且都没有使用中断，而采用轮询的方法避开SoC的工作。</li></ul>



<p>今年我们的队伍是临时组建的，也就是说组队前基本互不认识（起码我都不认识），所以并不是什么深谙嵌入式开发/熟悉CPU设计的人。我们所有人，包括我都是在组队完成以后才开始接触入门CPU设计的。</p>



<p>比赛结束以后我翻了一下团队赛的演示视频（是的，我也不知道为什么出现在b站上但不是官号）。2022年第六届龙芯杯了，就我个人的感觉而言，我觉得仍然没有队伍所做的工作能够超越哈利橙他们2019年第三届龙芯杯的工作。的确清华大学今年的参赛队伍设计了乱序的CPU性能能够超过nontrivials-mips，但是按照IPC的比例我觉得其实并没有很好的发挥出乱序处理器应有的能力，而他们的系统适配基本沿用2019年的工作。<s>这令我相信是神去了清华而不是清华培养出了神。</s></p>



<p>我们队伍今年的性能大约只有哈利橙他们的60%（因为没法直接对比），是因为流水cache出现了小概率bug，最后都没有调试出这个问题，因此只能提交主频低，IPC也不高的一个版本。如果流水dcache能够使用的话，我们的性能大约能够小幅超过他们。</p>



<h2 class="wp-block-heading">参赛过程</h2>



<p>以下是回顾参赛的过程，希望对我校或者其他参加龙芯杯的同学或者有志于从事CPU设计的同学有参考价值。</p>



<h3 class="wp-block-heading">语言选择</h3>



<p>我们队伍选择的开发语言是SystemVerilog，主要是考虑到我校的数字逻辑和组成原理都使用的是Verilog，而实际上2005年开始在IEEE的标准里Verilog其实就是SystemVerilog。相比于Verilog，SV主要的优点是有<strong>结构体</strong>和<strong>多维数组</strong>，在总线等信号很多但是较为固定的地方SV也有提供<strong>接口</strong>这一个概念供简化代码。</p>



<p>我校的其他队伍和龙芯杯参赛的其他队伍也都有使用一些其他的HDL，例如SpinalHDL和Chisel等。这些高级的硬件描述语言的工作方式通常是由编译器将高级语言编译成为Verilog然后再交由仿真或综合工具生成网表等。此类高级语言通常都能够解决Verilog系的接线和端口声明冗长的问题，有的语言还有更多的语法糖可以实现OOP等更加高级的代码精简方法。</p>



<p>对于参加龙芯杯使用的HDL的选择，其实并没有一个谁好谁坏，我把一些关键的区别总结在下面共有需要的人参考：</p>



<p>Verilog系语言的优势：</p>



<ul class="wp-block-list"><li>有完整原生的全套工具链支持，包括仿真、综合、实现、编辑&amp;代码补全等，虽然并不一定很好用但是一定是有的。</li><li>直接了当，生成的电路与代码描述的一致。</li><li>相当多开源项目/开源IP核都使用Verilog。</li></ul>



<p>Verilog系语言的劣势：</p>



<ul class="wp-block-list"><li>代码冗余，开发效率可能因此降低，开发者不能讲精力集中在功能设计上。</li><li>二义性语法多，如果出现未定义的语法，不同的工具可能行为不一致，非常影响开发。</li><li>测试testbench编写麻烦。</li></ul>



<p>Chisel类高级语言的优势：</p>



<ul class="wp-block-list"><li>代码精简，开发效率高。</li><li>生成的Verilog保证没有二义性。</li><li>可以使用高级语言编写测试样例，较为方便</li></ul>



<p>Chisel类高级语言的劣势：</p>



<ul class="wp-block-list"><li>通常较新，工具链支持可能不足。</li><li>可能存在编译器bug，而出现问题的时候由于生成的Verilog可读性较差，debug难度较大。</li><li>需要学习一门新的语言。</li></ul>



<p>当然有些优势/劣势并不一定符合你的实际情况，例如你熟悉java/scala语法，那可能Chisel等语言也很容易上手，所以对于语言的选择只要符合自己/自己队伍的情况就可以了，并没有必然的好坏。</p>



<h3 class="wp-block-heading">前期准备</h3>



<p>我校的龙芯杯宣传和备赛都开始得很早，在2021年秋季的数字逻辑设计实验课结束以后就开始了宣传和备赛，我猜测应该在大部分学校里面属于比较早的。我校2022年龙芯杯备赛的时候一开始来了11支队伍，30几个个人参赛，是的你没看错，我也觉得难以置信，而且往年也就是两支队伍+几个人。当然最后提交作品的大概就剩下3支mips+2两支la+8个个人左右（个人赛不太清楚）。</p>



<p>前期的话主要是以学习为主，切忌好高骛远，有的同学在开始动手之前先看了《超标量处理器设计》，然后打算先干一个乱序CPU，然后就没有然后了。我们队伍基本上到2022年3月都是在摸鱼/跟着《自己动手写CPU》抄代码，其他东西的准备也是几乎没有。直到我3月3号提交了ASC初赛的proposal，大家才开始干活（<s>才开始大力push队友</s>）。</p>



<p>4月初我们就分好了工，我负责分支预测，其他队友分别负责主流水线，DCache和总线。于是4月份我就在调研和复现，最终在4月底的时候完成了TAGE的RTL复现。五一假期的时候我们申请了一个教师做队伍内部交流（<s>首次面基</s>），那个时候DCache是说有bug还没有对接，axi已经有一个可用版本，主流水线的单发射版本已经可以进行功能测试了，双发射才开始改。</p>



<p>5月份我主要和主流水线的队友一起适配chiplab和修bug，<s>其他队友可能在摸鱼</s>。5月底的时候就基本完成了双发射CPU的代码编写，<s>这个大约就算是完成了前期的工作吧</s>。</p>



<p>接下来就是<strong>debug</strong>，<strong>debug</strong>和<strong>debug</strong>。</p>



<h3 class="wp-block-heading">工作量和难点</h3>



<p>对于参加LA赛道的工作量，我觉得最大的其实是适配系统。</p>



<p>虽然说支持Linux操作系统并不是两眼一抓瞎从头做起，官方的框架chiplab里面提供了一个十分完善的单发射五级流水的实例CPU，能够稳定启动Linux，因此理论上只要对着那个CPU增量开发就能够保证适配Linux。但是那个CPU充满了assign式的电路级别写法，十分晦涩难懂，所以我们选择了从头开始编写。然而对于完全没有CPU设计和系统适配经验的队伍来说，就算是有完整的实现可以参考，但是这个部分依然是工作量很大的部分。因为系统软件大量依赖特权资源，而这些特权资源其实并没有一个很好的仿真验证负载来测试，因此对于特权部分的调试我们队伍基本上只能依赖在仿真启动Linux，当然这个耗时久比较的久了。另外的是启动Linux操作系统有上亿条指令（没错，是真的上亿），这个对CPU的正确性要求很高，即便是有了6000万条的随机验证序列，chiplab的仿真SoC中也做了各种随机因素的引入，但是有的问题依然不能够在除了Linux以外的负载上复现。</p>



<p>其次就是DCache的一致性问题。LoongArch其实是一个软件维护一致性的ISA，相比于x86这种硬件维护的已经是大幅降低了硬件的设计难度，但是由于实际操作系统中访存指令的类型混杂，访存和一致性维护指令的混杂，使得DCache数据一致性的设计尤为重要但又十分难调试。我们队伍最终就是栽在了流水DCache的一致性维护上，导致Linux的启动并不稳定，有较低概率出现问题，只好提交一个旧的并不是最优的一个版本。</p>



<p>接下来才是CPU微架构的设计，或者其他的特性的设计。哪怕是设计一个乱序四发射的CPU，我相信除了访存部分，其他部分从设计到编写代码到调试完成，基本都可以在两个人月内完成。而访存部分的调试可能需要花同样多的时间。我们的队伍是以启动系统作为最高优先级，因此微架构上的设计可以说十分简陋和落后，仅仅是一个10级静态顺序双发射。</p>



<h3 class="wp-block-heading">队伍时间节点</h3>



<p>以下是我们队伍2022年参加龙芯杯的一些重要时间节点：</p>



<ul class="wp-block-list"><li>2021年12月：组队</li><li>2022年1月-3月：学习阶段，大家各自完成大部分的《自己动手写CPU》的内容</li><li>2022年4月：分工完成，我负责分支预测和前端，我完成TAGE的论文复现</li><li>2022年5月：完成主流水线并接入chiplab进行功能测试</li><li>2022年6月11日：首次仿真启动Linux，无Cache</li><li>2022年6月下旬：根据随机验证修复bug</li><li>2022年7月初：遇到Verilog二义性问题，花费全队一个星期debug</li><li>2022年7月12日：首次板上启动Linux，有ICache</li><li>2022年7月底：前端分支预测、写直通法dcache完成</li><li>2022年8月6日：前端freeze，首个可以提交的版本完成</li><li>2022年8月7-14日：流水写回dcache调试，未成功</li><li>2022年8月14日：提交旧版无bug版本</li><li>2022年8月18日：最终提交系统展示和文档ppt等</li></ul>



<h2 class="wp-block-heading">感想</h2>



<p>其实原本我校还有另一支队伍也参加了LA赛道，他们使用了Chisel作为开发语言，开始准备的时间比我们还要早很多，寒假结束就已经有一个五级流水的核了。而且他们的微结构比我们先进很多，也使用了记分板的乱序方法（没有重命名），但是他们最终没有调试出Linux，而无缘决赛。说到这个，不得不提我们最终的作品是一个静态10级流水的顺序双发射CPU，对于这个10级静态，我们队里的人和学长/老师都觉得十分浪费。访存所需的逻辑很多，导致基本上需要4-5级的流水来完成，但是作为一个静态的流水线，除了访存以外的指令在后面的访存流水中完全没有事情做，非常的浪费。而且我们的load to use问题解决的也不是很好，采取了保守的方式，而本可以采用更激进而高效的方法。但是其实我认为吧，LA这个赛道如果是第一年参赛，首要目标还是启动Linux吧，这也是我们队伍从一开始的原则。当然这导致我们的微架构没有充足的时间设计，只能草草沿用传统的静态长流水。</p>



<p>今年的龙芯杯也算是最终落下帷幕了，已经得知有乱序队伍今年Linux启动不稳定，打算明年继续了。</p>



<h2 class="wp-block-heading">成绩</h2>



<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" src="https://blog.eastonman.com/wp-content/uploads/2022/08/Screen-Shot-2022-08-21-at-10.18.55-1024x578.png" alt="" class="wp-image-1117" width="637" height="360" srcset="https://blog.eastonman.com/wp-content/uploads/2022/08/Screen-Shot-2022-08-21-at-10.18.55-1024x578.png 1024w, https://blog.eastonman.com/wp-content/uploads/2022/08/Screen-Shot-2022-08-21-at-10.18.55-300x169.png 300w, https://blog.eastonman.com/wp-content/uploads/2022/08/Screen-Shot-2022-08-21-at-10.18.55-768x433.png 768w, https://blog.eastonman.com/wp-content/uploads/2022/08/Screen-Shot-2022-08-21-at-10.18.55.png 1193w" sizes="(max-width: 637px) 100vw, 637px" /></figure>



<p>最终我们拿到了一等奖。对于这个成绩我还是十分满意的（<s>好像没有更满意的成绩了</s>）。其实也算有点意料之中吧，因为提交前看到LA群里的其他队伍的情况，很多队伍都还在调试Linux，估计是没有时间好好调性能了，我们虽然最终的流水DCache版本没有敢提交，但是前端是有仔细调优过的，性能也算中规中矩吧。</p>



<h2 class="wp-block-heading">后续</h2>



<p>项目代码整理完毕以后应该会开源出来。</p>



<p>接下来我也会撰写几篇文章描述我们的前端设计和TAGE预测器的详细复现内容，可以关注本博客或者订阅RSS。</p>The post <a href="https://blog.eastonman.com/blog/2022/08/nscscc2022/">NSCSCC2022 龙芯杯参赛总结</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></content:encoded>
					
					<wfw:commentRss>https://blog.eastonman.com/blog/2022/08/nscscc2022/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>现代处理器结构</title>
		<link>https://blog.eastonman.com/blog/2021/05/modern-processor/</link>
					<comments>https://blog.eastonman.com/blog/2021/05/modern-processor/#comments</comments>
		
		<dc:creator><![CDATA[Easton Man]]></dc:creator>
		<pubDate>Mon, 17 May 2021 15:10:04 +0000</pubDate>
				<category><![CDATA[技术]]></category>
		<category><![CDATA[计算机系统]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[体系结构]]></category>
		<category><![CDATA[处理器]]></category>
		<guid isPermaLink="false">https://blog.eastonman.com/?p=639</guid>

					<description><![CDATA[<p>一个简短的、直接的现代处理器微架构设计介绍。</p>
The post <a href="https://blog.eastonman.com/blog/2021/05/modern-processor/">现代处理器结构</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></description>
										<content:encoded><![CDATA[<p class="wpwc-reading-time">预计阅读时间： 36 分钟</p>
<p>一个简短的、直接的现代处理器微架构设计介绍。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>当今的机器实在是非常原始，它们只能理解区区几个简单的指令，比如“向左”、“向右”和“生产汽车”。</p><p><em>Today&#8217;s robots are very primitive, capable of understanding only a</em> <em>few simple instructions such as &#8216;go left&#8217;, &#8216;go right&#8217; and &#8216;build car&#8217;.</em></p><cite><a href="https://www.azquotes.com/quote/1403947" target="_blank" rel="noreferrer noopener">John Thomas Sladek</a></cite></blockquote>



<p><strong>警告：</strong>本文旨在以非正式和风趣的语言讲述严肃的科学。</p>



<p><strong>警告2：</strong>长文！预计阅读时间36分钟。</p>



<p>本文主要向计算机专业的低年级学生和对现代处理器结构感兴趣的读者介绍有关处理器微架构的一些概念。具体来说，有以下几个方面：</p>



<ul class="wp-block-list"><li>流水线（超标量执行、乱序执行、超长字指令、分支预测）</li><li>多核和超线程（同步超线程 SMT）</li><li>SIMD指令集（SSE、AVS、NEON、SVE）</li><li>缓存和缓存机制</li></ul>



<p>听起来内容很深奥，<strong>但是，不要害怕！</strong>这篇文章将带你快速地了解这些看似只有处理器设计从业者或者是体系结构专家才能了解的东西。也许你很快就可以和你的同学/朋友吹牛了<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/1f923.png" alt="🤣" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>



<h2 class="wp-block-heading">超10G！</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>超10G！全人类感谢你！</p><cite>——某up</cite></blockquote>



<p>主频越高，CPU性能越好，这似乎是很多人的误区（不包括以上引用的up主），但是，从上古时期开始，CPU的性能和主频就没有直接关系。那么这个刻板印象是从哪里来的呢？<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/1f914.png" alt="🤔" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>让我们来看看一些上古（上世纪90年代末）的处理器数据&#8230;</p>



<figure id="megahertztable" class="wp-block-table is-style-stripes"><table><tbody><tr><td><strong>主频</strong></td><td><strong>型号</strong></td><td><strong>SPECint95</strong></td><td><strong>SPECfp95</strong></td></tr><tr><td>195 MHz</td><td>MIPS R10000</td><td>11.0</td><td>17.0</td></tr><tr><td>400 MHz</td><td>Alpha 21164</td><td>12.3</td><td>17.2</td></tr><tr><td>300 MHz</td><td>UltraSPARC</td><td>12.1</td><td>15.5</td></tr><tr><td>300 MHz</td><td>Pentium II</td><td>11.6</td><td>8.8</td></tr><tr><td>300 MHz</td><td>PowerPC G3</td><td>14.8</td><td>11.4</td></tr><tr><td>135 MHz</td><td>POWER2</td><td>6.2</td><td>17.6</td></tr></tbody></table><figcaption>1997年的处理器性能</figcaption></figure>



<p>SPEC是一个当年常用的性能测试工具，乔布斯在宣布苹果的Macbook由IBM PowerPC平台转向Intel的酷睿平台的时候就在发布会上展示了SPEC的性能提升。</p>



<p>从表中可以看到，为什么300MHz的处理器有这么不同的性能差异？为什么低主频的CPU反而吊打高主频的？</p>



<p>什么？你说这都是上古的数据？那就来一个最近的：</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="903" height="340" src="https://blog.eastonman.com/wp-content/uploads/2021/05/image-1.png" alt="" class="wp-image-651" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/image-1.png 903w, https://blog.eastonman.com/wp-content/uploads/2021/05/image-1-300x113.png 300w, https://blog.eastonman.com/wp-content/uploads/2021/05/image-1-768x289.png 768w" sizes="(max-width: 903px) 100vw, 903px" /></figure>



<p>超到一半人类感谢你（5GHz）的Intel i9-9900K居然被M1吊打？</p>



<p>是的，你没有看错，这就说明显然除了主频以外还有一些什么东西，那就是——</p>



<h2 class="wp-block-heading">流水线和指令级并行</h2>



<p>指令在处理器中是一个接一个的执行的，对吗？不完全对。这样的说法可能是直观的，但是并不是事实，实际上，从80年代开始，CPU就不再是完全顺序执行每个指令了。现代处理器可以同时执行不同指令的不同阶段，甚至有的处理器也可以完全同时地执行多个指令。</p>



<p>让我们来看一看一个简单的四级流水线是怎么构成的。指令被分成四个部分：<strong>取指、译码、执行和写回</strong>。</p>



<p>如果CPU完全顺序执行，那么每条指令需要花费4个周期才能执行完毕，IPC=0.25（Instruction per cycle）。当然，古老一点的时期更喜欢使用CPI，因为当时的处理器普遍不能做到每周期执行一条指令。但是现在时代变了，你能接触到的任何一个桌面级处理器都可以在一个周期内执行一条、两条甚至是三条指令。</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="384" height="145" src="https://blog.eastonman.com/wp-content/uploads/2021/05/sequential2.png" alt="" class="wp-image-654" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/sequential2.png 384w, https://blog.eastonman.com/wp-content/uploads/2021/05/sequential2-300x113.png 300w" sizes="(max-width: 384px) 100vw, 384px" /><figcaption>顺序执行的处理器</figcaption></figure></div>



<p>正如你所看到的，实际上CPU内负责运算的组件（ALU）十分的悠闲，甚至只有25%的时间在干活。什么？怎么压榨ALU？我看你很有资本家的天赋嘛&#8230;</p>



<p>好吧，现代处理器确实有手段压榨这些ALU（对，现代处理器也不止一个ALU）。一个很符合直觉的想法就是既然大部分的阶段CPU都不是完全占用的，那么将这些阶段重叠起来就好了。确实，现代处理器就是这么干的。</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="384" height="145" src="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelined2.png" alt="" class="wp-image-660" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelined2.png 384w, https://blog.eastonman.com/wp-content/uploads/2021/05/pipelined2-300x113.png 300w" sizes="(max-width: 384px) 100vw, 384px" /><figcaption>流水线执行的处理器</figcaption></figure></div>



<p>现在我们的处理器大多数时候一个周期可以执行一条指令了，看起来不错！这已经是在没有增加主频的情况下达到四倍的加速了。</p>



<p>从硬件的角度来看，每级流水线都是由该级的逻辑模块构成的，CPU时钟就像一个水泵，每次把信号（或者也可以说是数据）从一级泵到下一级，就像这样：</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="380" height="96" src="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedmicroarch2.png" alt="" class="wp-image-661" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedmicroarch2.png 380w, https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedmicroarch2-300x76.png 300w" sizes="(max-width: 380px) 100vw, 380px" /><figcaption>流水线微架构</figcaption></figure></div>



<p>事实上，现代处理器除了以上这样简单的结构，首先还有很多额外的ALU，比如整数乘法、加法、位运算、浮点数的各种运算等等，几乎每种常用的运算都有至少一个ALU。其次，如果前一条指令的结果就是下一条指令的操作数，那么为什么还要把数据写回寄存器呢？因此就出现了Bypass（前递）通路，用于在这种情况下直接将数据重新送到运算器的输入端口。综合起来，详细一点的流水线微架构应该长这样：</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="390" height="92" src="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedfunctionalunits2.png" alt="" class="wp-image-665" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedfunctionalunits2.png 390w, https://blog.eastonman.com/wp-content/uploads/2021/05/pipelinedfunctionalunits2-300x71.png 300w" sizes="(max-width: 390px) 100vw, 390px" /><figcaption>详细的流水线微架构</figcaption></figure></div>



<h2 class="wp-block-heading">更深的流水线——超级流水线！</h2>



<p>自从CPU主频由于某种原因（某种神秘力量？<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/1f608.png" alt="😈" class="wp-smiley" style="height: 1em; max-height: 1em;" />）很多年没有大的进步以来（对的，超频榜第一还是AMD的推土机架构CPU），流水线的设计几乎成为了CPU厂商的竞赛主场。加深的流水线首先可以继续增大实际的IPC（理论上限仍是1），其次可以避免流水线对时序的影响。这与晶体管的特性有关，感兴趣的读者可以上网搜一搜多级流水线结构为什么会影响时序和最终综合出的主频。</p>



<p>在2000-2010年间，这种竞赛达到了最高峰，那时候的处理器甚至可以有高达31级的流水线。但是超深的流水线带来的是结构上的复杂和显著增大的动态调度模块设计难度，因此，从那以后就没有再出现过使用这么多级流水线的CPU了。作为对比，目前（2021年）的处理器多半视应用场景的不同采用10-20级不等的流水线。</p>



<p>x86和其它CISC处理器通常有着更深的流水线，因为他们在取指和译码阶段有数倍的任务要做，所以通常使用更深的流水线来避免这一阶段带来的性能损耗。</p>



<h2 class="wp-block-heading">多发射——超标量处理器</h2>



<p>既然整数的运算器和浮点数的运算器以及其它的的一些ALU互相之间都是没有依赖的，自己做自己的事情，那为什么不进一步压榨它们，让他们尽可能地一起忙起来呢？这就出现了多发射和超标量处理器。多发射的意思是处理器每个周期可以“发射”多于一条的指令，比如浮点运算和整数运算的指令就可以同时执行且互不干扰。为了完成这一点，取指和译码阶段的逻辑必须加强，这就出现了一个叫做<strong>调度器</strong>或者<strong>分发器</strong>的结构，就像这样：</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="437" height="335" src="https://blog.eastonman.com/wp-content/uploads/2021/05/superscalarmicroarch2.png" alt="" class="wp-image-685" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/superscalarmicroarch2.png 437w, https://blog.eastonman.com/wp-content/uploads/2021/05/superscalarmicroarch2-300x230.png 300w" sizes="(max-width: 437px) 100vw, 437px" /><figcaption>超标量处理器微架构</figcaption></figure></div>



<p>或者我们来看一张实际的Intel Skylake架构的调度器，图中红圈的就是负责每周期“发射”指令的调度器。</p>



<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img decoding="async" src="https://blog.eastonman.com/wp-content/uploads/2021/05/image-2.png" alt="" class="wp-image-684" width="-127" height="-87" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/image-2.png 792w, https://blog.eastonman.com/wp-content/uploads/2021/05/image-2-300x207.png 300w, https://blog.eastonman.com/wp-content/uploads/2021/05/image-2-768x529.png 768w" sizes="(max-width: 792px) 100vw, 792px" /><figcaption>Skylake 调度器</figcaption></figure></div>



<p>当然，现在不同的运算有了不同的“数据通路”，经过的运算器也不同。因为不同的运算器内部可能也分不同的执行阶段，于是不同的指令也就有了不同的流水线深度：简单的指令执行得快一些，复杂的指令执行得慢一些，这样可以降低简单指令的<strong>延迟</strong>（我们很快就会涉及到）。某些指令（比如除法）可能相当耗时，可能需要数十个周期才能返回，因此在编译器设计中，这些因素就变得格外重要了。有兴趣的读者可以思考<strong>梅森素数</strong>在这里的妙用。</p>



<p>超标量处理器中指令流可能是这个样子的：</p>



<figure class="wp-block-gallery alignwide columns-2 is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex"><ul class="blocks-gallery-grid"><li class="blocks-gallery-item"><figure><img loading="lazy" decoding="async" width="384" height="145" src="https://blog.eastonman.com/wp-content/uploads/2021/05/superscalar2.png" alt="" data-id="690" data-full-url="https://blog.eastonman.com/wp-content/uploads/2021/05/superscalar2.png" data-link="https://blog.eastonman.com/?attachment_id=690" class="wp-image-690" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/superscalar2.png 384w, https://blog.eastonman.com/wp-content/uploads/2021/05/superscalar2-300x113.png 300w" sizes="(max-width: 384px) 100vw, 384px" /></figure></li><li class="blocks-gallery-item"><figure><img loading="lazy" decoding="async" width="384" height="145" src="https://blog.eastonman.com/wp-content/uploads/2021/05/superpipelinedsuperscalar2.png" alt="" data-id="689" data-full-url="https://blog.eastonman.com/wp-content/uploads/2021/05/superpipelinedsuperscalar2.png" data-link="https://blog.eastonman.com/?attachment_id=689" class="wp-image-689" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/superpipelinedsuperscalar2.png 384w, https://blog.eastonman.com/wp-content/uploads/2021/05/superpipelinedsuperscalar2-300x113.png 300w" sizes="(max-width: 384px) 100vw, 384px" /></figure></li></ul></figure>



<p>现代处理器一般都有相当多的发射端口，比如上面提到的Intel Skylake是八发射的结构，苹果的M1也是八发射的，ARM最新发布的N1则是16发射的处理器。</p>



<h2 class="wp-block-heading">显式并行——超长指令集</h2>



<p>当兼容性不成问题的时候（很不幸，很少有这种时候），我们可以设计一种指令集，显式地指出某些指令是可以被并行执行的，这样就可以避免在译码时进行繁复的依赖检验。这样理论上可以使处理器的硬件设计变得更加简单、小巧，也更容易取得更高的主频。</p>



<p>这种类型的指令集中，“指令”实际上是“一组子指令”，这使得它们拥有非常多的指令，进而每个指令都很长，例如128bits，这就是<strong>超长指令集（VLIW）</strong>这个名字的来源。</p>



<p>超长指令集处理器的指令流和超标量处理器的指令流十分的类似，只是省去了繁杂的取指和译码阶段，像这样：</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="384" height="145" src="https://blog.eastonman.com/wp-content/uploads/2021/05/vliw2.png" alt="" class="wp-image-697" srcset="https://blog.eastonman.com/wp-content/uploads/2021/05/vliw2.png 384w, https://blog.eastonman.com/wp-content/uploads/2021/05/vliw2-300x113.png 300w" sizes="(max-width: 384px) 100vw, 384px" /><figcaption>VLIW指令流</figcaption></figure></div>



<p>除了硬件结构，超长指令集处理器和超标量处理器十分的相似，尤其是从编译器的角度来看（我们很快也会谈到）。</p>



<p>但是，超长指令集处理器通常被设计成<strong>不检查依赖</strong>的，这就使得它们必须依赖编译器的魔法才能保证结果的正确，而且，如果发生了缓存缺失，那它们不得不整个处理器都停下来，而不是仅仅停止遇到缓存缺失问题的那一条指令。编译器会在指令之间插入“nops”（no operations）——即空指令，以保证有数据依赖的指令能够正确地执行。这无疑增加了编译器的设计难度和编译所需的时间，但是这同时节省了宝贵的处理器片上资源，通常也能有略好的性能。</p>



<p>现在仍在生产的现代处理器中<strong>并没有</strong>采用VLIW指令集的处理器。Intel曾经大力推行过的IA-64架构就是一个超长指令集（VLIW）架构，由此设计的“Itanium”系列处理器在当时也被认为是x86的继承者，但是由于市场对这个新架构并不感冒，所以最终这个系列没有发展下去。现代硬件加速最火热的方向是GPU，其实GPU也可以看作是一种VLIW架构的的处理器，只不过它将VLIW架构更进一步，使用“核函数”代替指令，大大增加了这种体系结构的可扩展性，有兴趣的读者也可以了解相关方面的内容。</p>



<h2 class="wp-block-heading">数据依赖和延迟</h2>



<p>我们在流水线和多发射这条路上能走多远？既然多发射和多级流水线这么好，那为什么不做出50级流水线、30发射的处理器？我们来讨论以下两条指令：</p>



<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">a = b * c;
d = a + 1;</pre>



<p>第二条指令<strong>依赖于</strong>第一条指令——处理器在完成前一条指令之前无法执行下一条指令。这是一个很严重的问题，这样一来，多发射就没有用武之地了，因为无论你制造出了多少发射的处理器，这两条指令还是只能顺序地执行（除去取指等部分）。有关依赖和消除的问题我们会在后面讨论。</p>



<p>如果第一条指令是一个简单的加法指令，那么加法器在执行完毕后可以通过Bypass通路（前递）将数据传回ALU的输入端口并继续计算，这样流水线才可以正常工作。但是很不幸，第一条指令是一个需要多周期才能完成的乘法（目前的大多数CPU没有使用单周期乘法，因为复杂的逻辑通常会损害主频），这样的话，处理器为了等待第一条指令完成就不得不往流水线中加入若干“气泡”也就是类似于“nops”的指令来保证运算的正确性。</p>



<p>一条指令到达运算器的输入端口和执行结果可用之间需要耗费的CPU周期称为<strong>指令的延迟</strong>。流水线越深，指令的延迟就越高，所以更深的流水线如果无法有效地填满，那么结果只能是很高的指令延迟而无益于处理器的性能。</p>



<p>从编译器的角度（考虑了Bypass，硬件工程师口中的延迟通常不包括Bypass），现代处理器的指令延迟通常是：整数乘加和位操作1周期，浮点数乘加2-6周期不等，sincos这种复杂指令10+周期，最后是可能长达30-50周期的除法。</p>



<p>访存操作的延迟也是一个很麻烦的问题，因为它们通常是每条指令最开始执行的步骤，这使得它们造成的延迟很难用别的方式补偿。除此以外，他们的延迟也很难预测，因为延迟很大程度上取决于缓存是否命中，而缓存是动态调度的（我们很快也会讲到）。</p>



<h2 class="wp-block-heading">分支和分支预测</h2>



<p>另外一个流水线的重要问题就是分支，我们来看一看接下来的一段程序：</p>



<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">if (a > 7) {
    b = c;
} else {
    b = d;
}</pre>



<p>编译成的汇编程序将会是这样：</p>



<pre class="EnlighterJSRAW" data-enlighter-language="asm" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">    cmp a, 7    ; a > 7 ?
    ble L1
    mov c, b    ; b = c
    br L2
L1: mov d, b    ; b = d
L2: ...</pre>



<p>现在想象一个流水线处理器来执行这一段程序。当处理器执行到第二行，也就是第二行的跳转命令到达处理器的执行器的时候，它肯定已经把后面的所有指令都提前从内存中取出存并完成了译码工作了。但是，究竟跳转的是哪一条指令？是3，4行还是第5行？在跳转命令到达执行器之前我们并不知道应该跳转到哪里。在一个深流水线的处理器中，似乎不得不停下来等待这个跳转命令，再重新往流水线中填入新的指令。这当然是不可接受的，程序中，尤其是循环时，分支跳转的命令占比很大，如果每次都等待这条命令的完成，那么我们的流水线就不得不经常地暂停，而我们通过流水线取得的性能提升也将不复存在。</p>



<p><strong>于是现代处理器会做出猜测</strong>。什么？处理器竟然靠猜，我还以为发达的处理器设计行业能给出更好的解决方案呢！先不要着急，实际上程序中分支的跳转是有规律的，现代处理器分支预测的准确度通常能达到99%以上（虽然分支预测也是Intel的spectre和meltdown漏洞的来源<img src="https://s.w.org/images/core/emoji/16.0.1/72x72/1f47f.png" alt="👿" class="wp-smiley" style="height: 1em; max-height: 1em;" />）。</p>



<p><strong>处理器会沿着预测的分支执行下去</strong>，这样一来，我们的处理器就可以保持流水线和运算器的占用，并高速地执行下去。当然，执行的结果还不能作为最终的结果，只有在分支跳转命令的结果出来以后，预测正确的结果才会被写回（commit或retire）。那猜错了怎么办？那处理器也没有好的办法，只能重新从另一个分支开始进入流水线，在高度流水化的现代处理器里，分支预测错误的代价（<strong>分支预测的错误惩罚</strong>）是相当高昂的，通常会达到数十个CPU周期。</p>



<p>这里的关键在于，<strong>处理器如何做出预测</strong>。通常而言，分支预测分为静态和动态两种。</p>



<p><strong>静态分支预测即处理器做出的猜测与运行时的状态无关</strong>，而对跳转的优化由编译器完成。静态预测通常有一律跳或者往后跳预测不跳，往前跳转则预测跳。后者通常效果更好，原因是循环中一般会有大量向前的跳转指令。</p>



<p><strong>动态分支预测则是根据跳转指令的历史决定是否跳转</strong>。一个最简单的动态分支预测器就是<strong>2位饱和计数器</strong>，它是一个四个状态的状态机，特点是只有连续两次预测错误才会更改预测方向。它已经能在大部分场合下取得90%以上的预测正确率。</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img decoding="async" src="https://blog.eastonman.com/wp-content/uploads/2021/05/Branch_prediction_2bit_saturating_counter-dia.svg" alt="" class="wp-image-722"/><figcaption>2位饱和计数器 Author: Afog CC BY-SA 3.0</figcaption></figure></div>



<p>这种预测器在交替出现跳和不跳的分支指令时表现不佳，于是人们又发明了n级自适应分支预测器，它的原理与2位饱和计数器类似，不过它能够记住过去n次的历史，在重复的跳转模式中表现优异。</p>



<p>不幸的是，分支预测是各个CPU厂商的核心竞争力之一，大多数优秀的分支预测技术也是重要的商业机密，于是在这个方面并没有太多可以深入的。Cloudflare最近发布了一篇<a href="https://blog.cloudflare.com/branch-predictor/" target="_blank" rel="noreferrer noopener" title="https://blog.cloudflare.com/branch-predictor/">博文</a>深入测试了x86和ARM的M1上分支预测器的特征，有兴趣的读者可以看看。</p>



<h2 class="wp-block-heading">去除分支语句</h2>



<p>由于分支这实在是处理器不喜欢的东西，于是人们便想要尽量减少分支语句的使用。而以下这种情况很常见，在求取最大最小值或者是条件赋值的时候经常被使用（第1，2行）：</p>



<pre class="EnlighterJSRAW" data-enlighter-language="asm" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">cmp a, 7       ; a > 7 ?
mov c, b       ; b = c
cmovle d, b    ; if le, then b = d</pre>



<p>于是人们就设计出了第3行这种指令。这样的指令是在特定条件下将d的值赋给b，而并不引入分支，只需要在条件不满足的时候不进行写回（commit/retire）就可以了。这种指令被称为<strong>条件转移指令</strong>，编译器中经常使用这种trick来避免进行跳转。</p>



<p>我们古老的x86架构一开始并不支持条件转移指令，MIPS、SPARC也不例外，而Alpha架构从设计之初就考虑了这类指令（RISC-V这样的新指令集当然也有）。ARM则是第一个采用全可预测指令的指令集，这一点很有趣，因为早期的ARM处理器通常采用很浅的流水线，分支预测的惩罚很小。</p>



<h2 class="wp-block-heading">指令调度、寄存器重命名和乱序执行</h2>



<p>如果分支和长延迟的指令会带来流水线气泡，那么能不能把这些气泡占据的处理器时间用来干有用的事情呢？为了达到这个目的，就需要引入<strong>乱序执行</strong>。乱序执行允许处理器将部分指令的顺序打乱，在执行长延迟指令的同时执行一些别的指令。</p>



<p>历史上有两种方式来达到乱序执行的目的：软件的和硬件的。</p>



<p>软件的途径很好理解，就是通过编译器与体系结构的强耦合，在编译阶段就生成好无相互依赖，易于处理器调度的指令。在编译阶段进行指令重排又被称为<strong>静态指令调度</strong>，优点是软件实现可以更灵活（众所周知，软件什么都能干），通常软件也可以有足够的存储空间来分析整个程序，因此可以获得更优的指令排布。当然缺点也是显而易见的，由于编译器需要深入地了解体系结构相关的信息，如指令延迟和分支预测惩罚等，对可移植性造成了很大的困难。因此现代处理器更加常用的是硬件方式。</p>



<p>硬件方式主要是通过<strong>寄存器重命名</strong>来消除读—读和写—写假依赖。寄存器重命名就是对不同指令调用的相同寄存器使用不同的物理硬件存储，在写回阶段再对这些指令和寄存器进行排序，这样这些假依赖就不再是产生流水线气泡的原因了。注意，写—读依赖是真正的数据依赖，虽然像前递这样的技术可以降低延迟，但是并没有能够解决这种依赖的办法。现代处理器中也并非仅仅只有如16个通用寄存器和32个浮点寄存器等等，通常都有成百上千的物理寄存器在CPU的片上。寄存器重命名的算法最有名的便是Tomasulo算法，有兴趣的读者可以搜索一下。</p>



<p>硬件方式的优点在于降低了编译器的体系结构耦合度，提高了软件编写的便捷性，通常硬件乱序执行的效果也不必软件的差。而缺点在于依赖分析和寄存器重命名都需要耗费宝贵的片上空间和电力，但对于性能的提升却没有相应的大。因此，在一些更加关注低功耗和成本的CPU中，会采用顺序执行，如ARM的低功耗产品线，Intel Atom等。</p>



<h2 class="wp-block-heading">多核和超线程</h2>



<p>我们之前讨论了各种指令集并行的方法，而很多时候它们的效果并不是很好，因为相当一部分的程序没有提供细粒度的并行。因此，制造更“宽”更“深”的处理器效果相当有限。</p>



<p>但是CPU的设计者又想了，如果本程序中没有足够并行的没有相互依赖的指令，那么不同的程序之间肯定是没有数据依赖的（指令级数据依赖），那么在同一个物理核心上同时运行两个线程，互相填补流水线的空缺，岂不美哉？这就叫做<strong>同步多线程（SMT）</strong>，它提供了线程级的并行化。这种技术对于CPU以外的世界来说是透明的，就仿佛真的CPU数量多了一倍似的，因此现在人们也常说虚拟核心。</p>



<p>从硬件角度来说，同步多线程的实现需要将所有与运行状态有关的结构数量都翻倍，比如寄存器，PC计数器，MMU和TLB等等。幸运的是，这些结构并不是CPU的主要部分，最复杂的译码和分发器，运算器和缓存都是在两个线程之间共享的。</p>



<p>当然，真实的性能不可能翻倍，理论上限还是取决于运算器的数量，同步多线程只是能够将运算器更好地利用而已。因此在例如游戏画面生成这样地并行度本来就很高地任务中，SMT几乎没有任何地效果，反而因为偶尔地线程切换而带来一定地性能损失。</p>



<p>SMT处理器的指令流看起来大概是这样的：</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img decoding="async" src="https://blog.eastonman.com/wp-content/uploads/2021/05/smt2.svg" alt="" class="wp-image-769"/><figcaption>SMT处理器的指令流</figcaption></figure></div>



<p>太好了！现在我们有了填满哪些流水线气泡的方法了，而且绝无任何风险。所以，<strong>30发射的处理器我们来啦</strong>！对吗？不幸的是，不对。</p>



<p>虽然IBM曾在它的产品中使用过8线程的核心，但是很快我们就会看到，现代处理器的瓶颈早已不单单是CPU本身了，访存延迟和带宽都成为了更加迫切需要解决的问题。而同时使用8个MMU，8个PC，8个TLB怎么看也不是一个缓存友好的做法。因此，现在已经很少听到有多于一个核心两个线程的处理器了。</p>



<h2 class="wp-block-heading">数据并行——SIMD指令集</h2>



<p>除了指令级并行和超线程，在现代处理器中还有一种并行化的设计——数据并行化。数据并行化的思想是将同一条指令不同数据进行并行化，而不是对不同的指令进行并行。所以使用数据并行化的指令集通常又称为<strong>SIMD指令集</strong>（单指令多数据），也有称为<strong>向量指令集</strong>的。</p>



<p>在超级计算机和高性能计算领域，SIMD指令集被大量的使用，因为通常科学计算会处理极多的数据而对于每个数据的操作并不复杂，而且基本没有相互依赖性。在现代的个人计算机中，SIMD指令集也大量的存在，哪怕是最为廉价的手机中，SIMD指令集也有它的身影。</p>



<p>SIMD指令集的工作原理就像下图所示的那样：</p>



<div class="wp-block-image"><figure class="aligncenter"><img decoding="async" src="https://cdn.arstechnica.net/wp-content/uploads/archive/cpu/1q00/simd/figure6.gif" alt=""/><figcaption>SIMD指令原理</figcaption></figure></div>



<p>从硬件的角度来说，实现这样的并行化并不难，这就像每次都执行同一个指令的超标量处理器，CPU设计厂商唯一需要做的就是增大寄存器的容量而已。Intel在过去20年正在不断地增大可以并行的向量长度，从SSE的128bits到AVX512的512bits。而ARM从ARMv8a开始便从NEON的128bits飞跃到了SVE的2048bits长度，甚至还支持可变长度。</p>



<p>现代的x86-64处理器都支持SSE指令集，所以现在的编译器如果编译64位平台的目标文件，会自动的将SSE指令集加入用于优化。由于SIMD指令集发展迅速，不少指令的延迟甚至和传统的标量命令不相上下，而且SSE指令集也拥有操作单个操作数的指令，现代编译器在默认情况下对于单个浮点数的操作也会使用SSE指令集而不是使用传统的x87浮点指令。另外，几乎所有的体系结构都拥有自己的SIMD指令集。</p>



<p>像渲染画面或者科学计算这种简单而重复的任务很适合SIMD指令集，事实上，GPU的工作原理也与SIMD类似。但不幸的是，在大多数普通（没有经过特别的思考而写出的）代码中，SIMD指令集并不能被很好的应用。现代编译器全部都有不同程度的循环自动向量化（使用SIMD指令集），但是当程序的编写者没有很好地考虑数据的依赖性和内存布局（马上就会谈到）时，编译器往往不能对代码进行什么优化。而幸运的是，通常通过简单的改动，就可以编译器明白某些循环是可以被优化的，进而大幅的提升程序的运行速度。</p>



<h2 class="wp-block-heading">内存和内存墙</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>现代处理器实在是太快了，以至于它们大多数时候都在等待内存响应，而不是干正事。</p><cite>——佚名（忘记出处了）</cite></blockquote>



<p>自从计算机发明以来，处理器的发展速度远远超过存储的发展速度，以下是一个对比图：</p>



<div class="wp-block-image"><figure class="aligncenter"><img decoding="async" src="https://blog.royalsloth.eu/posts/2021/compiler-will-optimize-that-away/cpuMemoryPerformance_min.png" alt="Performance of processors and memory through the years 1980-2010"/><figcaption>处理器和内存速度对比</figcaption></figure></div>



<p>对于现代处理器来说，内存访问非常的昂贵。</p>



<ul class="wp-block-list"><li>在1980年，CPU访问一次内存通常只需要一个周期。</li><li>在2021年，CPU访问内存大约需要300-500个周期。</li></ul>



<p>当我们考虑到，我们在CPU上使用了那么多种手段来压榨运算器，使IPC能够突破1，这样来看，内存就更慢了。以下是一个表格，展示了如果处理器周期看作是1秒，访问其它的存储器需要的时间。</p>



<figure class="wp-block-table is-style-stripes"><table><tbody><tr><td>事件</td><td>延迟</td><td>等效延迟</td></tr><tr><td>CPU周期</td><td>0.2ns</td><td>1s</td></tr><tr><td>L1缓存访问</td><td>0.9ns</td><td>4s</td></tr><tr><td>L2缓存访问</td><td>3ns</td><td>15s</td></tr><tr><td>L3缓存访问</td><td>10ns</td><td>50s</td></tr><tr><td>内存访问</td><td>100ns</td><td>8分钟</td></tr><tr><td>固态硬盘访问</td><td>10-100us</td><td>15-150小时</td></tr><tr><td>机械硬盘访问</td><td>1-10ms</td><td>2-18月</td></tr></tbody></table><figcaption>等效延迟表</figcaption></figure>



<p>可以看到，现代CPU实在是太快了，程序编写者现在要比过去花费更多的精力来使他们的程序能够充分利用CPU的性能，而不是卡在内存操作上。</p>



<p>为了解决这个严重的问题，处理器设计者们也想出了办法，也就是上面的表格中已经出现了的缓存。80年代的CPU由于没有内存墙的问题，所以基本都没有设计缓存。而现代CPU通常而言都有高达三级的缓存（某些低功耗和移动端的CPU只有两级），理解这些缓存是怎么样工作的有利于程序设计者写出更快的程序。</p>



<h2 class="wp-block-heading">缓存</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>Cache这个词读音和Cash（现金）一样，而不是kay-sh、ca-shay或者Cake！</p><cite>我</cite></blockquote>



<p>现代处理器为了解决内存墙，使用多级的缓存来避免内存延迟的影响。一个典型的缓存结构是这样的：</p>



<figure class="wp-block-table is-style-stripes"><table><tbody><tr><th>等级</th><th>大小</th><th>延迟</th><th>物理位置</th></tr><tr><td>L1 cache</td><td>32 KB</td><td>4 cycles</td><td>每个核心内部</td></tr><tr><td>L2 cache</td><td>256 KB</td><td>12 cycles</td><td>每个die或每个核心</td></tr><tr><td>L3 cache</td><td>6 MB</td><td>~21 cycles</td><td>整个处理器共享或每die共享</td></tr><tr><td>RAM</td><td>4+ GB</td><td>~117 cycles</td><td>主板上的内存条上</td></tr></tbody></table><figcaption>缓存等级</figcaption></figure>



<p>令人高兴的是，现代处理器的缓存机制出奇的有效，L1缓存的命中率在大多数时候高达90%，这说明在大多数情况下内存访问的代价仅仅是几个周期而已。</p>



<p>缓存能够取得这么好的效果主要是因为程序具有很好的<strong>局部性</strong>。分为空间局部性和时间局部性。<strong>时间局部性</strong>说的是当程序访问一块内存时，很有可能接下来连续访问这一块内存。<strong>空间局部性</strong>是说程序访问一块内存，那么它很可能也许要访问附近的内存。为了利用好这样的局部性，内存中的数据是一块一块地从内存条上复制到缓存中的，这些快被称为<strong>缓存行</strong>。</p>



<p>从硬件的角度来说，缓存的工作原理和键值对表很类似。Key就是内存的地址，而Value则是对应的数据。事实上Key并不一定是完整的地址，通常是地址的高位一部分，而低位被用来索引缓存本身。用物理地址和虚拟地址来作为Key都是可行的，也各有好坏（就像所有的事情一样）。使用虚拟地址的缺点是进程的上下文切换需要刷新缓存，这非常昂贵。使用物理地址的缺点则是每次查缓存都需要先查页表。因此现代处理器通常采用虚拟地址作为缓存索引，而使用物理地址作为缓存行的标记。这样的方法又被称作“<strong>虚拟索引——物理标记</strong>”缓存。</p>



<h2 class="wp-block-heading">缓存冲突和关联度</h2>



<p>理想状态下，缓存应当保存最近最常使用的数据，但是对于CPU上的硬件缓存来说，有效维护使用状态的算法不能满足严格的延迟要求（例如Linux内核页缓存使用的LRU，有兴趣可以看我前面的文章<a href="https://blog.eastonman.com/blog/2021/04/linux-multi-lru/" title="https://blog.eastonman.com/blog/2021/04/linux-multi-lru/">Linux内核页面置换算法</a>），也难以用硬件实现，所以通常处理器使用简单的方法：<strong>每一个缓存行直接对应内存的几个位置</strong>。由于对应的几个位置不太可能同时访问，因此缓存是有效的。</p>



<p>这样的做法非常快速（本来缓存的设计目的就是这样的），但是当程序的确不断地来回访问同一个缓存行对应的不同位置的时候，缓存控制单元不得不反复从内存中装载数据，非常耗时，这被称为<strong>缓存冲突</strong>。解决办法就是不限制每一个内存区域只对应一个缓存行，而是对应几个，这个几个就被称作是<strong>缓存关联度</strong>。</p>



<p>当然，最快的方法是每个内存区域对应一个缓存行，这被叫做<strong>直接映射缓存</strong>，而使用4个关联度的缓存被称作<strong>4通道关联缓存</strong>。内存可以装载到任意一个缓存行的缓存叫做<strong>全关联缓存</strong>。使用关联的缓存带来的好处是大大减少了缓存冲突而保持查询延迟在一个合理的范围内。这也是现代处理器通常使用的方法。</p>



<h2 class="wp-block-heading">致谢</h2>



<p><a href="http://www.lighterra.com/papers/modernmicroprocessors/">Modern Microprocessors: A 90-Minute Guide!</a> 2016 By <a href="http://www.lighterra.com/jason/">Jason Robert Carey Patterson</a></p>



<p><a href="https://blog.royalsloth.eu/posts/the-compiler-will-optimize-that-away/" target="_blank" rel="noreferrer noopener" title="https://blog.royalsloth.eu/posts/the-compiler-will-optimize-that-away/">The compiler will optimize that away</a> 2021 By <a target="_blank" href="https://www.royalsloth.eu/" rel="noreferrer noopener">RoyalSloth</a></p>



<p><a href="http://www.anandtech.com/show/9582/intel-skylake-mobile-desktop-launch-architecture-analysis">The Intel Skylake Mobile and Desktop Launch, with Architecture Analysis</a> 2015 By&nbsp;<a href="https://www.anandtech.com" target="_blank" rel="noreferrer noopener" title="https://www.anandtech.com">AnandTech</a></p>



<p><a href="https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)" target="_blank" rel="noreferrer noopener" title="https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client)">Skylake(Client) Microarchitecture</a> 2020 By <a href="https://en.wikichip.org" target="_blank" rel="noreferrer noopener" title="https://en.wikichip.org">WikiChip</a></p>



<h2 class="wp-block-heading">深入阅读</h2>



<ul class="wp-block-list"><li>《深入理解计算机系统》（CSAPP)</li><li>《计算机体系结构：一种量化方法》</li><li><a href="https://www.bilibili.com/video/BV1Mo4y1Z7jb" target="_blank" rel="noreferrer noopener">吹牛还是真牛？苹果M1全网最硬核评测（上）</a> By 极客湾@bilibili.com</li></ul>



<p></p>The post <a href="https://blog.eastonman.com/blog/2021/05/modern-processor/">现代处理器结构</a> first appeared on <a href="https://blog.eastonman.com">Easton Man's Blog</a>.]]></content:encoded>
					
					<wfw:commentRss>https://blog.eastonman.com/blog/2021/05/modern-processor/feed/</wfw:commentRss>
			<slash:comments>11</slash:comments>
		
		
			</item>
	</channel>
</rss>
