精华内容
下载资源
问答
  • - 分支预测 __builtin_expect 或者 LIKELY/UNLIKELY 原理是什么? - cpu pipeline架构是如何演进的? - CPU分支预测是如何体现在 pipeline 执行指令 的过程中的? - 对CPU友好的分支预测 能够有多少的性能提升?


    从rocksdb代码看到的分支预测开始

    熟悉rocksdb代码的同学 再看到今天所分享的这个主题,就知道我说的是rocksdb中的LIKELY/UNLIKELY函数。

    这个宏的详细定义是这样的:

    #if defined(__GNUC__) && __GNUC__ >= 4
    #define LIKELY(x)   (__builtin_expect((x), 1))
    #define UNLIKELY(x) (__builtin_expect((x), 0))
    

    rocksdb 作为 各个newsql/nosql 以及相关分布式存储的 存储内核,必然对性能有极高的要求,而这一些细节则会直接体现在rocksdb的代码中。

    同样,以上LIKELY/UNLIKELY这样的宏 被应用在了rocksdb代码中的主要流程,像memtable->add, process_write这样的请求必经之路上,那这个宏相比于原本只需要用if/else这样的代码肯定有自己的独特之处。

    // Memtable->Add
    bool res = table->InsertKeyConcurrently(handle);
    if (UNLIKELY(!res)) {
      return res;
    }
    

    接下来详细分析一下分支预测对程序性能的影响,希望能够回答如下几个问题。

    • 分支预测 是什么?
    • cpu pipeline架构是如何演进的?
    • CPU分支预测是如何体现在 pipeline 执行指令 的过程中的?
    • 对CPU友好的分支预测 能够有多少的性能提升?

    关于__builtin_expect 的说明

    它是gcc编译器引入的一个指令,允许程序员将代码中最有可能执行的分支告诉编译器。具体的写法

    __builtin_expect(EXP,N) // 其中EXP可以为变量,也可以为表达式
    

    意思是,EXP==n的概率很大。

    rocksdb中做了一个封装,LIKELY(x) (__builtin_expect((x), 1)) ,即LIKELY表示x为真的概率很大;相反,UNLIKELY表示x为假的概率很大。

    当然这一些编译器指令需要 gcc版本 在2.96以上才支持,gcc会将代码中的调用的“分支转移”指令 编译成能够减少CPU跳转的汇编指令,方便后续的CPU执行。

    如下代码:

    int x, y;
     if(UNLIKELY(x > 0))
        y = 1; 
    else 
        y = -1;
    

    这个事例中,上面的代码中 gcc 编译的指令会预先读取 y = -1 这条指令,这适合 x 的值大于 0 的概率比较小的情况。如果 x 的值在大部分情况下是大于 0 的,就应该用 Likely(x > 0),这样编译出的指令是预先读取 y = 1 这条指令了。这样系统在运行时就会减少重新取指了。

    关于cpu pipeline机制的说明

    Cpu 为什么需要时钟

    我们在看CPU型号的时候 可以看到有如下这样的配置

    Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz
    
    • Intel 代表厂商,除了Intel还有AMD
    • Xeon 表示cpu的一种型号系列: 至强型号,除了这个型号还有core(酷睿),PenTIum(奔腾),Atom(凌动)等
    • Gold 5218 表示 至强 系列中的一种型号,数字越大,表示型号越新,也就是性能越好。
    • 2.30GHz 表示CPU的主频上限,也就是我们要说的时钟频率/周期。

    时钟在CPU中的重要性不言而喻,像DRAM, DMA 等都可以看作是CPU的外设,这一些外设通过CPU的调度有序“和谐”运行,而这个有序 则就是通过CPU的时钟周期来控制的。

    在这里插入图片描述

    如上图,一个clock period 可以表示一个时钟周期。

    总结来说,CPU时钟周期的主要作用:

    1. 协调 外设组件工作
      无固定频率的CPU和周边工作单元协同工作时,因为大家步调不一致,沟通起来效率会打折扣(通才采用应答的模式来进行速度匹配,类似于TCP)。

      有的器件它的工作速度可能非常快(比如说一个单纯的PC,最简单的情况下它只会对上一个上升沿或下降沿到来时自己的值+4),而这个快可以通过时钟周期来表示,比如1.5个时钟周期。

      同时,有的器件它工作的速度可能会非常慢(比如说FPU在计算一个极其复杂的浮点数除法时,再如DIV模块进行32位除法的时候一般需要32个周期),当然这个 也可以用时钟周期来表示,比如 4。这样,CPU能够通过时钟周期知道其他组件的运行情况,其他组件也能通过统一的CPU 时钟周期知道各自的运行情况,从而让整个计算机系统更加协调得运行。

    2. “同步”地工作

      例如CPU从寄存器中读取一个32位的数据,此时要求32位数据同时读入而不能有先后顺序,于是就需要有同一个时钟信号,让部件在同一个上升沿(或下降沿)去读取出这样一个数据。

    接下来可以看看CPU pipeline 架构 是如何从 单周期cpu --> 多周期cpu --> pipeline cpu的。

    指令执行的各个过程会在pipeline 处详细描述。

    单周期cpu

    单周期CPU在一个时钟周期内完成所有的工作。例如指令取出,得到结果等,它们全部都在一个时钟周期内完成。

    在这里插入图片描述

    如上图,一个时钟周期内完成一个指令不同阶段的完整执行,后续的每一个指令都会消耗一个时钟周期。

    在单周期cpu中时钟周期就是指令周期,因此每个指令周期都是固定的时长;

    单周期的缺点:

    由于不同的指令执行快慢是不一样的,导致指令周期必须取决于执行时间最长的指令,因此执行较为简单的指令时会浪费时间,处理器的主频也难以提高。

    优点:

    CPU架构简单,执行的执行不需要复杂的控制逻辑。

    多周期cpu

    多周期CPU将整个CPU的执行过程分成几个阶段,每个阶段用一个时钟去完成。在多周期CPU中,一个指令周期可以由数个时钟周期组成,不同的指令可以占用不同的时钟周期数,从而提高了时间片的利用率,不仅能提高CPU主频,还为组成指令流水线提供了基础。
    在这里插入图片描述

    但是存在的问题仍然是 部分指令的阶段执行较快,而时钟周期仍然是选择指令阶段执行最长的时钟作为时钟周期,所以此时cpu还是会存在等待的问题。

    相比于单周期cpu,主频明显提高了,但是仍然存在等待的情况,即时间利用率并不高,对应的cpu性能也不会很高(很多时间都是在空闲等待)。

    为了充分提高cpu的时钟频率,提升CPU性能,研究员研究出了更高性能的CPU架构 – pipeline

    pipeline cpu

    先总体介绍CPU 指令执行的五个阶段(如下图):
    在这里插入图片描述

    • IF(Instruction fetch) 取指:从 Instruction-Memory 中读取指令,并在下一个时钟上升沿到来时把指令送
      到 ID 级的指令缓冲器 id_ir 中。该级控制信号决定下一个指令指针的 pc 信号(即 Instruction-Memory 的指令地址 i_addr)
    • ID(Instruction decode)指令译码: 对 IF 级的指令进行译码,根据指令操作码获取操作数read reg_1、read reg_2 或者要 直接储存的数据内容 smdr,并在下一个时钟上升沿到来前把指令 id_ir(前 8 位,操作码+operand1)送 到 EX 级的指令缓冲器 ex_ir 中
    • EX(Execute)执行:该级进行算术运算(加、减)、简单传输(JUMP 操作)、逻辑运算(与、或、异或) 或移位操作(逻辑左移、逻辑右移、算术左移、算术右移)。算术逻辑单元 ALU 根据指令对两个操作数 reg_A、 reg_B 进行操作,将获得的结果 ALUo 送到下一级的 reg_C,在此过程中,控制标志信号 cf、nf、zf 并将 其传到相应的缓冲寄存器 ;或者产生存储数据的使能信号 d_we,同时将要直接储存的数据内容 smdr 传到 MEM 级的 smdr1。在下一个时钟上升沿到来前把指令 ex_ir 送到 MEM 级的指令缓冲器 mem_ir 中。总的来说就是拿到译码后的数据在ALU中进行计算,并将计算的结果放在MEM中的缓冲区中。
    • MEM(Memory Access):数据存储器访问: 根据指令处理 reg_C 获取需要的内容存储到缓冲器 reg_C1,并在下 一个时钟上升沿到来前把指令 mem_ir 送到 WB 级的指令缓冲器 wb_ir 中。只有在执行 LOAD、STORE 指令 时才对存储器进行读、写操作,对于此之外的其他指令,MEM 级只起到一个周期的作用。
    • WB(Write Back) 写回:对于需要刷新通用寄存器的操作,WB级把指令执行的结果回写到通用寄存器中

    简化版的指令流水线作业如下(wikibook中的图):
    在这里插入图片描述
    流水线能够极大得提升CPU执行效率的原因所在 是 让CPU的各个组件每时每刻都在工作,充分提升了CPU的时钟频率。

    也就是我们上面说到CPU执行过程的每个阶段都有各自的执行区域,可以将每个执行区域看作一个汽车零件加工厂,每个加工厂只需要负责自己组件的加工,不需要考虑其他的组件是如何加工的。当所有加工厂全速运转时,整个指令从开始到结束会经过每个加工厂,都最后完成汽车的组装的时候整个工厂并没有休息。类比于CPU的话,就是每个阶段的CPU组件都在全力运行。

    关于分支预测 如何在 CPU pipeline机制中工作

    • 分支预测器位于整个CPU核心流水线的差不多最前端部分,靠近IF的级。从指令缓存里面读取指令时,需要由分支预测器来判断从哪里读取。
    • 分支预测器维护一个历史记录表,记录以往执行过的分支指令的偏向情况,帮助未来的预测,本质上也是一块高速缓存。另一块是预测器的逻辑部分,依据记录表里面的记录情况预测将来的分支走向。

    比如一条指令执行十几次都是跳转,那么分支预测器预测将来碰到这条指令时仍然会跳转。如果预测的这个结果连续两次出错,那么预测器则跳转预测结果,改为不跳转。

    如果反复的预测不准确,即如果满足要求,则JMP到另一部分内存执行。
    如果不满足要求,跳过接下来的JMP指令。

    可见分支预测失败 会导致整个指令重新执行,即之前指令执行的几个阶段都会被从流水线抛弃掉,重新放入新的指令到流水线,这样的情况会大大降低CPU的 pipeline效率,相当于汽车工厂 中起始零件加工老是投放劣质材料,导致后续的零件加工发现问题,那这个零件加工到现在 即使快成为汽车了也会被直接回收。

    友好的分支预测对程序性能有多大影响呢

    接下来通过程序模拟分支预测成功和失败,来看看频繁失效的分支预测会对性能有多大的影响。

    如下代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    #include <sys/time.h>
     
    double now() {
      struct timeval t;
      double result;
      gettimeofday(&t, NULL);
      result = (t.tv_sec)*1000000 + t.tv_usec;
      return result; 
    }
    
    int cmp(const void*a,const void*b)//用来做比较的函数。
    {
        return *(int*)a-*(int*)b;
    }
    
    int main()
    {
        // 随机产生整数,用分区函数填充,以避免出现分桶不均
        const unsigned arraySize = 32768;
        int data[arraySize];
        srand((unsigned int)time(0));
     
        for (unsigned c = 0; c < arraySize; ++c)
            data[c] = rand() % 256;
     
        // !!! 排序后下面的Loop运行将更快
        // qsort(data, arraySize, sizeof(unsigned), cmp);
     
        // 测试部分
        double start = now();
        long long sum = 0;
     
        for (unsigned i = 0; i < 100000; ++i)
        {
            // 主要计算部分,选一半元素参与计算
            for (unsigned c = 0; c < arraySize; ++c)
            {
            	// 如果data 没有经过排序,分支预测失效的概率会高很多 data在256内分布不均匀
            	// 如果data 经过排序,那么就类似于  1,2,3,3,..45,46..126,126,...255 这样有序的
            	// 分支预测器能够快速生效
                if (data[c] >= 128) 
                    sum += data[c];
            }
        }
     
        double elapsedTime = (now() - start) / CLOCKS_PER_SEC;
     
        printf("%.2fs\n", elapsedTime);
        printf("sum = %lld\n", sum);
    }
    

    将数组排序 来提供友好的分支预测 与 不排序 所体现的性能:

    1. no sort
    17.40s
    sum = 312786000000
    2. sort
    5.93s
    sum = 312083300000
    

    排序与不排序之间的执行时间差了三倍。

    同样的代码分别用C++ 和 Go 再确认一下性能:
    C++

    #include <algorithm>
    #include <ctime>
    #include <iostream>
     
    int main()
    {
        // 随机产生整数,用分区函数填充,以避免出现分桶不均
        const unsigned arraySize = 32768;
        int data[arraySize];
     
        for (unsigned c = 0; c < arraySize; ++c)
            data[c] = std::rand() % 256;
     
        // !!! 排序后下面的Loop运行将更快
        // std::sort(data, data + arraySize);
     
        // 测试部分
        clock_t start = clock();
        long long sum = 0;
     
        for (unsigned i = 0; i < 100000; ++i)
        {
            // 主要计算部分,选一半元素参与计算
            for (unsigned c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }
     
        double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
     
        std::cout << elapsedTime << "s" << std::endl;
        std::cout << "sum = " << sum << std::endl;
    
        return 0;
    }
    

    C++实现的 排序和不排序的执行时间差异同样有接近三倍:

    # no sort
    19.5751s
    sum = 312426300000
    # sort
    6.00991
    sum = 312426300000
    

    再看看Go的实现:

    package main
    
    import (
    	"math/rand"
    	"sort"
    	"fmt"
    	"time"
    )
    
    func branch() {
    	var size int = 32768
    	testArr := make([]int, size)
    	rand.Seed(1)
    
    	for i := 0; i < size; i++ {
    		testArr[i] = rand.Intn(256)
    	}
    	
    	// sort 
    	sort.Ints(testArr)
    
    	var sum int
    	now := time.Now()
    
    	for j := 0; j < 100000; j++ {
    		for i := 0; i < size; i++ {
    			if testArr[i] >= 128 {
    				sum += testArr[i]
    			}
    		}
    	}
    
    	end := time.Since(now)
    	fmt.Println("time : ", end.Seconds(), "s sum = ", sum)
    }
    
    func main() {
    	branch()
    }
    

    输出如下, go 的性能差异体现的尤为明显(Go执行过程中使用的是协程机制,每一个内核线程可以多个用户线也就是goroutine 程绑定,这样频繁出错的分支预测必然会对执行性能有更加严重的影响)

    # sort
    time :  1.351624114 s sum =  312457800000
    # no sort
    time :  12.331254251 s sum =  312457800000
    

    总结

    通过对CPU pipeline 原理描述,加上 不友好的分支预测 对性能产生的严重影响,我们能够体会到系统设计的复杂和精妙。但核心也非常简单,一切都是追求极致的性能。

    现阶段我们GCC编译器能够帮助我们在程序运行前尽可能得解决分支预测的问题,比如-O1 ,-O2这样的编译器优化选项 以及 文章开头我们Rocksdb代码中利用到了GCC提供的分支预测指令__builtin_expect来帮助我们完成高效的分支预测。但最核心的还是需要我们对自己的代码有足够的掌控能力,对底层知识有更加深刻的理解,也需要我们自身像CPU性能的演进一样,利用好自身的每个时钟周期,且不断得自我精进,方可有极致的性能。

    reference

    1. CPU 分支预测流程介绍
    2. cpu pipeline
    3. pipeline processor
    4. cpu 5-stage pipeline

    展开全文
  • 即得: 机密数据内存地址 = array1基地址 + x 02 分支预测逻辑的运行原理 程序在处理器上执行时可能会由于处理器因边界检查期间或之前的缓存丢失、边界检查依赖的执行单元拥塞以及复杂算术依赖或嵌套推测执行等...

    来源:首席安全官Plus(ID:csoplus)

            首席安全官Plus是由一群科研院所的网络空间安全研究人员发起成立的民间网络空间安全知识平台,努力打造“有特色、高水平、国际化”的网络安全思想高地。围绕“大数据、云计算和人工智能”等高技术领域的网络安全前沿技术、产业趋势和资本市场,汇聚一流资源,产出一流安全洞察。如投稿,请发送到:csoplus@163.com。

    欢迎扫码关注我们的公众号,谢谢!

                                                                     

    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

             现代处理器通过一些微体系结构的部件来预测未来的程序行为,从而达到提高处理器性能的目的。这就需要相关微体系结构的部件能够记录过去程序行为的状态,并假设将来的行为与过去的行为相似或相关。

            然而,当多个程序在同一硬件上执行时,无论是并发执行还是分时执行,由一个程序的行为引起的微体系结构状态变化可能会影响到其他程序,进而可能会引起机密信息从一个程序泄漏到另一个程序中。

     

    01 分支预测逻辑的程序示例

    例如,考虑下面的两行代码:2个数组array1和array2,2个临时变量x和y,1个常量array1_size,表示数组array1的长度。

    if (x <  array1_size)    y =  array2[array1[x] * 4096];

                                                                                        图1:条件分支执行代码示例

    该代码片段的第一行用x < array1_size进行边界安全检查,防止读取array1边界外的机密信息,同时触发一个数据越界异常。

    需要注意的是,读取array1边界外的机密数据可通过提供机密数据内存地址与arary1基地址的相对位置进行计算,而相对位置又可以通过挨个尝试取得。如下,x表示机密数据内存地址与arary1基地址的相对位置,即得:

                                                                          机密数据内存地址 = array1基地址 + x

     

    02 分支预测逻辑的运行原理

     

                                                          

    程序在处理器上执行时可能会由于处理器因边界检查期间或之前的缓存丢失、边界检查依赖的执行单元拥塞以及复杂算术依赖或嵌套推测执行等原因,导致边界检查延迟执行。这使得在获取到边界检查结果前,条件分支后的代码已经在微体系结构层面预先推测地执行了最有可能执行的分支条件后的执行代码。上图介绍了在上述条件分支执行时的四种分支预测场景:

    1) 实际程序的边界检查条件为true,分支预测也为true。在这种情况下,程序有权限进行分支后的代码执行,分支预测部件也推测该程序有后续执行的权限,因此边界检查完成后,发现分支预测与实际一致,直接提交边界检查期间预测执行的结果,大大地提高了处理器的执行性能。

    2) 实际程序的边界检查条件为true,分支预测为false。在这种情况下,程序有权限进行分支后的代码执行,但是分支预测部件认为该程序不具有后续执行的权限,因此边界检查完成后,发现分支预测与实际不一致,该程序仍有权限进行后续执行,继续执行后续代码就可以了,只是不会提前执行有性能的提高而已,不存在机密信息泄露风险。

    3)实际程序的边界检查条件为false,分支预测为true。在这种情况下,程序并没有权限进行分支后的代码执行,但是分支预测部件认为该程序具有后续执行的权限,会推测地预先执行分支后的代码,产生了越权行为,因此存在机密数据泄露的风险。

    4) 实际程序的边界检查条件为false,分支预测为false。在这种情况下,程序并没有权限进行分支后的代码执行,分支预测部件也认为该程序不具有权限进行后续执行,因此该分支完全不执行,不存在机密信息泄露风险。

    因此,在上述场景三的情况下现代处理器内核分支预测逻辑是存在漏洞的。

    03 分支预测逻辑的漏洞利用

     

    由分支预测逻辑的运行原理可知,攻击者要利用分支预测逻辑的漏洞就需要构造场景三,并确保以下三个条件:

    1) 在x不越界的情况下训练分支预测器,让分支预测器误认为if条件一直为true。

    2) 选择一个越界的值x使得array1[x]对应为受害者机密信息k的内存地址;

    3) 确保array1_size和array2不被缓存,而k已经被缓存

    在确保上述三个条件之后,处理器执行图1的代码片段时,开始比较x 是否小于array1_size,在读取array1_size值的时候发现Cache没有命中,因此处理器要等待读取内存DRAM中的array1_size值。

    同时,分支预测器推测地认为if条件为true,因此预测执行逻辑将x与array1的基地址相加进而得到受害者机密数据的内存地址,然后去内存子系统读取机密数据,发现Cache命中,因此会快速的读取k(即array1[x])的值。

    然后,预测执行逻辑会使用k来计算arrary2[k*4096]的地址,并读取该地址内存信息,发现Cache未命中,然后去内存DRAM中读取arrary2[k*4096]的值,而在这个读取过程中,边界条件检查结果已经得到确定,处理器意识到推测地执行是错误的做法,并开始回滚寄存器状态。然而,array2的预测读取是基于k的特定地址进行的,会影响到array2在Cache中的状态。

    为了完成攻击,攻击者需要借助基于时间访问驱动的Cache侧信道攻击来获取机密数据k。主要有两种方式:

    1) 根据arrary2[k*4096]来判断array2的哪一个内存位置曾经被写入Cache(即Cache命中)即可,在得到内存位置后可直接推算k的值;(如Flush+Reload 、 Prime+Probe)

    2) 根array1[x’] 是否Cache命中(命中时array1[x’]等于k),通过选择x’进行度量。(如Evict+Time)

    转载请注明来源:首席安全官Plus ( 微信号: csoplus),谢谢!

    展开全文
  • 分支预测失败至少导致9个时钟周期(有序发射流水线的长度)的损失,包括无法获取指令,加上额外的等待时间,这些时间用于等待the mispredicted branch instruction to become the oldest instruction in the machine ...

    Instruction Pipeline

    关于Pentium Pro的指令流水线,我从4个来源看到3种不同的说法:11级,12级和14级(没有13级说,不大吉利吧)。其实大同小异,不用纠结到底是多少级,掌握每级的功能和用途才是学习的重点。分别摘录如下:

    1. Mindshare的Pentium Pro and Pentium II System Architecture书中说是11级,并且对每一级进行了非常详细的讲解,很值得参考。

     

    2. Wikepedia中说Pentium Pro是14级流水线,但是对每一级未有说明。

    The Pentium Pro incorporated a new microarchitecture in a departure from the Pentium x86 architecture. It has a decoupled, 14-stage superpipelined architecture which used an instruction pool. 

     

    Pentium Pro处理器集成了一套不同于Pentium架构的全新微架构(注:即P6微架构)。该微架构是一套解耦合的14级超流水线架构,同时还引入了微指令池。

    3. 在Performance Characterization of the Pentium® Pro Processor论文中,提到了Pentium Pro有14级流水线,同时极简单的说有序前端是8级,乱序执行核是3级,以及终了逻辑是3级。

    The Pentium Pro processor implements a 14-stage pipeline capable of decoding 3 instructions per clock cycle. The in-order front end has 8 stages. The out-of-order core has 3 stages, and the in-order retirement logic has 3 stages.

     

    4. Pentium Pro的SDM卷2,第2-5页中说,Pentium Pro的流水线是12级,未有详细说明。

    To handle this level of instruction throughput, the Pentium Pro processor uses a decoupled, 12-stage superpipeline that supports out-of-order instruction execution.

     

    P6微架构的指令流水线使用了动态执行机制,揉合了乱序执行和预测式执行(由硬件级别的寄存器重命名和分支预测提供支持)。 处理器有一个有序的发射流水线,这个流水线将Intel386格式的x86指令分解(即翻译,或转换)成相对简单的微指令,然后这些微指令会被分发给一个超标量乱序执行核执行。处理器的乱序执行核实现了多个流水线,可以同时处理整型,跳转,浮点和内存访问操作。若干执行单元也可能会集成在单个流水线中实现,例如整型算术逻辑单元ALU和几个浮点执行单元(加法器,乘法器和除法器)可以共享某单个流水线。通过交错机制,数据高速缓存可以实现伪双端口(pseudo-dual ported)操作,其中一个端口专用于读取,另一个端口专用于存储。大多数简单运算(整型ALU,浮点加法,甚至浮点乘法)可以流水线化,达到每个时钟周期1至2运算的吞吐量。浮点数除法无法流水线化。高延迟的操作可以和低延迟的操作并发执行。

     

    In-order Pipeline

    有序流水线执行分支预测,指令地址翻译,指令预取,指令译码,以及寄存器重命名。代码生成器(例如编译器或者汇编程序员)对于这几个阶段都要有性能方面的考量。只要BTB分支预测成功,有序流水线的这几个阶段通常都不会影响程序执行性能。当分支预测失败时,这几个阶段就可能会产生一些延迟,用来获取将要执行的新指令。

    Out-of-order Core

    乱序执行团(OOO Cluster)解决数据依赖,分发微指令到对应的执行单元,缓存微指令的执行结果知道该指令之前的所有操作都完成,使得处理器的状态可以按照x86指令的顺序正确地得到更新。

     

    Caches

    片上的L1 Caches包括了1) 8K字节,4-way set associative指令cache,和2) 8K字节,2-way set associative数据cache。Cache Line大小都是32字节。 L1 Cache miss也未必会招致完全的读内存延迟。L2 cache makes the full latency caused by  a L1 cache miss。 L1和L2都miss的最小延迟是14个时钟周期。

     

    Branch Target Buffer

    Pentium Pro处理器使用了一个类似于Pentium的分支预测算法。通过BTB缓存,先前看见过的分支可以动态的被预测。以前未看见过的分支只能使用一个静态预测算法来预测。BTB存储着先前看见过的分支的历史记录和对应的目标地址。当遇见一个分支语句时,BTB直接将目标地址交给指令预取单元。一旦分支被执行,BTB将更新成目标地址。

    很难确定分支语句消耗的时钟周期。Pentium Pro有多个级别的分支支持,可以按时钟周期数来量化:

    未跳转分支(not-taken branch)不会引发性能损失。 未跳转分支包括以下两种情形:

    1. BTB预测不会发生跳转的分支
    2. 不在BTB中(默认被预测为不跳转)的向前分支(forward branch)。

    注: 由于分支预测失败,处理器需要获取正确的跳转后指令才能继续执行,因此产生程序的性能损失(penalty)。Intel文档将性能损失分为几档

    1. 微性能损失minor penalty, 大约1个周期
    2. 小性能损失small penalty,大约5-6周期
    3. 大性能损失significant penalty,至少9个周期

    详细情形见下

    被BTB正确地预测,且发生跳转的分支会有1个时钟周期的性能损失。指令获取将被挂起1一个时钟周期。在这1一个周期内,指令译码单元没有指令可以翻译,因为导致小于4个微指令的分发。这种类型的微性能损失包括先前见过的无条件跳转分支(即在BTB中)。对于正确预测的跳转分支,这种小性能损失是1个周期不做指令获取,再加上跳转之后没有微指令发射。这种损失通常会与处理器的其他操作重叠。

    预测失败的分支会引发大性能损失。

    分支预测失败至少导致9个时钟周期(有序发射流水线的长度)的损失,包括无法获取指令,加上额外的等待时间,这些时间用于等待the mispredicted branch instruction to become the oldest instruction in the machine and retire。这种性能损失是不可预测的,主要依赖于执行环境,但是实验表明会有大约10-15周期的损失。

    Decoder Shortstop

    没有在BTB中的分支,但是如果被decoder shortstop branch predication机制正确地预测了,会发生小性能损失(大约5-6周期)。这类情形包括以前没有出现过(没有在BTB中)的无条件直接分支。译码器总是可以正确的预测这种分支。

    负偏移的条件分支(conditional branches w/ negative displacement),例如闭循环分支(loop-closing branch),会被shortstop机制预测为发生跳转(taken)。这种分支的第一次跳转会是小性能损失,随后的第二次跳转则是微性能损失,由BTB正确地预测。

    不再BTB中但是由译码器正确预测的分支导致的小损失大约是5个周期,相比之下,预测失败的分支或者根本没有预测的分支会导致10-15个周期的大损失。

     

    Instruction Prefetcher

    指令预取单元会激进地按直线顺序预取指令。很明显,如果设计代码没有循环分支而是顺序执行能充分利用这种优势。而且,将不常执行的代码隔离到过程的底部或者程序的结尾处,这样也可以避免不必要的预取而提升指令预取的效率。

    注意指令预取总是以16字节对齐。就像Intel486处理器一样,Pentium Pro对齐在16字节边界处预取指令。因此,如果一条分支指令的目标地址(例如汇编程序中某个标签的地址)等于14 mod 16处,在第一个周期内只会有2个字节的指令被预取到。

    目标地址布局示例图。 Target address % 16 = 14,即target address = n * 16 + 14。

     

    要获得最佳性能,可以把JUMP/CALL/RET指令的目标地址对齐在16字节处。但是这样可能会增大代码段的长度,而且会花额外的周期数来译码NOP指令,处理cache miss等。对于分支跳转对齐与代码长度的折中考虑是很敏感的,只有“重要的分支目标”才需要对齐。有性能剖析和反馈能力的编译器可以提供分支指令的动态频率,这样可以更好的指导目标地址的对齐策略。

    展开全文
  • 深入理解操作系统(12)第四章:处理器体系结构(4)Y86-64的流水线实现(包括:PIPE-处理器/预测下一个PC/分支预测/流水线冒险/暂停,转发避免冒险/PPE硬件结构及实现/CPI)1. Y86-64的流水线实现1.1 插入流水线...

    1. Y86-64的流水线实现

    我们开始时以SEQ+作为基础,在各个阶段之间添加流水线寄存器。

    1.1 插入流水线寄存器

    1.1.1 PIPE-抽象试图

    在我们创建一个流水线化的Y86处理器的最初尝试中,我们要在SEQ的各个阶段之间插入流水线寄存器,并对信号重新做了排列,得到PIPE-处理器。

    PIPE-处理器,“-”代表这个处理器和最终的处理器设计相比,性能要差
    

    PIPE-的抽象结构如图4.39所示

    图4.39
    在这里插入图片描述

    1.1.2 例子说明

    下图表明的是下面这段代码序列是怎样通过我们的五阶段流水线的

    irmovl $1, %eax #I1
    irmovl $2, %ebx #I2
    irmovl $3, %ecx #I3
    irmovl $4, %edx #I4
    halt			#I5
    

    图4.40
    在这里插入图片描述

    1.2 对信号进行重新排列和标号

    SEQ+一次只处理一条指令,所以诸如vaC、srcA和vaE这样的信号有惟一的值。在流水线化的设计中,对应于正在经过系统的各个指令,有多组这样的值。

    在我们采用的命名机制中,通过在信号名字前加上大写的流水线寄存器的名字作为前缀,可以惟一地确定存放在流水线寄存器中的信号。

    例如,四个icode值分别命名为 D_icode、E_icode、M_icode和W_icode。
    

    我们还需要引用某些在一个阶段内刚刚计算出来的信号。

    PIPE-的硬件结构
    图4.41

    在这里插入图片描述

    1.3 预测下一个PC

    1.3.1 预测PC的下一个值,来实现每时钟周期都发射一条新指令

    我们流水线化的设计的目的就是每时钟周期都发射(Issue)一条新指令
    也就是说每个时钟周期都有一条新指令进入执行阶段并最终完成。
    

    要达到这个目的也就意味着吞吐量是每个时钟周期一条指令。

    为了达到这个目的,我们必须在取出当前指令之后,马上确定下一条指令的位置。
    

    不幸的是,如果取出的指令是条件分支指令,要到几个周期后,也就是指令通过执行阶段之后,我们才能知道是否要选择分支。类似地如果取出的指令是ret,要到指令通过访存阶段,才能确定返回地址。

    除了条件转移指令和ret这些例外,根据取指阶段中计算的信息,我们能够确定一条指令的地址。
    对于cal和jmp(无条件转移)来说,下一条指令的地址是指令中的常数字valC,而对于其他指令来说就是valP。

    因此,通过预测PC的下一个值,在大多数情况下,我们能达到每个时钟周期发射一条新指令的目的。
    

    1.3.2 分支预测

    对大多数指令类型来说,我们的预测是完全可靠的。
    对条件转移来说我们既可以预测选择了分支,那么新PC值应为valC,也可以预测没有选择分支,那么新PC值应为valP。
    无论在哪种情况中,我们都必须以某种方式来处理我们预测错误的情况,因为此时我们已经取出并部分执行了错误的指令。
    我们会在4.59节中再讨论这个问题猜测分支方向并根据猜测开始取指的技术称为分支预测。

    在我们的设计中,只使用了简单的策略,那就是总是预测选择了条件分支,因而预测PC的新值为valC
    

    1.3.3 分支预测策略

    我们的设计使用总是选择(always taken)分支的预测策略。
    

    研究表明这个策略的成功率大约为60%。反过来,从不选择(never taken,NT)策略的成功率大约为40%。

    一种稍微复杂一点的称为反向选择、正向不选择的策略,当分支地址较低时就预测选择分支,而分支地址较高时,就预测不选择分支而选择下一条指令,这种策略的成功率大约为65%。
    这种改进是源自这样一个事实,那就是循环是由后向分支结束的,而循环通常会执行多次。前向分支用于条件操作,而选择的可能性较小。

    1.3.4 带堆栈的返回地址预测

    对大多数程序来说,很容易预测返回值,因为过程调用和返回是成对出现的。大多数函数调用,会返回到调用后的那条指令。高性能处理器中运用了这个属性,在取指单元中放入一个硬件栈,它保存着过程调用指令产生的返回地址。每次执行过程调用指令时,都将其返回地址压入栈中。当取出一个返回指令时,就从栈中弹出最上面的值,作为预测的返回值。

    同分支预测一样,也必须提供在预测错误时的恢复机制,因为还是有调用和返回不匹配的时候。
    

    通常,这种预测是很可靠的。这个硬件栈对程序员来说是不可见的。

    PIPE的取指阶段,负责预测PC的下一个值,以及为指令的读取选择实际的PC。

    1.4 流水线冒险

    1.4.1 相关

    我们在4.44节中的讨论,将流水线技术引入一个带反馈的系统会导致相邻指令间在发生相关时出现问题。
    在完成我们的设计之前,必须解决这个问题。这些相关有两种形式:

    1. 数据相关,下一条指令会用到这条指令计算出的结果
    2. 控制相关,一条指令要确定下一条指令的位置
    

    1.4.2 冒险:

    在执行跳转,调用或返回指令时,这些相关可能会导致流水线产生计算错误,称为冒险。
    

    同相关一样,冒险也可以分为两类:

    1. 数据冒险( data hazard): 
    	下一条指令会用到这条指令计算出的结果,未读取到会导致错误
    	
    2. 控制冒险( control hazard)。
    

    在本节中,我们关心的是数据冒险。我们会将控制冒险作为整个流水线控制的一部分加以讨论(4.59节)。

    1.4.3 无冒险-prog1

    图4.42描述的是PPE-处理器处理称为prog1的指令序列的情况。
    我们重点关注两条 irmovl指令和addl指令之间的数据相关可能造成的数据冒险。
    图4.42
    在这里插入图片描述

    在周期7开始以后,两条irmovl都已经通过写回阶段,所以寄存器文件保存着更新过的%edx和%eax的值。
    在此示例中,两条irmovl指令和addl指令之间的数据相关没有造成数据冒险。

    原因:

    因为三条nop指令在有数据相关的指令之间创些延迟。
    

    1.4.4 有冒险-prog2

    irmovl指令和add指令之间有两条nop指令

    图4.43
    在这里插入图片描述

    第一个irmovl指令已经通过了写回阶段,因此程序寄存器%edx已经在寄存器文件中更新过了。
    在该周期内,第二个irmovl指令处于写回阶段,因此对程序寄存器%eax的写要到周期7开始,时钟上升时,才会发生。
    结果,读出%eax的错误值(在此我们假设所有的寄存器的初始值为0),因为对该寄存器的写还未发生。很明显,我们的流水线还不能正确处理这样的冒险。

    1.4.5 有冒险-prog3

    irmovl指令和add指令之间有一条nop指令
    图4.44
    在这里插入图片描述

    现在我们必须检查周期5内流水线的行为,此时addl指令通过解码阶段。不幸的是,对寄存器%edx的写仍处在写回阶段,而对寄存器%eax的写还处在访存阶段。因此,addl指令会得到两个错误的操作数。

    1.4.6 列举数据冒险的类型

    当一条指令更新,而后面指令会读到的那些程序状态时,就有可能出现冒险。

    所谓的程序状态包括:程序寄存器、条件码、存储器和程序计数器。
    

    下面让我们来看看每类状态出现冒险的可能性:

    1.程序寄存器:我们已经认识这种冒险了。
    		出现这种冒险是因为寄存器文件的读写是在不同的阶段进行的,
    		导致不同指令之间可能出现不希望的相互作用
    
    2.条件码:在执行阶段中,既可能写(整数操作)也可能读(条件转移)条件码。
    		在条件转移经过这个阶段之前,前面所有的整数操作都已经完成这个阶段了。
    		所以不会发生冒险。
    
    3.程序计数器:更新和读取程序计数器之间的冲突导致了控制冒险。
    		当我们的取指阶段逻辑在取下一条指令之前,正确预测了程序计数器的新值时,就不会产生冒险。
    		预测错误的分支和ret指令需要特殊的处理
    
    4.存储器:对数据存储器的读和写都发生在访存阶段。
    		在一条读存储器的指令到达这个阶段之前前面所有要写存储器的指令都已经完成这个阶段了。
    

    1.5 用暂停来避免冒险

    暂停( stalling)是一种常用的用来避免冒险的技术。
    暂停时,处理器会停止流水线中一条或多条指令,直到冒险条件不再满足。
    (其实就是等待,当前等待前面得指令执行结束,可以获取正确值为止。)
    

    只要一条指令的源操作数会被流水线后面某个阶段中的指令产生,处理器就会通过将指令阻塞在解码阶段来避免数据冒险。

    1.6 用转发(forwarding)来避免冒险

    我们PIPE-的设计是在解码阶段从寄存器文件中读入源操作数,但是有可能对这些源寄存器的写要在写回阶段才能进行。
    与其暂停直到写完成,不如简单地将要写的值传到流水线寄存器E作为源操作数。

    转发:直接将写回阶段的值传到流水线寄存器做源操作数,而不用再写寄存,读寄存器
    这种将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发(data forwarding)。
    或简称转发
    

    图4.49
    在这里插入图片描述

    图4.49用prog2周期6的流水线图的详细描述来说明了这一策略。
    解码阶段逻辑发现寄存器%eax是操作数valB的源寄存器,而在写端口E上还有一个对%eax的未进行的写。它只要简单地将提供到端口E的数据字(信号W_valE)作为操作数valB的值,就能避免暂停。

    这种将结果值直接从一个流水线阶段传到较早阶段的技术称为数据转发(data forwarding,或简称转发)。
    

    它使得prog2的指令能通过流水线而不需要任何暂停。

    图4.50
    在这里插入图片描述

    如图4.50描述的那样,

    当访存阶段中有对寄存器未进行的写时,也可以使用数据转发,以避免程序prog3中的暂停。
    

    在周期5中,解码阶段逻辑发现,在写回阶段中端口E上有对寄存器%edx未进行的写,以及在访存阶段中有会在端口E上对寄存器%eax未进行的写。它不会暂停直到这些写真正发生,而是用写回阶段中的值(信号w_valE)作为操作数valA,用访存阶段中的值(信号M_vaE)作为操作数vaB。
    为了充分利用数据转发技术,我们还可以将新计算出来的值从执行阶段传到解码阶段,以避免程序prog4所需要的暂停

    图4.51
    在这里插入图片描述

    如图4.51所示。
    在周期4中,解码阶段逻辑发现在访存阶段中有对寄存器%edx未进行的写,而且执行阶段中ALU正在计算的值稍后也会写入寄存器%eax。

    它可以将访存阶段中的值(信号M_valE)作为操作数valA,
    也可以将ALU的输出(信号e_valE)作为操作数valB注意,使用ALU的输出不会造成任何同步问题。
    

    解码阶段只要在时钟周期结束之前产生信号valA和valB,这样在时钟上升开始下一个周期时,流水线寄存器E就能装载来自解码阶段的值了。而在此之前ALU的输出已经是合法的了。

    图4.52
    在这里插入图片描述

    图4.52给出的是PPE的抽象结构,它是PPE-的扩展,能通过转发处理数据冒险。
    我们可以看到添加了从五个转发源到解码阶段的额外的反馈路径(用深灰色表示)。这些旁路路径( bypasspath) 反馈到解码阶段中一个标号为“Forward”的块。
    这个块会用从寄存器文件中读出的值或转发过来的值作为源操作数valA和vaB。

    图4.53
    在这里插入图片描述

    图453给出了PPE硬件结构的一个更为详细的说明。

    1.6 加载使用(load/use)数据冒险

    有一类数据冒险不能单纯用转发来解决,因为存储器读是在流水线较后面才发生的。

    1.6.1 问题:

    图4.54
    在这里插入图片描述

    图4.54举例说明了加载/使用冒险(load/use hazard),其中一条指令(位于地址0x018的mrmovl)从存储器中读出寄存器%eax的值,而下一条指令(位于地址0x01e的add)需要该值作为源操作数,图的下部是周期7和8的扩展说明。

    add指令在周期7中需要该寄存器的值,但是mrmovl指令直到周期8才产生出这个值。
    

    为了从mrmovl“转发到”addl,转发逻辑不得不将值送回到过去的时间!这显然是不可能的,我们必须找到其他机制来解决这种形式的数据冒险。

    1.6.2 解决:暂停+转发

    图4.55
    在这里插入图片描述

    如图4.55展示的那样,

    我们可以通过将暂停和转发结合起来,避免加载/使用数据冒险。
    

    当mrmovl指令通过执行阶段时,流水线控制逻辑发现解码阶段中的指令(add)需要这个从存储器中读出的结果。它会将解码阶段中的指令暂停一个周期,导致执行阶段中插入一个气泡。如周期8的扩展说明所示,从存储器中读出的值可以从访存阶段转发到解码阶段中的add指令。寄存器%edx的值也可以从写回阶段转发到访存阶段。就像流水线图中,从周期7中标号为“D”的方框到周期8中标号为“E”的方框的箭头表明的那样,插入的气泡代替了正常情况下本来应该继续通过流水线的add指令。

    1.6.3 加载互锁

    这种用暂停来处理加载/使用冒险的方法称为加载互锁(load interlock)。
    

    加载互锁和转发技术结合起来足以处理所有可能类型的数据冒险。因为只有加载互锁会降低流水线的吞吐量,我们几乎可以实现每个时钟周期发射一条新指令的吞吐量目标。

    1.7 PIPE各阶段的实现

    现在我们已经创建了PIPE(即我们使用转发技术的流水线化的Y86处理器)的整体结构。

    PIPE使用了一组与前面顺序设计相同的硬件单元,
    另外增加了一些流水线寄存器、重新配置了的逻辑块以及流水线控制逻辑。
    

    在本节中,我们将浏览各个逻辑块的设计,而将流水线控制逻辑的设计放到下一节中再讲。
    许多逻辑块与SEQ和SEQ+中相应部件完全相同,除了我们必须从来自不同流水线寄存器(用大写的流水线寄存器的名字作为前缀)或来自各个阶段计算(用小写的阶段名字的第个字母作为前缀)的信号中选择适当的值。

    1.7.1 PC选择取指阶段

    这个阶段还必须选择程序计数器的当前值,以及预测下一个PC值。

    PC选择逻辑从三个程序计数器源中进行选择。当一条预测错误的分支进入访存阶段时,会从流水线寄存器M(信号M_valA)中读出该指令valP的值(指明下一条指令的地址)。当ret指令进入写回阶段时,会从流水线寄存器W(信号W_valM)中读出返回地址。其他情况会使用存放在流水线寄存器F中(信号F_oredPC)的PC的预测值。

    图略
    后面再详细添加。

    1.7.2 解码和写回阶段

    图4.57略给出的是PIPE的解码和写回逻辑的详细说明。
    标号为“dsE”、“dsM"“srcA”和“srcB的块非常类似于它们在SEQ的实现中的相应部件。我们观察到,提供给写端口的寄存器ID来自于写回阶段(信号W_dsE和 w_dstM),而不是来自于解码阶段。这是因为我们希望进行写的目的寄存器是由写回阶段中的指令指定的。

    1.7.3 缓存阶段

    1.8 流水线控制逻辑

    现在我们准备要创建流水线控制逻辑,完成我们的PPE设计了。
    这个逻辑必须处理下面三种控制情况,即其他机制(例如数据转发和分支预测)不足以处理的控制情况处理

    1. ret:
    	流水线必须暂停直到ret指令到达写回阶段
    
    2. 加载/使用冒险:
    	在一条从存储器中读出一个值的指令和一条使用该值的指令之间,流水线必须暂停一个周期。
    
    3. 预测错误的分支:
    	在分支逻辑发现不应该选择分支之前,分支目标处的几条指令已经进入流水线了。
    	必须从流水线中去掉这些指令。
    

    我们会浏览每种情况所期望的行为,然后再设计出处理这些情况的控制逻辑。

    例子略

    1.9 性能分析

    1.9.1 问题:

    我们可以看到,所有需要流水线控制逻辑进行特殊处理的条件,都会导致我们的流水线不能够实现每个时钟周期发射一条新指令的目标。

    1.9.2 解决:插入气泡

    我们可以通过确定往流水线中插入气泡的频率,来衡量这种效率的损失,因为插入气泡会导致无用的流水线周期。

    一条返回指令会产生三个气泡
    一个加载/使用冒险会产生一个
    一个预测错误的分支会产生两个
    

    1.9.3 CPI每指令周期数

    我们可以通过计算PIPE执行一条指令所需要的平均时钟周期数的估计值,来量化这些处罚对整体性能的影响,

    这种衡量方法称为CPI(cycles per instruction,每指令周期数)。
    

    这种衡量值是流水线平均吞吐量的倒数,不过时间单位是时钟周期,而不是微微秒。

    1.9 未完成的工作

    我们已经创建了PIPE流水线化的微处理器结构,设计了控制逻辑块,并实现了普通流水线流( pipeline flow)不足以处理特殊情况的流水线控制逻辑。

    问题:
    不过,PPE还是缺乏一些实际微处理器设计中所必需的关键特性。我们会强调其中一些,并讨论要增加这些特性需要些什么。

    1.9.1 异常处理

    当一个机器级程序遇到出错的情况时

    例如,非法指令代码或是指令或数据地址越界会导致程序流中断,
    称为异常(exception)。
    

    异常看上去就像过程调用,它调用一个异常处理程序( exceptionhandler),该程序是操作系统的一部分。

    执行halt指令也会触发一个异常。异常处理是处理器指令集体系结构的一部分。通常,依赖于异常的类型,异常会导致处理停止。

    异常处理包括一些细节问题:

    1. 首先,可能同时有多条指令会引起异常
    2. 当首先取出一条指令,开始执行时,导致了一个异常,而后来由于分支预测错误,取消了该指令
    

    1.9.2 多周期指令

    例子:
    Y86指令集中的所有指令都包括一些简单的操作,例如数字加法。这些操作可以在执行阶段个周期内处理完。
    在一个更完整的指令集中,

    我们还需要实现一些需要更为复杂操作的指令,例如整数乘法和除法,以及浮点运算。
    他们需要大约几十个个指令周期
    

    在一个像PIPE这样性能中等的处理器中,这些操作的典型执行时间从浮点加法的3或4个周期到整数除法的32个周期。

    解决:
    为了实现这些指令,我们既需要额外的硬件来执行这些计算,还需要一种机制来协调这些指令的处理与流水线其他部分之间的关系。
    实现多周期指令的一种简单方法就是只是简单地扩展执行阶段逻辑的功能,添加一些整数和浮点算术运算单元。

    1.9.3 存储系统的接口

    在我们对PPE的描述中,我们假设取指单元和数据存储器都可以在一个时钟周期内读或是写存储器中任意的位置。
    我们还忽略了由自我修改(self-modifying)代码造成的可能冒险。在自我修改代码中,一条指令对一个存储区域进行写,而后面的指令又从这个区域中读取。进一步说,我们是以存储器位置的虚拟地址来引用它们的,这就要求在执行实际的读或写操作之前,要将虚拟地址翻译成物理地址。显然,要在一个时钟周期内完成所有这些处理是不现实的。

    更糟糕的是,正在访问的存储器的值可能是位于磁盘上的,这会需要上百万个时钟周期才能把数据读入到处理器存储器中。
    

    1.10 当前的微处理器设计

    一个五阶段流水线,例如我们已经讲过的PIPE处理器,代表了20世纪80年代中期的处理器设计水平
    

    2. 本章小结

    2.1 ISA 指令集体系结构

    我们已经看到,指令集体系结构(即ISA)在处理器行为(就指令集合及其编码而言)和如何实现处理器之间提供了一层抽象。

    一个处理器支持的指令和指令的字节级编码称为它的ISA(指令集体系结构)。
    ISA提供了程序执行的一种顺序说明,也就是一条指令执行完了下一条指令才会开始。
    

    2.2 Y86

    基本IA32指令集,并且大大简化其数据类型、地址模式和指令编码,我们定义出了Y86指令集。得到的ISA既有RISC指令集的属性,也有CISC指令集的属性。然后,我们将不同指令组织放到五个阶段中处理,在此,根据被执行的指令的不同,每个阶段中的操作也不相同。

    2.3 SEQ

    SEQ(取的是“sequentia”处理器的意思)的处理器。
    SEQ就是一个能执行这些计算的硬件结构(指令操作6步骤)的抽象表示
    

    从此,我们构造了SEQ处理器,其中每个时钟周期推进一条指令通过每个阶段。

    2.4 SEQ+

    SEQ+:

    使得更新PC阶段在一个周期开始时执行,而不是结束时才执行,这样产生的处理器设计称为SEQ+,
    因为它扩展了基本的SEQ处理器。
    

    通过重新排列各个阶段,我们创建了SEQ+设计,其中第一个阶段选择程序计数器的值,它被用来取出当前指令流水线化通过让不同的阶段并行操作,改进了系统的吞吐量性能。
    在任意一个给定的时刻,多条指令被处理。在引入这种并行性的过程中,我们必须非常小心,以提供与程序的顺序执行相同的用户可见的、程序级行为。

    2.5 PPE-

    PPE-:

    在SEQ+的各个阶段之间插入流水线寄存器,并对信号重新做了排列,得到PIPE-处理器。
    PIPE-处理器,“-”代表这个处理器和最终的处理器设计相比,性能要差
    

    我们通过往SEQ+中添加流水线寄存器,并重新安排周期来创建PPE-流水线,介绍了流水线化。
    然后,我们添加了转发逻辑,加速了将结果从一条指令发送到另一条指令,从而提高了流水线的性能。有几种特殊情况需要额外的流水线控制逻辑来暂停或取消一些流水线阶段。

    2.6 有关处理器设计的几个重要经验

    在本章中,我们学习了有关处理器设计的几个重要经验

    1. 管理复杂性是首要问题。
    	我们想要优化使用硬件资源,在最小的成本下获得最大的性能。
    	为了实现这个目的,我们创建了一个非常简单而一致的框架,
    	来处理所有不同的指令类型有了这个框架,我们就能够在处理不同指令类型的逻辑中间共享硬件单元。
    	
    2. 我们不需要直接实现ISA
    	ISA的直接实现意味着一个顺序的设计。
    	了获得更高的性能,我们想运用硬件能力以同时执行许多操作,这就导致要使用流水线化的设计。
    	
    3. 硬件设计人员必须非常谨慎小
    	芯片被制造出来,就几乎不可能改正任何错误了。
    	开始就使设计正确是非常重要的。
    
    展开全文
  • 在GPU避免分支的方法 Authored by Brandon Fogerty(XR Graphics Engineer at Unity Technologies.) with Additional Organized by JP.Lee(李正彪)leegoonz@163.com 概要 这边来稿文章中,希望了解如何编写GPU...
  • C语言课程分支语句

    2021-05-20 09:30:00
    《C语言课程分支语句》由会员分享,可在线阅读,更多相关《C语言课程分支语句(36页珍藏版)》请在人人文库网上搜索。1、重庆邮电大学计算机科学与技术学院 李盘林 lipl,分支结构,2020/6/30,回顾-格式输入函数,格式...
  • 5算法与算术运算

    2019-02-15 00:56:00
    但通过以下算术运算: a[i]=1-a[i]就很好地模拟“开关”灯的操作。 main( ) { int n,a[1000],i,k; print(“input a number”); input(“%d”,&n); for( i=1;i;i++) a[i]=0; for( i=2;i;i++) { k=1; while...
  • 文章目录从零开始学RISC-V之指令模板背景介绍一个BJP来了根据定义解码分析指令行为修改具体代码算术运算一个又一个的顶层仿真结果及分析 背景介绍 在上一篇中,我们知道了第一条RISC-V指令的实现过程。其实可以从中...
  • 计算机体系结构量化研究方法——流水线基础与中级概念流水线什么是流水线RISC基本指令集基础...分支预测的性能通过预测降低分支成本静态分支预测动态分支预测分支预测缓冲区如何实现流水化MIPS简单实现MIPS基本流水线...
  • 不难看出,正如马克思所说,一个学科只有上升到用数学来描述的高度,才算真正的完善,这些旁的学科的精华所在,无不是用了一个完美的数学模型描述甚至预测了某一种现象,数学是真正的百科之母。 当然我自己也有一个...
  • 选择结构、循环结构、函数定义与使用55 本章学习目标55 4.1 选择结构55 4.1.1 条件表达式55 4.1.2 单分支选择结构55 4.1.3 双分支选择结构56 4.1.4 嵌套的分支结构56 4.2 循环结构57 4.2.1 for循环57 4.2.2 ...
  • > 算术运算符 > 关系运算符 > && > || 4.复杂条件使用括号提高可读性 if-else选择结构 注意:只有一条语句时,不建议省略{}。 if(条件){ //代码块1 }else{ //代码块2 } 例3:如果张三Java考试成绩大于90分,老师...
  • 认识数学各个分支

    2018-07-21 08:53:00
    自古以来,数学家对于整数性质的研究一直十分重视,但是直到十九世纪,这些研究成果还只是孤立地记载在各个时期的算术著作中,也就是说还没有形成完整统一的学科。   自我国古代,许多著名的数学著作中都关于...
  • 最全数学各个分支简介

    千次阅读 2020-08-26 17:11:56
    数论的发展简况 自古以来,数学家对于整数性质的研究一直十分重视,但是直到十九世纪,这些研究成果还只是孤立地记载在各个时期的算术著作中,也就是说还没有形成完整统一的学科。 自我国古代,许多著名的数学著作...
  • 一个指令的处理要经过很多的阶段,每个阶段执行所需操作的一小部分(从内存读取指令、确定操作类型、从内存中读取数据、执行算术运算、写数据、更新程序计数器)。 分支语句(if else等)的缺点: 当机器遇到分支时...
  • 我参考了陈火旺院士的《高级程序设计语言编译原理》,在这篇文章里我主要是站在编译原理的角度讲述一种语法分析程序的实现的方法,通过对一个典型的例子——算术表达式的分析,从而使大家了解构造一个实用的语法分析...
  • 【Mark】计算机科学导论

    千次阅读 多人点赞 2020-01-14 00:45:22
    目的是为了让CPU内部电路满负荷运转,(几乎所有CPU都采用了该技术) 奔腾芯片上内置了一个分支目标缓存器,能动态预测程序分支的转移情况,从而使流水线保持较高的吞吐率 (分支预测) 工作电压,分为CPU核心电压 ...
  • ai 预测未来股市If you have ever studied computer architecture, you probably know the basic steps the hardware takes to execute a certain routine. If you don’t, here is a brief explanation about this ...
  • 数学分支

    万次阅读 2013-05-29 19:49:31
    至于几何、代数等许多数学分支学科的名称,都是后来很晚的时候才有的。 国外系统地整理前人数学知识的书,要算是希腊的欧几里得的《几何原本》最早。《几何原本》全书共十五卷,后两卷是后人增补的。全书大部分是...
  • 极简算法史

    千次阅读 2019-02-18 23:30:34
    人们用算术完成加法和乘法,用逻辑讨论“或”和“与”,那么,为什么不尝试把上述两种方法结合起来呢?假设有两个彼此相交的集合,其中一些元素只属于两个集合中的一个,即为逻辑或,而另一些元素为两个集合所共有,...
  • 细节问题:1当首先取出一条指令,开始执行时,导致了一个异常,而后来由于分支预测错误,取消了该指令。2因为流水线化的处理器会在不同的阶段更新系统状态的不同部分。 当处于访存或写回阶段中的指令导致异常时,...
  • 因为预判失败需要付出较大的代价,一般在 10 ~ 20 个时钟周期之间,所以如何提高分支预测器的准确率成为了比较重要的课题,常见的实现包括静态分支预测、动态分支预测和随机分支预测等。 上面的这些指令级并行仅仅...
  • RISCV 入门 (学习笔记)

    千次阅读 2021-10-15 11:40:29
    指令集2.1 可配置的通用寄存器组2.2 规整的指令编码2.3 简洁的存储器访问指令2.4 高效的分支跳转指令2.5 简洁的子程序调用2.6 无条件码执行2.7 无分支延迟槽2.8 无零开销硬件循环2.9 简洁的运算指令2.10 优雅的压缩...
  • 二.计算机组成原理:1. 计算机概要与技术:程序概念入门;...3.计算机的算术运算:计算机加减乘除法运算。浮点数表示,IEEE754标准,浮点运算。4.处理器:数据通路的概念以及建立数据通路的思想;流水线...
  • 为什么经常有人对一些科学计算程序一筹莫展,他可以读懂每一行代码,但是却无法预测程序的预测结果,甚至对程序的结构与功能也一知半解,给他一个稍微复杂点的数学公式,他可能就不知道怎么把它变成计算机程序。...
  • 在流水线处理器中减少分支延迟到零 Antonio M. Gonzalez and Jose M. Llaberia 摘要 一种减少流水处理器中分支延迟到零的机制将在本文被描述以及评估。这种机制基于多重预取、提早计算目标地址、延迟分支、并行...
  • 前提:在机器代码中,寄存器里有一组单个字的条件码寄存器,他们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行条件分支指令。常见的条件码如下: 但是,条件码是不能直接访问的,常用的方式有三种...
  • 分支控制(Branch Control)
  • 分支预测对的时候,可以提高性能,只需要很少的额外能量,甚至可以节省能量。但是当它“错误预测”分支时,处理器必须丢弃错误推测的指令,这时的计算工作和能量都被浪费了。处理器的内部状态还必须恢复到错误预测...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,911
精华内容 1,564
关键字:

分支预测算术