JSON、ABI 与流水线

共 43 题
📑 题目列表 43 题
#
★★★

1. JSON 中 true/false/null 字面量在 RFC 8259 中的定义?

在 RFC 8259 中,JSON 的 true/false/null 字面量是如何定义的?

  • JSON 值类型
  • 字面量定义
  • 大小写敏感性

RFC 8259 定义 JSON 有六种值:对象、数组、字符串、数字、布尔值(true/false)和 null。true/false/null 都是字面量关键字,必须严格小写(不能被写成 True、False、NULL 等),且必须原样出现。null 表示"空值/无值",true/false 表示布尔值。它们与字符串不同,不带引号。解析器必须精确匹配这三个字面量,大小写敏感。

JSON 的布尔值用 true/false,空值用 null,都是小写字面量。这与 JavaScript 的布尔/空值对应,但 JSON 严格限定小写形式。

#
★★★

2. JSON 数字类型与 IEEE 754 双精度的可表示范围?

JSON 数字类型与 IEEE 754 双精度的可表示范围有何关系?

  • JSON 数字语法
  • 双精度表示范围
  • 精度损失

JSON 规范定义数字的语法(可选负号、整数、可选小数、可选指数),但并没有规定数字的具体精度/宽度。RFC 8259 建议实现采用与 IEEE 754 双精度(binary64)兼容的表示,且大多数 JSON 解析器(如 JavaScript 的 Number)默认用双精度。因此 JSON 数字通常能表示 double 范围(约 ±1.8e308,最小约 5e-324),但超出 2^53 的整数无法精确表示,会损失精度。JSON 规范本身不限制整数范围,但实现层面受双精度约束。

JSON 数字无固定精度,实现通常落回双精度。因此大整数(>2^53)在 JSON 中可能丢失精度,这是 JSON 处理大数时要注意的点。

#
★★★

3. 解释 JSON 字符串中 \uXXXX 转义序列与 UTF-8 字节序列的差异?

解释 JSON 字符串中 \uXXXX 转义序列与 UTF-8 字节序列的差异?

  • \uXXXX 转义
  • 代理对
  • UTF-8 编码

JSON 字符串中的 \uXXXX 是 Unicode 转义序列,表示一个 16 位码元(UTF-16 code unit)。BMP 内的码点(如 \u4E2D)直接用 \uXXXX。BMP 外的码点(如 emoji)需要用两个 \uXXXX 组合成代理对(如 \uD83D\uDE00 表示 U+1F600)。而 UTF-8 字节序列是另一种编码表示:把码点编码为 1-4 字节(如 U+4E2D 在 UTF-8 中为 E4 B8 AD)。差异:\uXXXX 是"码元级"的转义(面向 UTF-16),UTF-8 字节是"传输字节级"的编码。同一个码点,\uXXXX 用 6 个字符(\uXXXX)表示,UTF-8 用 1-4 个原始字节表示。

\uXXXX 描述的是 UTF-16 码元,BMP 外需代理对;UTF-8 是字节编码。两者都是表示 Unicode 码点的不同层面,JSON 传输时用 \uXXXX 转义可保证 ASCII 安全,实际字节层面按 UTF-8 编码。

#
★★★

4. 解释为何 JSON 不支持注释与尾随逗号?

解释为何 JSON 不支持注释与尾随逗号?

  • JSON 设计哲学
  • 严格语法
  • 跨语言解析

JSON 的设计目标是"最小、严格、可预测的跨语言数据交换格式"。不支持注释与尾随逗号是因为:一是保持语法简洁,让解析器简单高效;二是避免不同语言对注释/尾随逗号处理的不一致,保证用任何语言解析得到相同结果。注释(如 //、/* */)和尾随逗号(对象或数组最后一个元素后的逗号)在多种语言(JavaScript、Python)中语法不同,若允许会造成歧义。因此 RFC 8259 严格定义语法,不允许注释和尾随逗号,保证互操作性。

JSON 的严格性是其"数据交换标准"定位的必然结果。注释应放在数据之外(如 schema 文档),尾随逗号要用工具去除。这也是 JSON 与 JSONC(带注释 JSON)的区别。

#
★★★

5. ASN.1 BER 与 DER 在证书编码中的差异(canonical)?

解释 ASN.1 的 BER 与 DER 在证书编码中的差异(canonical)?

  • BER 编码
  • DER 确定性编码
  • 证书签名

BER(Basic Encoding Rules)是 ASN.1 的标准编码,允许对同一数据有多种合法编码(如整数前导零、SET 元素顺序等),因此不唯一。DER(Distinguished Encoding Rules)是 BER 的确定性(canonical)子集,规定每个值只有唯一一种编码:整数不能有冗余前导零、SET 元素按标记(tag)升序排列、长度用确定形式等。证书(X.509)必须用 DER 编码,因为签名是对编码字节序列计算的,若编码不唯一,签名验证会失败。所以 DER 保证"同一证书内容只有一种编码方式",从而保证签名可验证。

DER 的"确定性"是其核心价值:同样的数据永远编码成同样的字节,签名才可复现。这是证书签名验证的前提,也是 BER 无法用于证书的原因。

#
★★★

6. ARM64 一条典型 ADD 指令的位字段(Rd、Rn、Rm)含义?

解释 ARM64 一条典型 ADD 指令的位字段(Rd、Rn、Rm)含义?

  • ARM64 寄存器寻址
  • ADD 指令格式
  • 位字段

ARM64 的 ADD(寄存器)指令把两个寄存器相加并存入目标寄存器。其固定 32 位编码中,M 指令(算术)包含:Rd 是目标寄存器(结果写入),Rn 是第一个源寄存器(被加数),Rm 是第二个源寄存器(加数)。每条寄存器字段占 5 位,可索引 0-31 号寄存器(x0-x31)。指令还有 opcode 字段与移位选项(如可选 LSL、扩展)。例如 ADD x0, x1, x2 表示 x0 = x1 + x2:Rd=x0,Rn=x1,Rm=x2。

ARM64 是典型的 RISC,寄存器字段固定 5 位,三寄存器操作数明确。理解 Rd/Rn/Rm 是阅读 ARM64 编码的基础。

#
★★★

7. CPU 取指-译码-执行三阶段流水线中每阶段的最小时钟周期为何受限于 L1 I-cache 命中延迟?

CPU 取指-译码-执行三阶段流水线中,每阶段的最小时钟周期为何受限于 L1 I-cache 命中延迟?

  • 流水线时钟周期
  • L1 I-cache 延迟
  • 关键路径

流水线的时钟周期由"最慢阶段"决定,因为所有阶段必须同步在一个时钟周期内完成。取指(IF)阶段需要访问 L1 指令缓存(I-cache)来获取指令,而 L1 I-cache 的命中延迟通常为 4-5 个周期(地址译码、tag 比较、数据读取)。若取指阶段包含完整的 I-cache 访问,则该阶段成为关键路径,整个时钟周期必须至少容纳 I-cache 延迟,否则取指无法在当前周期内完成。因此流水线周期受限于取指阶段(含 L1 I-cache 命中)的延迟,这是三阶段流水线无法达到很高主频的原因之一。

流水线周期 = 最慢阶段。取指依赖 L1 I-cache,其延迟是流水线周期的下限。更深/更细的流水线(现代 CPU 十几级)把取指拆成多阶段以缩短每级延迟,从而提升主频。

#
★★★

8. 为何 x86 CPU 把变长指令译码为微操作(μops)后再交给后端?

为何 x86 CPU 把变长指令译码为微操作(μops)后再交给后端?

  • x86 变长指令
  • 微操作(μops)
  • 乱序执行

x86 指令是变长编码(1-15 字节),且复杂指令(如内存操作数、SIMD、字符串操作)会映射到多个内部操作。直接把变长指令交给后端,会让译码复杂、后端难以统一调度。因此现代 x86 用译码器(decoder)把 CISC 指令翻译成定长的微操作(μops),后端(乱序执行引擎)只处理 μops,这样后端逻辑统一、可乱序执行、寄存器重命名等。μops 是 RISC 风格的内部操作,方便动态调度、重命名、乱序执行与投机执行。这也是"x86 用 RISC 内核执行 CISC 指令"的体现。

变长+复杂指令不适合直接流水线执行。译为 μops 把"复杂译码"与"统一执行"解耦,是现代 x86 微架构(如 Intel 的 Fusion、AMD 的宏/micro-op)的核心设计。

#
★★★

9. 解释 MIPS R 型指令与 I 型指令的 32 位布局,opcode/funct 字段的位置如何?

解释 MIPS R 型指令与 I 型指令的 32 位布局,特别是 opcode/funct 字段位置?

  • MIPS 指令格式
  • R 型布局
  • I 型布局

MIPS 指令固定 32 位。R 型(寄存器运算)布局:opcode(6 位,最高)、rs(5 位)、rt(5 位)、rd(5 位)、shamt(5 位)、funct(6 位,最低)。R 型用 opcode=0 表示,真正的运算由最低 6 位 funct 决定(如 add 的 funct=0x20)。I 型(立即数)布局:opcode(6 位)、rs(5 位)、rt(5 位)、immediate(16 位,最低)。I 型用 opcode 区分操作(如 addi 的 opcode=0x08,lw=0x23)。opcode 位于最高 6 位,funct 位于最低 6 位。

MIPS 是固定长度指令,R 型与 I 型的前 6 位都是 opcode,R 型依赖 funct 区分具体运算,I 型直接由 opcode 区分。理解字段位置是 MIPS 汇编/编译的基础。

#
★★★

10. 解释 instruction cache 与 data cache 分离(哈佛结构)的初衷?

解释 instruction cache 与 data cache 分离(哈佛结构)的初衷?

  • 哈佛结构
  • I-cache/D-cache 分离
  • 带宽与冲突

instruction cache(I-cache)与 data cache(D-cache)分离的初衷主要有三点:一是带宽,取指与访存可以并行,避免取指和数据访问争用同一个缓存端口,提高带宽;二是避免结构冲突,若指令和数据共用缓存,取指与取数据会互相冲突(结构冒险),分离后两者独立;三是特性的差异,指令访问是顺序的(利于预取),数据访问是随机的,分离后可以针对各自规律优化(如 I-cache 用更高局部性、D-cache 支持写)。现代 CPU 的 L1 分离(I-cache/D-cache),L2/L3 统一,正是这一设计的体现。

哈佛结构(指令/数据分离)的核心是"带宽+避免冲突+特性优化"。现代 CPU 在 L1 采用分离,在共享的 L2/L3 统一,兼顾带宽与容量利用率。

#
★★★

11. 解释复杂指令集(CISC)与精简指令集(RISC)在译码复杂度上的差异?

解释复杂指令集(CISC)与精简指令集(RISC)在译码复杂度上的差异?

  • CISC 译码复杂度
  • RISC 译码简单
  • 指令格式

CISC(如 x86)指令变长、格式多样(1-15 字节),且指令语义复杂(如单条指令可含内存操作数、隐式寄存器、多步骤操作),译码器需要处理大量情况,逻辑复杂、译码延迟大、面积大。RISC(如 ARM、MIPS、RISC-V)指令固定长度(如 32 位)、格式统一、操作数规则简单(寄存器到寄存器),译码器逻辑简单、译码快、面积小。因此 RISC 更容易实现高主频流水线,而 CISC 需要更复杂的译码硬件(现代 x86 用译码器把 CISC 转为 μops)。

译码复杂度是 CISC 与 RISC 的核心差异。固定格式+简单语义让 RISC 译码高效,CISC 的灵活指令以复杂译码为代价。这也是 x86 采用"译码为 μops"的原因。

#
★★★

12. 为何现代 CPU 倾向于 RISC-like 微操作执行而非直接执行 CISC 指令?

为何现代 CPU 倾向于用 RISC-like 微操作执行而非直接执行 CISC 指令?

  • 微操作(μops)
  • 统一执行后端
  • 乱序调度

现代 x86 CPU 把 CISC 指令译码为 RISC-like 的微操作(μops)再执行,原因是:一是统一执行模型,μops 是定长、简单、寄存器到寄存器的操作,后端可以用统一的执行单元(ALU、FPU、Load/Store)处理,无需为每种复杂指令设计专用通路;二是便于乱序执行,μops 易于调度、寄存器重命名、投机执行;三是简化复杂性,复杂指令拆成多个 μops,各自独立流水线执行,提高吞吐。这本质上是"用 RISC 内核执行 CISC 指令集",保留 CISC 的软件兼容性,同时获得 RISC 的硬件高效性。

这是 CISC 与 RISC 融合的典型:x86 外在指令集是 CISC,内在执行是 RISC-like 的 μops。它兼顾了兼容性(运行既有 x86 软件)与性能(统一高效的执行核心)。

#
★★★

13. 解释 MIPS jr $ra 寄存器跳转与 jal 函数调用的位编码?

解释 MIPS 的 jr $ra 寄存器跳转与 jal 函数调用的位编码?

  • jr 指令编码
  • jal 指令编码
  • 寄存器跳转 vs 直接跳转

jr 是 R 型指令,格式:opcode(6 位,全 0)、rs(5 位,源寄存器,jr $ra 时 rs=31)、rt(5 位,0)、rd(5 位,0)、shamt(5 位,0)、funct(6 位,jr 的 funct=0x08)。jr $ra 用 rs=31 表示跳转到 $ra 寄存器保存的地址。jal 是 J 型指令,格式:opcode(6 位,jal 的 opcode=0x03)、target(26 位,目标地址的低 26 位)。jal 执行时把返回地址(PC+8)存入 $ra(即 $31),再跳转到 target<<2 拼接的地址。jr 是寄存器间接跳转(返回地址动态),jal 是直接跳转并保存返回地址。

jr 依赖寄存器(R 型,funct 区分),jal 是直接跳转(J 型,opcode 区分)。函数返回用 jr $ra(读 $ra 中的返回地址),函数调用用 jal(保存返回地址到 $ra 并跳转)。

#
★★

14. x86-64 Microsoft ABI(Windows)与 System V ABI 在 ABI 传参规则上的差异?

解释 x86-64 Microsoft ABI(Windows)与 System V ABI 在传参规则上的差异?

  • Windows x64 传参
  • SysV 传参
  • 寄存器差异

两者都把前 4 个整数参数用寄存器传递,但寄存器不同:System V ABI(Linux/macOS)用 rdi、rsi、rdx、rcx、r8、r9(前 6 个整数参数),返回值在 rax;Windows x64 ABI 用 rcx、rdx、r8、r9(前 4 个整数参数),且调用者必须为这 4 个寄存器参数预留 32 字节的"影子空间"(shadow space)在栈上,即使参数没那么多。Windows 前 4 个浮点参数用 xmm0-xmm3,SysV 用 xmm0-xmm7。此外 Windows 的调用者/被调用者保存约定、栈对齐(16 字节)等也有差异。SysV 允许更多参数用寄存器(6 整数 + 8 浮点),Windows 更少(4+4)。

这是两个主流 x86-64 ABI 的关键差异:寄存器编号、参数个数、影子空间。跨平台调用(如动态库)必须遵循对应 ABI,否则参数错乱。

#
★★

15. x86-64 System V ABI 中 rdi/rsi/rdx/rcx/r8/r9 为函数参数寄存器的顺序与用途?

解释 x86-64 System V ABI 中 rdi/rsi/rdx/rcx/r8/r9 作为函数参数寄存器的顺序与用途?

  • SysV 参数寄存器顺序
  • 参数传递
  • 返回值

在 x86-64 System V ABI(Linux/macOS)中,整数/指针参数按顺序用 rdi(第 1 个)、rsi(第 2 个)、rdx(第 3 个)、rcx(第 4 个)、r8(第 5 个)、r9(第 6 个)传递;第 7 个及以后参数从栈上传递(从右向左压栈)。浮点参数用 xmm0-xmm7。返回值放在 rax(及 rdx 用于 128 位返回值)。这些寄存器是"调用者保存"(caller-saved),被调用者可直接使用。这套约定使函数调用无需把所有参数压栈,效率高。

SysV 用 6 个整数寄存器传参,是性能密集型 ABI 设计。理解参数寄存器顺序是阅读汇编、调试 ABI 问题的关键。

#
★★

16. 为何 ARM64 ABI 传参规则要求被调用者保存 x19-x28 而非所有寄存器?

为何 ARM64 ABI 传参规则要求被调用者保存 x19-x28 而非所有寄存器?

  • callee-saved 寄存器
  • 保存成本
  • 平衡取舍

ABI 把寄存器分为调用者保存(caller-saved)与被调用者保存(callee-saved)。ARM64 规定 x19-x28 为 callee-saved(被调用者需保存并在返回前恢复),x0-x18 及 x30 等为 caller-saved。只保存部分寄存器而非全部,是因为:一是保存所有寄存器代价过高,会频繁压栈/弹栈,拖慢函数调用;二是调用者保存的寄存器(参数、返回值、临时)由调用者自行管理,无需被调用者保存;三是设计上把"低频跨调用存活"的寄存器(如基于处理器的变量)放 callee-saved,让编译器只在需要时保存,减少保存开销。这是性能与简单性的平衡。

callee-saved 寄存器让"跨函数存活的值"能在被调用函数中安全使用,但只对少数寄存器要求保存,避免每个函数都保存全部寄存器。这是 ABI 优化的核心思想。

#
★★

17. 为何 x86-64 SysV 把返回值放在 rax/rdx(128 位)?

为何 x86-64 SysV 把返回值放在 rax/rdx(128 位)?

  • 返回值寄存器
  • 128 位返回值
  • rax/rdx

在 x86-64 SysV ABI 中,整数/指针返回值放在 rax;对于 128 位返回值(如 __int128 或两整数组合),低 64 位放 rax、高 64 位放 rdx。这是为了无需通过内存即可高效返回 16 字节的值。rax 是专用的累加/返回值寄存器,rdx 作为高位扩展。相比用栈返回,寄存器返回避免额外的访存。对于更大的返回值(结构体),则按规则通过隐藏指针(sret)或内存返回。

rax/rdx 组合提供 128 位寄存器返回通道,是 ABI 对"小返回值"的高效约定。大于 128 位的返回值才走内存/隐藏指针。

#
★★

18. 给定 C 函数 int f(int a, int b, int c, int d, int e, int f, int g);其在 x86-64 SysV 下寄存器与栈分配?

给定 C 函数 int f(int a, int b, int c, int d, int e, int f, int g),其在 x86-64 SysV 下寄存器与栈如何分配?

  • SysV 前 6 参数寄存器
  • 第 7 参数入栈
  • 栈传参顺序

参数 a、b、c、d、e、f 分别用寄存器 rdi、rsi、rdx、rcx、r8、r9 传递。第 7 个参数 g 从栈上传递,调用者把它压入栈(按 SysV 约定,多余的参数从右向左压栈,因此 g 在栈顶位置)。被调用者通过 rsp 偏移访问栈上的 g。返回值为 int,放在 rax。注意栈对齐要求(调用前 rsp 需 16 字节对齐)。

SysV 把前 6 个整数参数放寄存器,第 7 个起放栈。这是个经典的"参数寄存器用尽后入栈"的例子。

#
★★

19. 解释 ARM64 AAPCS 中 x0-x7 为参数/返回值寄存器的 ABI 约定?

解释 ARM64 AAPCS 中 x0-x7 作为参数/返回值寄存器的 ABI 约定?

  • AAPCS 参数寄存器
  • x0-x7
  • 返回值

ARM64 的 AAPCS(ARM AArch64 Procedure Call Standard)规定 x0-x7 为函数参数寄存器(8 个),用于传递前 8 个整数/指针参数;浮点参数用 v0-v7(即 d0-d7 对应)。返回值放在 x0(和 x1 用于 128 位返回值)。x0-x7 是 caller-saved(调用者保存),被调用者无需保存。相比 x86-64 SysV 的 6 个整数参数寄存器,ARM64 用 8 个,允许更多参数用寄存器传递,减少栈访问。x9-x15 为临时寄存器,x19-x28 为 callee-saved。

ARM64 AAPCS 用 x0-x7 传参,x0 兼作返回值,是 RISC 精简传参的关键。理解其寄存器角色对编写/阅读 ARM64 汇编至关重要。

#
★★

20. 解释 MIPS $sp/$fp/$ra/$gp 四个特殊寄存器在 O32 下的角色?

解释 MIPS 的 $sp/$fp/$ra/$gp 四个特殊寄存器在 O32 ABI 下的角色?

  • $sp 栈指针
  • $fp 帧指针
  • $ra 返回地址

O32 是 MIPS 32 位 ABI。$sp($29)是栈指针,指向栈顶,用于压栈/弹栈与局部变量访问。$fp($30)是帧指针,指向当前栈帧基址,用于访问局部变量/参数(调试时也便于回溯)。$ra($31)是返回地址寄存器,函数调用(jal)时保存返回地址,函数返回用 jr $ra。$gp($28)是全局指针,指向全局数据区(.sdata/.sbss)的基址,用于快速访问全局/静态变量(通过 $gp 加偏移,避免长距离绝对寻址)。这四个寄存器在 O32 中各有明确分工。

O32 是 MIPS 的经典 ABI,$sp/$fp/$ra/$gp 分别管理栈、帧、返回地址、全局数据。理解它们对分析 MIPS 函数调用与栈布局很重要。

#
★★

21. 解释 MIPS O32 ABI 与 N64 ABI 在参数寄存器($a0-$a3)差异?

解释 MIPS O32 ABI 与 N64 ABI 在参数寄存器($a0-$a3)上的差异?

  • O32 参数寄存器
  • N64 参数寄存器
  • 位宽差异

O32 是 32 位 ABI,用 $a0-$a3($4-$7)共 4 个寄存器传递前 4 个整数/指针参数,每个寄存器 32 位;浮点参数用 $f12-$f15。N64 是 64 位 ABI,参数寄存器同样为 $a0-$a7($4-$11)共 8 个,且每个寄存器是 64 位宽,能容纳 64 位整数/指针;浮点参数用 $f12-$f19。N64 相比 O32 增加了参数寄存器数量(8 个 vs 4 个)并扩展到 64 位,因此能更高效地传递更多参数,减少栈传参。

O32 与 N64 是 MIPS 的两个主要 ABI,差异在于位宽(32/64)与参数寄存器数量(4/8)。N64 面向 64 位系统,传参更高效。

#
★★

22. 在 System V AMD64 ABI 中,红区(128 字节)的使用限制是什么?

在 System V AMD64 ABI 中,红区(128 字节)的使用限制是什么?

  • 红区定义
  • 使用限制
  • 信号处理

红区(red zone)是 System V AMD64 ABI 中,rsp 下方(更低地址)的 128 字节,属于当前函数临时可用的区域,无需调整 rsp 即可使用。限制:一是红区只对"叶函数"(不再调用其他函数)安全,因为调用其他函数会覆盖红区;二是信号处理程序/中断会使用进程栈顶,可能破坏红区内容,因此任何会触发信号/异步事件的情况不能依赖红区;三是 Windows x64 ABI 没有红区。编译器通常在叶函数优化时利用红区存放局部变量,避免压栈/弹栈。

红区是"可免费使用、无需移动栈指针"的 128 字节,但只适用于叶函数且不受信号干扰。理解其边界有助于避免栈被破坏的 bug。

#
★★

23. 解释 ARM64 AAPCS64 中 d0/d1 用于 SIMD 参数传递的规则?

解释 ARM64 AAPCS64 中 d0/d1 用于 SIMD 参数传递的规则?

  • AAPCS64 浮点参数
  • d0/d1
  • v0-v7

在 ARM64 AAPCS64 中,浮点/向量参数用 v0-v7(128 位向量寄存器)传递,其中低 64 位被引用为 d0-d7(双精度),低 32 位为 s0-s7(单精度)。因此 d0/d1 是前两个浮点(double)参数使用的寄存器。规则:整数参数与浮点参数各自独立编号,前 8 个浮点/向量参数用 v0-v7,前 8 个整数/指针参数用 x0-x7;两者互不占用。返回值:浮点返回用 v0(d0/s0)。SIMD 向量参数也用 v0-v7 传递。

AAPCS64 用 v0-v7 统一承载浮点/向量参数,d0/d1 是其低 64 位别名。浮点与整数参数寄存器独立计数,是 ARM64 传参的清晰规则。

#
★★

24. RAW、WAR、WAW 三种数据冒险的顺序约束关系?

解释 RAW、WAR、WAW 三种数据冒险的顺序约束关系?

  • RAW 真依赖
  • WAR 反依赖
  • WAW 输出依赖

RAW(Read After Write,写后读)是真依赖:指令 B 读的寄存器是指令 A 写的,必须 A 先写、B 后读,顺序不可颠倒,是唯一无法靠重命名消除的依赖。WAR(Write After Read,读后写)是反依赖:B 写 A 读过的寄存器,顺序约束是"先读后写",可通过寄存器重命名消除(B 写到新物理寄存器)。WAW(Write After Write,写后写)是输出依赖:两条指令写同一寄存器,约束是"写顺序一致",也可通过重命名消除。RAW 反映数据流,WAR/WAW 只是名字冲突(假依赖),可通过寄存器重命名去除。

这是依赖分析的核心。RAW 是真依赖(数据流),WAR/WAW 是假依赖(名字冲突),可重命名消除。乱序执行正是靠重命名消除 WAR/WAW 来突破顺序限制。

#
★★

25. 为何 ARM 在流水线早期把分支指令提前到 ID 阶段(branch folding)?

为何 ARM 在流水线早期把分支指令提前到 ID 阶段(branch folding)?

  • 分支折叠
  • 减少分支延迟
  • 控制冒险

branch folding(分支折叠)是把分支指令的执行提前到流水线早期(如 ID 译码阶段),尽早确定分支方向与目标,从而减少控制冒险(分支延迟)。因为分支指令本身不产生数据(仅跳转),可以提前判断。ARM 早期流水线(如 ARM 3 级)在 ID 阶段执行分支,使分支在译码时就确定目标,避免等到 EX 阶段才跳转,从而减少转移损失(通常 1 个周期)。这降低了控制冒险的开销,提高流水线效率。

分支折叠通过"尽早执行分支"缩短分支延迟,是硬件流水线优化控制冒险的手段之一。现代 CPU 还结合分支预测进一步降低代价。

#
★★

26. 为何 MIPS 设计为“编译期解决数据冒险”而 x86 设计为“硬件动态调度”?

为何 MIPS 设计为"编译期解决数据冒险"而 x86 设计为"硬件动态调度"?

  • 静态调度(MIPS)
  • 动态调度(x86)
  • 兼容性

MIPS 是 RISC,追求简单硬件,指令固定、流水线规则简单,编译器(静态调度)可通过重排指令、插入 NOP 在编译期解决/规避数据冒险,硬件保持简单。x86 是 CISC,面对大量既有二进制软件(不能重新编译),必须在硬件层面动态调度(乱序执行、寄存器重命名、旁路)来适配各种指令组合,保证软件兼容性。因此设计哲学导致:MIPS 把调度责任交给编译器(静态),x86 把调度交给硬件(动态),因为 x86 无法假设所有软件都能重编译。

静态 vs 动态调度源于"谁负责性能"与"软件能否重编译"。MIPS 面向可重编译的编译器生态,x86 面向必须兼容的既有二进制,故用硬件动态调度兜底。

#
★★

27. 给定 5 级流水线中 add r1, r2, r3; add r4, r1, r5 的 RAW 冲突,需要几个 stall 周期?

给定 5 级流水线中 add r1, r2, r3; add r4, r1, r5 的 RAW 冲突,需要几个 stall 周期?

  • RAW 冒险
  • 5 级流水线
  • stall 计算

5 级流水线 IF/ID/EX/MEM/WB。第一条 add 在 WB 阶段写回 r1,第二条 add 在 ID 阶段读 r1(作为源操作数)。若支持全旁路(EX→EX forwarding),第二条可在 EX 阶段直接用第一条 EX 阶段的结果,无需 stall(0 个 stall)。若不支持 forwarding,第二条需等第一条 WB 完成,需要 2 个 stall。经典全旁路模型下答案是 0 个 stall。

这是经典 MIPS 5 级流水线 RAW 冒险题。全旁路(EX→EX forwarding)可从 EX 阶段转发,将无 forward 时的 2 个 stall 减为 0 个。题目按经典全旁路模型给 0 个 stall。

#
★★

28. 解释 Tomasulo 算法中保留站与公共数据总线(CDB)的关系?

解释 Tomasulo 算法中保留站与公共数据总线(CDB)的关系?

  • 保留站
  • CDB
  • 数据转发

Tomasulo 算法用保留站(reservation station)为每条待执行指令暂存操作数/操作信息,实现乱序调度。当指令的操作数未就绪时,保留站记录"等待哪个结果"(通过寄存器/保留站标记)。公共数据总线(CDB,Common Data Bus)是结果广播总线:执行单元完成计算后,把结果连同目标标记写到 CDB,所有保留站同时监听 CDB,匹配到等待该结果的保留站立即接收并更新其操作数。因此 CDB 实现了"结果广播 + 数据转发",让等待指令无需阻塞即可获得数据,是 Tomasulo 消除 RAW 等待并支持乱序的关键。

保留站是"等待的地方",CDB 是"结果广播的通道"。二者配合实现数据转发与乱序完成:保留站监听 CDB 获取所需结果,一旦就绪即可发射。

#
★★

29. 在 5 级流水线中,结构冒险最常见的来源(内存端口冲突)?

在 5 级流水线中,结构冒险最常见的来源(内存端口冲突)是什么?

  • 结构冒险
  • 内存端口冲突
  • 取指 vs 访存

结构冒险是两条指令在同一阶段争用同一硬件资源。在 5 级流水线中,最常见的结构冒险是内存端口冲突:IF 阶段需要取指访问存储器,MEM 阶段需要取数据/写数据也访问存储器,若两者共用单个存储器端口,则会在同一周期争用(取指与访存冲突)。经典 MIPS 用分离的指令存储器与数据存储器(或哈佛结构)来避免;现代 CPU 用分离的 I-cache 与 D-cache 解决。此外浮点单元的共享、寄存器文件的读写端口冲突也是结构冒险来源。

结构冒险源于"资源不足"。内存端口冲突是取指与访存争用统一存储器,通过分离指令/数据存储器(哈佛结构)消除。

#
★★

30. BSON 在 MongoDB 中整数 int32/int64 的字节序?

BSON 在 MongoDB 中整数 int32/int64 的字节序是什么?

  • BSON 字节序
  • int32/int64
  • MongoDB 平台

BSON(Binary JSON)规范规定多字节整数(int32、int64)使用小端序(little-endian)存储。MongoDB 主要运行在 x86 平台(小端),因此 BSON 选择小端序作为标准,与主流平台原生字节序一致,读写无需转换。BSON 用类型字节标记数据类型(如 0x10 表示 int32、0x12 表示 int64),随后是小端序的定长整数。这与网络协议的大端规则不同,BSON 是面向本地存储的二进制格式,明确采用小端。

BSON 的字节序是小端,与 MongoDB 的 x86 平台一致。这是"本地二进制格式用平台原生字节序"的典型,与网络字节序(大端)形成对比。

#
★★

31. Thrift Binary Protocol 的字段类型字节映射表?

解释 Thrift Binary Protocol 的字段类型字节映射表?

  • Thrift 类型编码
  • 类型字节
  • 协议格式

Thrift 的 Binary Protocol 用类型标志字节(byte)标记字段类型,常见映射:STOP=0x00(结构结束)、BYTE=0x03、I16=0x06、I32=0x08、I64=0x0A、DOUBLE=0x04、STRING=0x0B、STRUCT=0x0C、BOOL=0x02、LIST=0x0F、SET=0x0E、MAP=0x0D。数据结构:每个字段前有字段类型字节 + 字段编号(I16),再跟字段值;结构以 STOP 类型结束。整数用固定宽度(I16/I32/I64)或变长(另有 compact protocol),字符串先写长度(I32)再写字节。这个类型字节表使解码器能识别下一个字段的类型。

Thrift Binary Protocol 用"类型字节+字段ID"自描述每个字段,实现二进制序列化。类型字节映射是协议的核心,解码器据此判断字段类型与长度。

#
★★

32. CBOR 与 MessagePack 的主要差异(自描述、长度不确定)?

解释 CBOR 与 MessagePack 的主要差异(自描述、长度不确定)?

  • CBOR 特性
  • MessagePack 特性
  • 自描述与长度

两者都是自描述的二进制序列化格式(带类型标记,无需 schema 即可解码),但 CBOR(RFC 8949)与 MessagePack 有差异:CBOR 严格遵循 RFC 标准,支持明确的大小/长度前缀(确定长度与不定长度流),支持 bignum、标签、浮点格式选择,被用于 COSE(CBOR 对象签名)等安全场景;MessagePack 更简单、更紧凑,整数编码用 fixint 优化,但早期版本对某些类型(如 map 的键顺序、浮点)处理较宽松。CBOR 强调"确定性"与"流式/不定长度",MessagePack 强调"紧凑与速度"。两者都自描述(无需外部 schema),但 CBOR 对长度与类型定义更严谨。

CBOR 与 MessagePack 都是自描述二进制 JSON 替代。CBOR 更规范、支持不定长度与安全相关加密(COSE),MessagePack 更轻量紧凑。这是二进制序列化选型的核心对比。

#
★★

33. FlatBuffers vs Protobuf 在零拷贝读写上的本质差异

解释 FlatBuffers 与 Protobuf 的零拷贝读写的本质差异?

  • FlatBuffers 零拷贝
  • Protobuf 序列化
  • 内存布局

Protobuf 是"序列化到独立缓冲区"的格式:对象先编码为字节流,读取时需再反序列化回对象,产生拷贝与解析开销;且解析需要遍历字段。FlatBuffers 采用"零拷贝"设计:把对象直接以扁平布局写入(内存中字段以偏移量链接),读取时无需反序列化,直接通过偏移量访问字段(返回指向原始缓冲区的指针),实现零拷贝读取。FlatBuffers 的写入需一次性构建不可变缓冲,但读取极端高效;Protobuf 读写都有解析/序列化开销。本质差异:FlatBuffers 让数据"即插即读"(内存即格式),Protobuf 需要中间表示的转换。

零拷贝的本质是"数据在内存中的布局即序列化格式,读时零解析"。FlatBuffers 用偏移量索引实现,适合高频读取;Protobuf 更通用但需解析。这是游戏/高频读取场景选 FlatBuffers 的原因。

#
★★

34. MessagePack 的整数编码格式,fixint/uint 8/16/32/64 如何组织?

解释 MessagePack 的整数编码格式:fixint/uint 8/16/32/64?

  • fixint
  • 无符号整数
  • 编码长度

MessagePack 对整数用类型前缀区分长度。fixint 是 1 字节表示小整数:正数 0x00-0x7F(0-127),负数 0xE0-0xFF(-32 到 -1),无需额外长度字节。较大的整数用带前缀的定长编码:uint 8(0xcc + 1 字节)、uint 16(0xcd + 2 字节)、uint 32(0xce + 4 字节)、uint 64(0xcf + 8 字节);有符号对应 int 8(0xd0)、int 16(0xd1)、int 32(0xd2)、int 64(0xd3)。编码器按值大小选择最小表示,小整数用 1 字节 fixint,节省空间。

MessagePack 用"fixint + 定长前缀"分层编码整数,小整数最省(1 字节)。这是其紧凑性的关键设计。

#
★★

35. Protocol Buffers 3 中 scalar 类型 varint 编码的字节节省?

解释 Protocol Buffers 3 中 scalar 类型 varint 编码的字节节省?

  • varint 编码
  • 字节节省
  • LEB128

Protobuf 的 varint 编码用 LEB128 变长整数:每个字节最高位是 continuation 标志(1 表示还有后续字节),低 7 位存数据,小整数用 1 字节、大整数用更多字节。这对小正数极省:0-127 用 1 字节,128-16383 用 2 字节,以此类推。字段用 tag(字段号 + 类型)编码,字段号为 1-15 时 tag 用 1 字节。因此小整数、小字段号组合让消息非常紧凑。但负整数(如 int32 的负数)会因补码全 1 而占满 10 字节,故 Protobuf 对负数建议用 sint32/sint64(zigzag 编码)也压缩。

varint 通过"小值少字节"节省空间,是 Protobuf 高效的核心。zigzag 编码处理负数,使小负值也紧凑。这是理解 Protobuf 尺寸优化的关键。

#

36. 解释 JSON 解析中的 RFC 8259 严格模式与 JavaScript 兼容模式的差异?

解释 JSON 解析中的 RFC 8259 严格模式与 JavaScript 兼容模式的差异?

  • 严格模式
  • 兼容模式
  • 解析差异

RFC 8259 严格模式要求解析器严格遵守 JSON 语法:顶层可以是任何值(对象/数组/字符串/数字/布尔/null),数字不接受前导零、十六进制、Infinity/NaN,字符串不接受未转义的控制字符、单引号,不允许注释、尾随逗号。JavaScript 兼容模式(宽松的 JSON.parse 或 JSON 扩展)可能接受一些 JavaScript 语法的扩展(如某些实现允许注释、尾随逗号、单引号、十六进制数字、NaN/Infinity),但这偏离 RFC 8259。严格模式保证跨语言一致性,兼容模式提供便利但可能引入歧义。生产环境(如跨系统交换)应使用严格模式。

严格 vs 兼容是"标准合规"与"便利性"的取舍。RFC 8259 严格模式是安全、确定性的标准;兼容模式放宽语法,但可能不互操作。多数 JSON 库默认严格,可配置宽松。

#

37. 给定一段 x86 jmp rel8 跳转指令,手工计算相对偏移字节的取值范围?

给定一段 x86 jmp rel8 跳转指令,手工计算相对偏移字节的取值范围?

  • rel8 短跳转
  • 相对偏移
  • 有符号范围

x86 的 jmp rel8(短跳转)用 1 字节有符号偏移,范围是 -128 到 +127(0x80 到 0xFF 表示 -128 到 -1,0x00 到 0x7F 表示 0 到 127)。偏移是相对"下一条指令地址"(EIP)计算的,即目标地址 = 下一条指令地址 + 偏移。因此短跳转的目标只能位于下一条指令地址之后 127 字节以内(正方向)或之前 128 字节以内(负方向)。超出范围需用 rel32(4 字节偏移)跳转。

rel8 是 1 字节有符号偏移,范围 -128~+127。理解相对跳转是"相对下一条指令偏移"并能判断是否可用短跳转,是手写汇编/反汇编的基础。

#

38. 给定一段 x86 指令 ret/push/call 的字节序列,解释每条指令的长度变化?

给定一段 x86 指令 ret/push/call 的字节序列,解释每条指令的长度变化?

  • x86 指令长度
  • ret/push/call 编码
  • 变长指令

x86 是变长指令。ret 通常 1 字节(0xC3,无操作数);push 根据操作数不同长度不同:push reg 为 1 字节(如 push rax=0x50 是 1 字节,push imm8 为 2 字节)、push imm32 为 5 字节(0x68 + 4 字节立即数);call 相对调用用 rel32 为 5 字节(0xE8 + 4 字节偏移),通过寄存器/内存的间接 call 为 2-3 字节(x86 没有 call rel8 短跳转形式)。因此指令长度从 1 到 5+ 字节不等,体现 x86 变长特性。

x86 指令长度由操作码、操作数类型、寻址方式决定,故变长。ret 最短(1 字节),push/call 因操作数而变长。这是理解 x86 内存布局与反汇编的基础。

#

39. 解释条件码(ZF/CF/SF/OF)在 x86 CMP 指令后的语义?

解释条件码(ZF/CF/SF/OF)在 x86 CMP 指令后的语义?

  • 条件码
  • CMP 指令
  • 标志位意义

CMP 指令执行减法(目的 - 源)但不写回结果,只更新标志位。ZF(零标志):结果为 0 时置 1,用于判断相等(dest == src)。CF(进位标志):减法发生借位(无符号比较 dest < src)时置 1,用于判断无符号大小。SF(符号标志):结果最高位(符号位)为 1 时置 1,反映结果符号。OF(溢出标志):有符号运算溢出时置 1,用于判断有符号比较(结合 SF 判断正负)。例如 CMP 后:JE 看 ZF,JB/JC 看 CF(无符号小于),JG 看 SF/OF/ZF 组合(有符号大于)。条件码组合使 CMP 能支持有符号与无符号比较。

CMP 设置标志位供后续条件跳转使用。ZF 判相等、CF 判无符号、SF/OF 判有符号,是 x86 条件码的核心语义。

#

40. 解释 frame pointer (rbp/fp) 与 stack pointer (rsp/sp) 在调试与异常中的角色?

解释 frame pointer(rbp/fp)与 stack pointer(rsp/sp)在调试与异常中的角色?

  • 帧指针
  • 栈指针
  • 调试/异常

stack pointer(rsp/sp)指向当前栈顶,随压栈/弹栈动态变化,是访问栈上数据(局部变量、参数)的基准。frame pointer(rbp/fp)是指向当前栈帧基址的固定指针,用于访问函数局部变量与参数(通过 rbp+偏移),因为 rsp 会变而 rbp 在函数内不变。在调试与异常处理中,rbp 的关键作用:通过 rbp 链(rbp 指向调用者 rbp+返回地址)可以回溯调用栈(stack unwinding),定位函数调用层次;异常处理(如 C++ 栈展开、C 级 unwind)依赖帧信息找到现场。现代编译器在优化下可能省略 rbp(用 rsp 即可),但会通过 .eh_frame/调试信息提供栈回溯。rbp 提供稳定的帧基准,利于调试与异常。

rsp 是动态栈顶,rbp 是稳定帧基址。rbp 链式结构让调试器/异常处理器能回溯调用栈,这是其核心价值。优化模式可能省略 rbp,改用调试信息 unwind。

#

41. 给定 load-use 冒险场景,列出 stall + forwarding 的最小周期开销?

给定 load-use 冒险场景,列出 stall + forwarding 的最小周期开销?

  • load-use 冒险
  • stall
  • forwarding

load-use 冒险是 load 指令的结果被紧随其后的指令立即使用(如 lw r1, 0(r2); add r3, r1, r4)。在 5 级流水线中,load 在 MEM 阶段才得到数据,而被用指令在 EX 阶段就需要数据,即使 forwarding 也只能从 MEM 阶段转发到 EX,因此需要插入 1 个 stall(load 与用它的指令之间隔一个周期)。最小开销是 1 个 stall + 1 个 forwarding(从 MEM 转发给 EX)。若编译器重排(把无关指令插入中间)则可完全消除 stall。

load-use 是转发的例外:load 结果晚(MEM 阶段),forwarding 只能从 MEM 转发,仍需 1 个 stall。这是转发无法完全消除的一类冒险,需靠编译调度填充。

#

42. 解释 forwarding(旁路)如何消除部分 RAW 冒险?

解释 forwarding(旁路)如何消除部分 RAW 冒险?

  • forwarding 原理
  • 旁路数据
  • RAW 消除

forwarding(旁路/bypassing)是:当一条指令的结果刚产生(如 EX 阶段计算完成)还未写回寄存器文件时,直接把结果旁路转发给后续需要它的指令(旁路网络 bypass network),让后续指令无需等待写回即可使用。在 5 级流水线中,ALU 结果在 EX 阶段末即可通过旁路转发给下一条指令的 EX 阶段输入,从而消除原本需要 stall 的 RAW 冒险。但 load-use 例外(load 结果在 MEM 阶段才产生,旁路从 MEM 转发仍需 1 stall)。forwarding 通过额外的旁路通路(物理连线)实现,是硬件消除 RAW 的关键技术。

forwarding 用"结果一产生就转发"代替"写回再读",把先前需要 stall 的 RAW 冒险大多消除。硬件成本是旁路网络,但大幅提升流水线效率。

#

43. 解释控制冒险中的 branch delay slot 与 branch prediction 关系?

解释控制冒险中的 branch delay slot 与 branch prediction 关系?

  • 分支延迟槽
  • 分支预测
  • 控制冒险

控制冒险源于分支方向未确定时流水线中的指令是否有效。branch delay slot(分支延迟槽)是 RISC(如 MIPS)的静态方案:分支指令后紧跟一个延迟槽指令,该指令无论分支是否跳转都执行/被填充,由编译器用"始终执行无副作用"的指令填充,从而把分支延迟掩盖,无需硬件预测。branch prediction(分支预测)是动态方案:硬件在分支未确定时预测方向并继续取指,预测正确则无损失,预测错误则冲刷流水线并重取。关系:delay slot 是"静态、编译器、固定延迟"的古老方案,prediction 是"动态、硬件、自适应"的现代方案;现代高性能 CPU 用预测取代 delay slot,因为 delay slot 浪费指令槽且难填。

delay slot 与 branch prediction 都是应对控制冒险的手段,前者静态(编译期、占用延迟槽),后者动态(硬件预测)。现代 CPU 均用预测,delay slot 仅存于 MIPS 等经典 RISC。