Roxy's Library

Back

本文内容基于 2025 秋季《计算机体系结构》双语班课程讲述,如有差错,欢迎指正

Performance Metrics#

体系机构里面关于性能有很多名词:

  • Response time (aka execution time, latency): 响应时间,完成某个任务所需的时间
    • 想要增强性能,就要减少响应时间,Performance=1ResponseTimePerformance = \frac{1}{ResponseTime}
    • 如果我们说xx的性能是yynn倍,意味着ResponseTime(y)=n×ResponseTime(x)ResponseTime(y) = n \times ResponseTime(x)
  • Throughput: 吞吐量,给定时间内完成的任务数量
    • 一般来说,减少响应时间会增加吞吐量(反之不一定)
  • Clock rate: 时钟频率,单位Hz,表示每秒钟时钟周期数
  • CPI: Cycles Per Instruction,每条指令平均需要的时钟周期数
    • CPI=TotalCyclesInstructionCountCPI = \frac{TotalCycles}{InstructionCount}
    • 一种用来比较同一种指令集不同实现的好坏的性能指标
  • Effective CPI: 在一个程序中,考虑到不同指令有不同CPI的平均值
    • EffectiveCPI=i=1n(CPIi×Fractioni)EffectiveCPI = \sum_{i=1}^{n} (CPI_i \times Fraction_i)
    • 其中CPIiCPI_i是第ii类指令的CPI,FractioniFraction_i是第ii类指令在总指令中所占比例
  • IPC: Instructions Per Cycle,每个时钟周期内执行的指令数
    • IPC=InstructionCountTotalCycles=1CPIIPC = \frac{InstructionCount}{TotalCycles} = \frac{1}{CPI}

CPU Performance Equation#

CPU Performance Equation

cpu 执行时间由三个要素决定:

  • Instruction Count (IC): 指令数量,对于同一个程序,IC由ISA和编译器决定
  • CPI: 每条指令平均需要的时钟周期数,取决于ISA和底层硬件实现
  • Clock Cycle Time (CCT): 每个时钟周期的时间长度,取决于硬件实现和硬件制成工艺

这三个要素之间彼此有牵制,会相互影响,因此在做优化时需要权衡。

用 Effective CPI 的形式,上面的公式可以写成:

CPU Time=i=1n(CPIi×ICi)×Clock Cycle TimeCPU\ Time = \sum_{i=1}^{n} (CPI_i \times IC_i) \times Clock\ Cycle\ Time

Amdahl’s Law#

加速比(Speedup)是用来衡量优化效果的指标,定义为优化前后的性能比值:

Speedup=PerformancenewPerformanceold=ResponseTimeoldResponseTimenewSpeedup = \frac{Performance_{new}}{Performance_{old}} = \frac{ResponseTime_{old}}{ResponseTime_{new}}

有时候我们常常对一个系统的某个部分进行优化,那么整体的加速比就会小于部分的加速比。这就是Amdahl定律所描述的现象

ResponseTimenew=ResponseTimeold×((1Fractionenhanced)+FractionenhancedSpeedupenhanced)ResponseTime_{new} = ResponseTime_{old} \times \left( (1 - Fraction_{enhanced}) + \frac{Fraction_{enhanced}}{Speedup_{enhanced}} \right) Overall Speedup=1(1Fractionenhanced)+FractionenhancedSpeedupenhancedOverall\ Speedup = \frac{1}{(1 - Fraction_{enhanced}) + \frac{Fraction_{enhanced}}{Speedup_{enhanced}}}

这里面的FractionenhancedFraction_{enhanced}指的是被优化部分所花时间在整体中所占的比例

Amdahl定律告诉我们,优化时间占比大的代码,会有更显著的整体性能提升

RISC-V ISA#

CISC vs RISC:

  • CISC: Complex Instruction Set Computer
    • 指令长度不固定
    • Programmer friendly
    • 指令集复杂
    • 对于硬件的实现要求高,每加一条指令都需要额外的硬件支持
  • RISC: Reduced Instruction Set Computer
    • 指令长度固定(RISC-V是32位指令)
    • 引入load/store架构,运算指令只能在寄存器之间进行,只有专门的Load和Store指令才能访问内存
    • 寻址方式有限
    • 操作有限

RISC-V 的设计具有高度的模块化和可扩展性。它定义了一个最基础的整数指令集,仅包含加减法、跳转、Load/Store 等最基本功能。 在此基础上,用户可以根据需求选择添加标准扩展模块,如 M(乘除法)、A(原子操作)、F(单精度浮点)、D(双精度浮点)和 C(压缩指令)。 这种组合方式通过命名体现,例如 RV32I 代表基于 32 位基础整数指令集

User-Level ISA#

最简单的 RISC-V 指令集包括以下三类指令:

  • 算术逻辑指令(Arithmetic,Logical,Shift)
  • 数据传输指令(Load/Store)
  • 控制流指令(Branch/Jump)

可以分为六种格式

RISC-V Instruction Formats

R-format

比如寄存器加法指令:add rd, rs1, rs2,表示将寄存器rs1和rs2的值相加,结果存入寄存器rd

这类指令中用到了三个寄存器,由于RISC-V一共有32个寄存器,因此每个寄存器需要5位二进制数来表示(2^5=32)

另外操作码也分为了三个部分来表示:opcode(7位),funct3(3位),funct7(7位)。这与RISC-V指令集的不断扩展有关

R-format

I-format&S-format

RISC-V中的load/store指令分别使用I-format和S-format

RISC-V I-format & S-format

这里的ld\sd其实是load doubleWordstore doubleWord。 类似的还可以有lb\sb(load/store byte),lw\sw(load/store word)

访问的内存地址为address = rs1 + offset,这点与y86一样。offset是一个12位的有符号立即数

当我们把8位的数据加载到32位寄存器时(lb),对于高位会进行符号扩展;如果将寄存器中的低8位数据写入内存时(sb),会将低8位截断后储存

对于一些立即数指令,比如addi rd, rs1, imm,表示将寄存器rs1的值与立即数imm相加,结果存入寄存器rd。这种指令也可以使用I-format

但是注意到,I-format中的立即数是12位的有符号数,如果需要更大的立即数,就需要使用其他指令组合来实现

U-format

对于把一个大常数加载到寄存器,RISC-V提供了lui rd, imm指令,表示将立即数imm加载到寄存器rd的高20位,低12位补0

RISC-V U-format

通过这个指令,我们可以把一个32位的常数分成两部分来加载:高20位通过lui指令加载,低12位通过addi指令(或者其他)加载

SB-format

RISC-V中的分支指令使用SB-format,比如beq rs1, rs2, offset,表示如果寄存器rs1和rs2的值相等,则跳转到当前指令地址加上offset的位置继续执行。 类似的指令还有bne(不等则跳转)\cdots

对于SB-format,比较有意思的一点是,立即数offset的编码方式比较特殊(见下图)

RISC-V SB-format

为了支持压缩指令集(C-Extension),指令地址需按 2 字节(16-bit)对齐。 因此,编码中的立即数实际上省略了最低位(bit 0 默认为 0),指令中存储的是 bit[12:1] 的信息,这扩大了跳转范围(210-2^{10} to21012^{10} - 1 words)。

UJ-format

对于函数调用,与普通跳转的区别在于必须能够返回。 这需要保存调用处的下一条指令地址(Return Address)。有两条常用的指令:

  • JAL (Jump and Link):JAL rd, offset。该指令跳转到目标地址,同时将下一条指令的地址(PC+4PC+4)保存到目标寄存器 rd 中。按照惯例,rd 通常使用 x1(即 ra 寄存器)作为返回地址寄存器。
  • JALR (Jump and Link Register):JALR rd, offset(rs1)。这是一种间接跳转,目标地址为 rs1+offsetrs1 + offset

一些应用场景:

  • JAL x1, offset:函数调用,跳转到 offset 处,同时将返回地址保存到 ra 寄存器(x1)
  • JALR x0, offset(rs1):函数返回,从 ra 寄存器中获取返回地址并跳转
  • JAL x0, Label:无条件跳转到 Label 处
  • lui rd, imm + JALR x1, offset(rd):实现长跳转

UJ-format的立即数编码方式与SB-format类似,也省略了最低位

RISC-V Registers#

RISC-V Registers 一些没太听过的寄存器

  • global pointer (gp): 全局指针,用于访问静态数据
  • thread pointer (tp): 线程指针,指向线程私有存储

从硬件实现的物理层面来看,寄存器堆(Registers File)通常包含 2 个读端口和 1 个写端口,包含32个通用寄存器

Caller-Saved vs Callee-Saved:

  • Caller-Saved (临时寄存器): 由调用者负责保存和恢复
    • t0-t6 (x5-x7, x28-x31)
  • Callee-Saved (保存寄存器): 由被调用者负责保存和恢复
    • s0-s11 (x8, x9, x18-x27)

Synchronization#

对于同步原语,CISC中有像C&S这样的指令,在RISC-V中也提供了两个原子操作指令:

  • lr.d rd, (rs1) (Load Reserved): 从内存加载一个值到rd,并在处理器内部标记该内存地址为“保留”状态
  • sc.d rd, rs2, (rs1) (Store Conditional): 尝试将rs2的值存储到rs1中的内存地址。如果该地址自LR以来没有被其他处理器修改过,则存储成功,并将rd设置为0;否则,存储失败,并将rd设置为失败码
  • 里面的.d表示操作的是double words,类似的可以有.w(word),.b(byte)

在使用这两个指令时的一些注意事项:

  • SC指令只有在保留的地址有效且该地址包含被写入的值时才会成功,例如;
lr.d t0, (a0)      
sc.d t1, t0, (a1)  # 失败,因为 a1 之前没有被保留过
plaintext
  • SC执行后会清除之前所有的保留状态,例如:
lr.d t0, (a0)      
lr.d t1, (a1)      
sc.d t2, t0, (a0)  # 成功,把所有的保留状态都清除了,a0 和 a1 都不再是保留状态
sc.d t3, t1, (a1)  # 失败,因为 a1 不再是保留状态
plaintext
Performance & RISC-V ISA
https://astro-pure.js.org/blog/computerarch_09_18
Author GreyRat
Published at December 30, 2025
Comment seems to stuck. Try to refresh?✨