learn.lianglianglee.com/专栏/深入浅出计算机组成原理/22 冒险和预测(一):hazard是“危”也是“机”.md.html
2022-08-14 03:40:33 +08:00

433 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>22 冒险和预测hazard是“危”也是“机”.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/深入浅出计算机组成原理/00 开篇词 为什么你需要学习计算机组成原理?.md.html">00 开篇词 为什么你需要学习计算机组成原理?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/01 冯·诺依曼体系结构:计算机组成的金字塔.md.html">01 冯·诺依曼体系结构:计算机组成的金字塔</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/02 给你一张知识地图,计算机组成原理应该这么学.md.html">02 给你一张知识地图,计算机组成原理应该这么学</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/03 通过你的CPU主频我们来谈谈“性能”究竟是什么.md.html">03 通过你的CPU主频我们来谈谈“性能”究竟是什么</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/04 穿越功耗墙,我们该从哪些方面提升“性能”?.md.html">04 穿越功耗墙,我们该从哪些方面提升“性能”?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/05 计算机指令:让我们试试用纸带编程.md.html">05 计算机指令:让我们试试用纸带编程</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/06 指令跳转原来if...else就是goto.md.html">06 指令跳转原来if...else就是goto</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/07 函数调用为什么会发生stack overflow.md.html">07 函数调用为什么会发生stack overflow</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/08 ELF和静态链接为什么程序无法同时在Linux和Windows下运行.md.html">08 ELF和静态链接为什么程序无法同时在Linux和Windows下运行</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/09 程序装载“640K内存”真的不够用么.md.html">09 程序装载“640K内存”真的不够用么</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/10 动态链接:程序内部的“共享单车”.md.html">10 动态链接:程序内部的“共享单车”</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/11 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”?.md.html">11 二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/12 理解电路:从电报机到门电路,我们如何做到“千里传信”?.md.html">12 理解电路:从电报机到门电路,我们如何做到“千里传信”?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/13 加法器:如何像搭乐高一样搭电路(上)?.md.html">13 加法器:如何像搭乐高一样搭电路(上)?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/14 乘法器:如何像搭乐高一样搭电路(下)?.md.html">14 乘法器:如何像搭乐高一样搭电路(下)?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/15 浮点数和定点数怎么用有限的Bit表示尽可能多的信息.md.html">15 浮点数和定点数怎么用有限的Bit表示尽可能多的信息</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/16 浮点数和定点数(下):深入理解浮点数到底有什么用?.md.html">16 浮点数和定点数(下):深入理解浮点数到底有什么用?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/17 建立数据通路(上):指令加运算=CPU.md.html">17 建立数据通路(上):指令加运算=CPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/18 建立数据通路(中):指令加运算=CPU.md.html">18 建立数据通路(中):指令加运算=CPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/19 建立数据通路(下):指令加运算=CPU.md.html">19 建立数据通路(下):指令加运算=CPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/20 面向流水线的指令设计一心多用的现代CPU.md.html">20 面向流水线的指令设计一心多用的现代CPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/21 面向流水线的指令设计奔腾4是怎么失败的.md.html">21 面向流水线的指令设计奔腾4是怎么失败的</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入浅出计算机组成原理/22 冒险和预测hazard是“危”也是“机”.md.html">22 冒险和预测hazard是“危”也是“机”</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/23 冒险和预测(二):流水线里的接力赛.md.html">23 冒险和预测(二):流水线里的接力赛</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/24 冒险和预测CPU里的“线程池”.md.html">24 冒险和预测CPU里的“线程池”</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/25 冒险和预测(四):今天下雨了,明天还会下雨么?.md.html">25 冒险和预测(四):今天下雨了,明天还会下雨么?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/26 Superscalar和VLIW如何让CPU的吞吐率超过1.md.html">26 Superscalar和VLIW如何让CPU的吞吐率超过1</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/27 SIMD如何加速矩阵乘法.md.html">27 SIMD如何加速矩阵乘法</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/28 异常和中断:程序出错了怎么办?.md.html">28 异常和中断:程序出错了怎么办?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/29 CISC和RISC为什么手机芯片都是ARM.md.html">29 CISC和RISC为什么手机芯片都是ARM</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/30 GPU为什么玩游戏需要使用GPU.md.html">30 GPU为什么玩游戏需要使用GPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/31 GPU为什么深度学习需要使用GPU.md.html">31 GPU为什么深度学习需要使用GPU</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/32 FPGA、ASIC和TPU计算机体系结构的黄金时代.md.html">32 FPGA、ASIC和TPU计算机体系结构的黄金时代</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/33 解读TPU设计和拆解一块ASIC芯片.md.html">33 解读TPU设计和拆解一块ASIC芯片</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/34 理解虚拟机:你在云上拿到的计算机是什么样的?.md.html">34 理解虚拟机:你在云上拿到的计算机是什么样的?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/35 存储器层次结构全景:数据存储的大金字塔长什么样?.md.html">35 存储器层次结构全景:数据存储的大金字塔长什么样?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/36 局部性原理:数据库性能跟不上,加个缓存就好了?.md.html">36 局部性原理:数据库性能跟不上,加个缓存就好了?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/37 理解CPU Cache“4毫秒”究竟值多少钱.md.html">37 理解CPU Cache“4毫秒”究竟值多少钱</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/38 高速缓存(下):你确定你的数据更新了么?.md.html">38 高速缓存(下):你确定你的数据更新了么?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/39 MESI协议如何让多核CPU的高速缓存保持一致.md.html">39 MESI协议如何让多核CPU的高速缓存保持一致</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/40 理解内存(上):虚拟内存和内存保护是什么?.md.html">40 理解内存(上):虚拟内存和内存保护是什么?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/41 理解内存解析TLB和内存保护.md.html">41 理解内存解析TLB和内存保护</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/42 总线:计算机内部的高速公路.md.html">42 总线:计算机内部的高速公路</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/43 输入输出设备我们并不是只能用灯泡显示“0”和“1”.md.html">43 输入输出设备我们并不是只能用灯泡显示“0”和“1”</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/44 理解IO_WAITIO性能到底是怎么回事儿.md.html">44 理解IO_WAITIO性能到底是怎么回事儿</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/45 机械硬盘Google早期用过的“黑科技”.md.html">45 机械硬盘Google早期用过的“黑科技”</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/46 SSD硬盘如何完成性能优化的KPI.md.html">46 SSD硬盘如何完成性能优化的KPI</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/47 SSD硬盘如何完成性能优化的KPI.md.html">47 SSD硬盘如何完成性能优化的KPI</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/48 DMA为什么Kafka这么快.md.html">48 DMA为什么Kafka这么快</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/49 数据完整性(上):硬件坏了怎么办?.md.html">49 数据完整性(上):硬件坏了怎么办?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/50 数据完整性(下):如何还原犯罪现场?.md.html">50 数据完整性(下):如何还原犯罪现场?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/51 分布式计算:如果所有人的大脑都联网会怎样?.md.html">51 分布式计算:如果所有人的大脑都联网会怎样?</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/52 设计大型DMP系统MongoDB并不是什么灵丹妙药.md.html">52 设计大型DMP系统MongoDB并不是什么灵丹妙药</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/53 设计大型DMP系统SSD拯救了所有的DBA.md.html">53 设计大型DMP系统SSD拯救了所有的DBA</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/54 理解Disruptor带你体会CPU高速缓存的风驰电掣.md.html">54 理解Disruptor带你体会CPU高速缓存的风驰电掣</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/55 理解Disruptor不需要换挡和踩刹车的CPU有多快.md.html">55 理解Disruptor不需要换挡和踩刹车的CPU有多快</a>
</li>
<li>
<a href="/专栏/深入浅出计算机组成原理/结束语 知也无涯,愿你也享受发现的乐趣.md.html">结束语 知也无涯,愿你也享受发现的乐趣</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>22 冒险和预测hazard是“危”也是“机”</h1>
<p>过去两讲,我为你讲解了流水线设计 CPU 所需要的基本概念。接下来,我们一起来看看,要想通过流水线设计来提升 CPU 的吞吐率,我们需要冒哪些风险。</p>
<p>任何一本讲解 CPU 的流水线设计的教科书,都会提到流水线设计需要解决的三大冒险,分别是<strong>结构冒险</strong>Structural Hazard<strong>数据冒险</strong>Data Hazard以及<strong>控制冒险</strong>Control Hazard</p>
<p>这三大冒险的名字很有意思,它们都叫作<strong>hazard</strong>(冒险)。喜欢玩游戏的话,你应该知道一个著名的游戏,生化危机,英文名就叫 Biohazard。的确hazard 还有一个意思就是“危机”。那为什么在流水线设计里hazard 没有翻译成“危机”,而是要叫“冒险”呢?</p>
<p>在 CPU 的流水线设计里,固然我们会遇到各种“危险”情况,使得流水线里的下一条指令不能正常运行。但是,我们其实还是通过“抢跑”的方式,“冒险”拿到了一个提升指令吞吐率的机会。流水线架构的 CPU是我们主动进行的冒险选择。我们期望能够通过冒险带来更高的回报所以这不是无奈之下的应对之举自然也算不上什么危机了。</p>
<p>事实上,对于各种冒险可能造成的问题,我们其实都准备好了应对的方案。这一讲里,我们先从结构冒险和数据冒险说起,一起来看看这些冒险及其对应的应对方案。</p>
<h2>结构冒险:为什么工程师都喜欢用机械键盘?</h2>
<p>我们先来看一看结构冒险。结构冒险,本质上是一个硬件层面的资源竞争问题,也就是一个硬件电路层面的问题。</p>
<p>CPU 在同一个时钟周期,同时在运行两条计算机指令的不同阶段。但是这两个不同的阶段,可能会用到同样的硬件电路。</p>
<p>最典型的例子就是内存的数据访问。请你看看下面这张示意图,其实就是<a href="https://time.geekbang.org/column/article/99523">第 20 讲</a>里对应的 5 级流水线的示意图。</p>
<p>可以看到,在第 1 条指令执行到访存MEM阶段的时候流水线里的第 4 条指令在执行取指令Fetch的操作。访存和取指令都要进行内存数据的读取。我们的内存只有一个地址译码器的作为地址输入那就只能在一个时钟周期里面读取一条数据没办法同时执行第 1 条指令的读取内存数据和第 4 条指令的读取指令代码。</p>
<p><img src="assets/c2a4c0340cb835350ea954cdc520704e.jpeg" alt="img" /></p>
<p>同一个时钟周期,两个不同指令访问同一个资源</p>
<p>类似的资源冲突,其实你在日常使用计算机的时候也会遇到。最常见的就是薄膜键盘的“锁键”问题。常用的最廉价的薄膜键盘,并不是每一个按键的背后都有一根独立的线路,而是多个键共用一个线路。如果我们在同一时间,按下两个共用一个线路的按键,这两个按键的信号就没办法都传输出去。</p>
<p>这也是为什么,重度键盘用户,都要买贵一点儿的机械键盘或者电容键盘。因为这些键盘的每个按键都有独立的传输线路,可以做到“全键无冲”,这样,无论你是要大量写文章、写程序,还是打游戏,都不会遇到按下了键却没生效的情况。</p>
<p>“全键无冲”这样的资源冲突解决方案,其实本质就是<strong>增加资源</strong>。同样的方案,我们一样可以用在 CPU 的结构冒险里面。对于访问内存数据和取指令的冲突,一个直观的解决方案就是把我们的内存分成两部分,让它们各有各的地址译码器。这两部分分别是<strong>存放指令的程序内存</strong><strong>存放数据的数据内存</strong></p>
<p>这样把内存拆成两部分的解决方案,在计算机体系结构里叫作<a href="https://en.wikipedia.org/wiki/Harvard_architecture">哈佛架构</a>Harvard Architecture来自哈佛大学设计<a href="https://en.wikipedia.org/wiki/Harvard_Mark_I">Mark I 型计算机</a>时候的设计。对应的我们之前说的冯·诺依曼体系结构又叫作普林斯顿架构Princeton Architecture。从这些名字里我们可以看到早年的计算机体系结构的设计其实产生于美国各个高校之间的竞争中。</p>
<p>不过,我们今天使用的 CPU仍然是冯·诺依曼体系结构的并没有把内存拆成程序内存和数据内存这两部分。因为如果那样拆的话对程序指令和数据需要的内存空间我们就没有办法根据实际的应用去动态分配了。虽然解决了资源冲突的问题但是也失去了灵活性。</p>
<p><img src="assets/e7508cb409d398380753b292b6df8391.jpeg" alt="img" /></p>
<p>现代 CPU 架构,借鉴了哈佛架构,在高速缓存层面拆分成指令缓存和数据缓存</p>
<p>不过,借鉴了哈佛结构的思路,现代的 CPU 虽然没有在内存层面进行对应的拆分,却在 CPU 内部的高速缓存部分进行了区分,把高速缓存分成了<strong>指令缓存</strong>Instruction Cache<strong>数据缓存</strong>Data Cache两部分。</p>
<p>内存的访问速度远比 CPU 的速度要慢,所以现代的 CPU 并不会直接读取主内存。它会从主内存把指令和数据加载到高速缓存中,这样后续的访问都是访问高速缓存。而指令缓存和数据缓存的拆分,使得我们的 CPU 在进行数据访问和取指令的时候,不会再发生资源冲突的问题了。</p>
<h2>数据冒险:三种不同的依赖关系</h2>
<p>结构冒险是一个硬件层面的问题,我们可以靠增加硬件资源的方式来解决。然而还有很多冒险问题,是程序逻辑层面的事儿。其中,最常见的就是数据冒险。</p>
<p>数据冒险,其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是<strong>先写后读</strong>Read After WriteRAW<strong>先读后写</strong>Write After ReadWAR<strong>写后再写</strong>Write After WriteWAW。下面我们分别看一下这几种情况。</p>
<h3>先写后读Read After Write</h3>
<p>我们先来一起看看先写后读这种情况。这里有一段简单的 C 语言代码编译出来的汇编指令。这段代码简单地定义两个变量 a 和 b然后计算 a = a + 2。再根据计算出来的结果计算 b = a + 3。</p>
<pre><code>int main() {
int a = 1;
int b = 2;
a = a + 2;
b = a + 3;
}
int main() {
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b = 2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = a + 2;
12: 83 45 fc 02 add DWORD PTR [rbp-0x4],0x2
b = a + 3;
16: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
19: 83 c0 03 add eax,0x3
1c: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
}
1f: 5d pop rbp
20: c3 ret
</code></pre>
<p>你可以看到,在内存地址为 12 的机器码,我们把 0x2 添加到 rbp-0x4 对应的内存地址里面。然后,在紧接着的内存地址为 16 的机器码,我们又要从 rbp-0x4 这个内存地址里面,把数据写入到 eax 这个寄存器里面。</p>
<p>所以,我们需要保证,在内存地址为 16 的指令读取 rbp-0x4 里面的值之前,内存地址 12 的指令写入到 rbp-0x4 的操作必须完成。这就是先写后读所面临的数据依赖。如果这个顺序保证不了,我们的程序就会出错。</p>
<p>这个先写后读的依赖关系,我们一般被称之为<strong>数据依赖</strong>,也就是 Data Dependency。</p>
<h3>先读后写Write After Read</h3>
<p>我们还会面临的另外一种情况,先读后写。我们小小地修改一下代码,先计算 a = b + a然后再计算 b = a + b。</p>
<pre><code>int main() {
int a = 1;
int b = 2;
a = b + a;
b = a + b;
}
int main() {
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b = 2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = b + a;
12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
15: 01 45 fc add DWORD PTR [rbp-0x4],eax
b = a + b;
18: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
1b: 01 45 f8 add DWORD PTR [rbp-0x8],eax
}
1e: 5d pop rbp
1f: c3 ret
</code></pre>
<p>我们同样看看对应生成的汇编代码。在内存地址为 15 的汇编指令里,我们要把 eax 寄存器里面的值读出来,再加到 rbp-0x4 的内存地址里。接着在内存地址为 18 的汇编指令里,我们要再写入更新 eax 寄存器里面。</p>
<p>如果我们在内存地址 18 的 eax 的写入先完成了,在内存地址为 15 的代码里面取出 eax 才发生,我们的程序计算就会出错。这里,我们同样要保障对于 eax 的先读后写的操作顺序。</p>
<p>这个先读后写的依赖,一般被叫作<strong>反依赖</strong>,也就是 Anti-Dependency。</p>
<h3>写后再写Write After Write</h3>
<p>我们再次小小地改写上面的代码。这次,我们先设置变量 a = 1然后再设置变量 a = 2。</p>
<pre><code>int main() {
int a = 1;
a = 2;
}
int main() {
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
a = 2;
b: c7 45 fc 02 00 00 00 mov DWORD PTR [rbp-0x4],0x2
}
</code></pre>
<p>在这个情况下,你会看到,内存地址 4 所在的指令和内存地址 b 所在的指令,都是将对应的数据写入到 rbp-0x4 的内存地址里面。如果内存地址 b 的指令在内存地址 4 的指令之后写入。那么这些指令完成之后rbp-0x4 里的数据就是错误的。这就会导致后续需要使用这个内存地址里的数据指令,没有办法拿到正确的值。所以,我们也需要保障内存地址 4 的指令的写入,在内存地址 b 的指令的写入之前完成。</p>
<p>这个写后再写的依赖,一般被叫作<strong>输出依赖</strong>,也就是 Output Dependency。</p>
<h3>再等等:通过流水线停顿解决数据冒险</h3>
<p>除了读之后再进行读,你会发现,对于同一个寄存器或者内存地址的操作,都有明确强制的顺序要求。而这个顺序操作的要求,也为我们使用流水线带来了很大的挑战。因为流水线架构的核心,就是在前一个指令还没有结束的时候,后面的指令就要开始执行。</p>
<p>所以,我们需要有解决这些数据冒险的办法。其中最简单的一个办法,不过也是最笨的一个办法,就是<a href="https://en.wikipedia.org/wiki/Pipeline_stall">流水线停顿</a>Pipeline Stall或者叫流水线冒泡Pipeline Bubbling</p>
<p>流水线停顿的办法很容易理解。如果我们发现了后面执行的指令,会对前面执行的指令有数据层面的依赖关系,那最简单的办法就是“<strong>再等等</strong>”。我们在进行指令译码的时候,会拿到对应指令所需要访问的寄存器和内存地址。所以,在这个时候,我们能够判断出来,这个指令是否会触发数据冒险。如果会触发数据冒险,我们就可以决定,让整个流水线停顿一个或者多个周期。</p>
<p><img src="assets/d1e24e4b18411a5391757a197de2bdc8.jpeg" alt="img" /></p>
<p>我在前面说过,时钟信号会不停地在 0 和 1 之前自动切换。其实,我们并没有办法真的停顿下来。流水线的每一个操作步骤必须要干点儿事情。所以,在实践过程中,我们并不是让流水线停下来,而是在执行后面的操作步骤前面,插入一个 NOP 操作,也就是执行一个其实什么都不干的操作。</p>
<p><img src="assets/0d762f2ce532d87cfe69c7b167af9c2a.jpeg" alt="img" /></p>
<p>这个插入的指令就好像一个水管Pipeline里面进了一个空的气泡。在水流经过的时候没有传送水到下一个步骤而是给了一个什么都没有的空气泡。这也是为什么我们的流水线停顿又被叫作流水线冒泡Pipeline Bubble的原因。</p>
<h2>总结延伸</h2>
<p>讲到这里,相信你已经弄明白了什么是结构冒险,什么是数据冒险,以及数据冒险所要保障的三种依赖,也就是数据依赖、反依赖以及输出依赖。</p>
<p>一方面,我们可以通过增加资源来解决结构冒险问题。我们现代的 CPU 的体系结构,其实也是在冯·诺依曼体系结构下,借鉴哈佛结构的一个混合结构的解决方案。我们的内存虽然没有按照功能拆分,但是在高速缓存层面进行了拆分,也就是拆分成指令缓存和数据缓存这样的方式,从硬件层面,使得同一个时钟下对于相同资源的竞争不再发生。</p>
<p>另一方面,我们也可以通过“等待”,也就是插入无效的 NOP 操作的方式,来解决冒险问题。这就是所谓的流水线停顿。不过,流水线停顿这样的解决方案,是以牺牲 CPU 性能为代价的。因为,实际上在最差的情况下,我们的流水线架构的 CPU又会退化成单指令周期的 CPU 了。</p>
<p>所以,下一讲,我们进一步看看,其他更高级的解决数据冒险的方案,以及控制冒险的解决方案,也就是操作数前推、乱序执行和还有分支预测技术。</p>
<h2>推荐阅读</h2>
<p>想要进一步理解流水线冒险里数据冒险的相关知识,你可以仔细看一看《计算机组成与设计:硬件 / 软件接口》的第 4.54.7 章。</p>
<p>想要了解流水线冒险里面结构冒险的相关知识,你可以去看一看 Coursera 上普林斯顿大学的 Computer Architecture 的<a href="https://zh.coursera.org/lecture/comparch/structural-hazard-lB2xV">Structure Hazard</a>部分。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入浅出计算机组成原理/21 面向流水线的指令设计奔腾4是怎么失败的.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入浅出计算机组成原理/23 冒险和预测(二):流水线里的接力赛.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997ab57d9d3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>