一条 15 字节的指令如何从内存传输到 CPU?
How is a 15 bytes instruction transferred form memory to CPU?
假设我们使用的是 x86-64 机器,这意味着它的通用寄存器是 64 位长,它的数据总线一次可以处理 64 位,它的 ALU 最多可以处理 64 位(对吗?)。
有一个像
这样的简单指令
MOV , %eax
通过 64 位数据总线将 32 位数字移动到 CPU 寄存器。
我已阅读以下内容:
An x86-64 instruction may be at most 15 bytes in length.
我的问题是,如果数据总线最大为 64 位,这怎么可能?它如何处理 120 位的指令。 CPU 是否在多个周期获取它?
我的第二个问题是,是否有更大长度的特殊寄存器来存储所有 120 位?
指令编码
现代 X86 指令由以下内容构建:
- 前缀 (0, 1, 2, 3, 4)
- VEX (0, 2, 3)
- 操作码 (1)
- ModR/M (1)
- SIB (0,1)
- DISP (0, 1, 2, 4)
- IMM (0, 1, 2, 4)
前缀为零到四个字节:
第 1 组:LOCK 或 REP
第 2 组:段(CS、SS、DS、ES、FS、GS——并非所有都在 64 位中可用)和分支提示(即分支是否更有可能被采用或不采用?)
第 3 组:操作数大小(66H,某些指令是强制性的!)
第 4 组:地址大小
VEX
VEX 用于 AVX 扩展(大部分)
操作码
OPCODE 是实际的指令,如果不计算 VEX 和一些其他前缀,只有 8 位/特殊字节,例如著名的0F
。 (在 8086 和 80186 上处理代码 0F
表示 POP CS
,后来被重新用于扩展操作码。)
ModR/M定义模式
它告诉我们在这条指令中使用了哪个寄存器 and/or 内存模式。某些指令不支持所有可用模式。
比例、索引、基准
SIB 是 ModR/M 的扩展。
位移
DISP 是位移,一个添加到地址寄存器的立即数(如 [ESP+13])它也可以是到内存位置的直接地址。
立即
IMM 立即值(在 MOV EBX,
中 — 8 是在 EBX
中加载的值,立即值。)
请注意,IMM 通常限于 32 位。 REX
可用于获取 64 位,但并非对所有指令都可用(因为任何一条指令的总字节数为 15 字节)。要在寄存器中加载 64 位,您总是从内存中加载它。这样做的一种方法是使用基于 IP 的地址。 (类似这样的东西:MOV R8, [RIP, -42]
)我还注意到,在过去,gcc 等编译器不使用该指令。但是,对于 64 位处理器,可以使用 32 位位移,因此该值几乎可以是任何地方 (±2Gb)。
加载指令
64位处理器在指令缓存中加载指令。它一次加载 16 个字节(可能因处理器而异)。然后处理器解释这些字节。根据处理器的不同,它可能会将这些字节转换为一组 RISC 指令,或者直接 .
执行指令
例如,LOOP label
指令实际上几乎相当于至少两个指令:
SUB ECX, 1
JNZ label
有些处理器过去很难处理这样的问题,因此 LOOP 非常慢。一个原因是当 LOOP
改变 none.
时 SUB
改变了很多 EFLAGS
解释器不在寄存器中加载指令。它在 CPU 中加载它并在相应的单元(ALU、ACU、FPU 等)中处理它。不过,有指向当前指令的 RIP 寄存器。就您而言,RIP 始终指向当前指令的开头或下一条指令的开头。
具体是怎么实现的,我不知道。他们可能很快(即刻)确定涉及哪个单元并将指令推送到那里。确定大小并不复杂,因此他们可以快速获取所有字节并将它们推送到相关单元 FIFO 中,可能是 15 或 16 字节的值(即 FIFO 中的一项肯定总是 16 字节,一个字节可能被忽略,这意味着硬件甚至没有线来读取它!)这些字节每次都会位于相同的位置。因此,如果输入没有 LOCK
或 REP
,它会在该 FIFO 字节中输入 00h
。
请注意,在 FIFO 中的单元之间移动 16 个字节不算什么。多年来,GPU 一直在其 FIFO 中移动大量数据。
你可以说这些FIFO是额外的寄存器。寄存器文件与FIFO是一样的,只是它具有随机访问而不是“PUSH/POP”类型的机制。两者都使用相似的技术,a.k.a。内存,将数据保存在 FIFO 和寄存器中。
文档
我建议第一个文档,当前标题为:
来自 Intel 的有关可用指令的好读物(不是绝对的所有内容,但足以入门!)
指令获取是一个独立于数据的数据路径load/store。 使用 64 位mov
指令未完成。有专门的逻辑来处理获取和解码可变长度未对齐的 x86 指令。
单个指令可以跨越 4k 页边界,因此它的字节来自 2 个不连续的物理页!前端必须能够获取指令字节并将它们连接到缓冲区中。
即使 8086 也有一个小的指令预取缓冲区,尽管解码不一定需要它,因为在 8088 上它小于最长的指令(不包括前缀)。
查看David Kanter's Sandybridge writeup for a diagram of the front-end in Sandybridge (and Nehalem and Bulldozer). Also Agner Fog's microarch guide. See https://en.wikichip.org/wiki/amd/microarchitectures/zen#Decode了解更多关于最近 AMD 前端的信息。
在 P6 和 SnB 系列 Intel CPUs 上,代码获取和预解码(以查找 insn 边界)发生在 16 字节块中,每个周期查找多达 6 条指令的长度并消耗多达 16每个周期 x86 机器代码的字节数。如果指令运行超过块的末尾,预解码器将保留这些字节直到下一个周期。 Agner Fog 的 microarch pdf 有一些关于优化以避免预解码瓶颈的细节; x86 解码困难。例如在某些情况下,操作数大小前缀会更改指令的 rest 的长度。例如66
前缀是 add eax, imm32
(5 个字节)和 add ax, imm16
(66
+ 3 个字节)之间的唯一区别。英特尔 CPU 中的预解码器在这种情况下会停止,需要额外的周期来处理它。 (亚历克西斯的回答声称长度查找很容易。对于多年来积累的所有 ISA 扩展,肯定 不 容易,其中 VEX 前缀是另一个的无效编码例如指令。当您尝试并行执行多条指令时,它会变得更加困难,因为您必须考虑第一个指令之后的所有指令的多个起点。较早的 CPUs 过去常常很慢解码前缀,例如每个前缀或什至转义字节需要一个额外的周期。但现代主流英特尔(不是低功耗)可以处理任意数量的前缀而不会受到惩罚。)
一次最多向解码器提供 4 条指令(或 5 条或 6 条宏融合指令)。根据 uarch,这最多可以产生 7 个微操作 (uops)(Core2/Nehalem 上的 4-1-1-1 模式)、4 个(Skylake 之前的 SnB 系列)或 5 个(Skylake)。 SKL 仍然只有 4 个解码器,但允许它们产生最多 5 微指令,例如对于像 2-1-1-1 这样的模式。
并行解码 x86 指令是一个瓶颈,以至于现代 CPUs(自 SnB 系列以来的英特尔,自 Zen 以来的 AMD)缓存解码的 uops 以简化代码的热部分。 Pentium 4 的跟踪缓存是该方向的早期实验,但效果不佳(并且它没有解码器吞吐量来维持跟踪缓存未命中的可接受性能)。
另请参见关于逆向计算的 What's the relationship between early 90s Pentium microprocessor and today's Intel designs?,我的回答谈到了为什么 P4 是一个 CPU 架构的死胡同,以及 P6 系列(PPro / PIII)如何演变成英特尔当前的 Sandybridge -家庭。
所有 x86-64 CPUs 都足够新,可以通过宽内部数据路径实现高性能,但是 16 位和 32 位 CPUs 具有相同的 15-字节最大长度(包括冗余前缀)。他们可能会使用一个至少足够大的缓冲区来保存不包括前缀的指令,如果他们在查看操作码之前单独解码这些指令,modrm +额外的寻址模式字节,and/or immediate.
原始 8086 除外,其中充满一条指令的 REP 前缀的 64k 代码段 是 有效的。那时英特尔还没有对指令长度和 8086 解码前缀与指令的其余部分分开定义任何限制。
还相关:
- With variable length instructions how does the computer know the length of the instruction being fetched?
- Instruction decoding when instructions are length-variable
假设我们使用的是 x86-64 机器,这意味着它的通用寄存器是 64 位长,它的数据总线一次可以处理 64 位,它的 ALU 最多可以处理 64 位(对吗?)。
有一个像
这样的简单指令MOV , %eax
通过 64 位数据总线将 32 位数字移动到 CPU 寄存器。
我已阅读以下内容:
An x86-64 instruction may be at most 15 bytes in length.
我的问题是,如果数据总线最大为 64 位,这怎么可能?它如何处理 120 位的指令。 CPU 是否在多个周期获取它?
我的第二个问题是,是否有更大长度的特殊寄存器来存储所有 120 位?
指令编码
现代 X86 指令由以下内容构建:
- 前缀 (0, 1, 2, 3, 4)
- VEX (0, 2, 3)
- 操作码 (1)
- ModR/M (1)
- SIB (0,1)
- DISP (0, 1, 2, 4)
- IMM (0, 1, 2, 4)
前缀为零到四个字节:
第 1 组:LOCK 或 REP
第 2 组:段(CS、SS、DS、ES、FS、GS——并非所有都在 64 位中可用)和分支提示(即分支是否更有可能被采用或不采用?)
第 3 组:操作数大小(66H,某些指令是强制性的!)
第 4 组:地址大小
VEX
VEX 用于 AVX 扩展(大部分)
操作码
OPCODE 是实际的指令,如果不计算 VEX 和一些其他前缀,只有 8 位/特殊字节,例如著名的0F
。 (在 8086 和 80186 上处理代码 0F
表示 POP CS
,后来被重新用于扩展操作码。)
ModR/M定义模式
它告诉我们在这条指令中使用了哪个寄存器 and/or 内存模式。某些指令不支持所有可用模式。
比例、索引、基准
SIB 是 ModR/M 的扩展。
位移
DISP 是位移,一个添加到地址寄存器的立即数(如 [ESP+13])它也可以是到内存位置的直接地址。
立即
IMM 立即值(在 MOV EBX,
中 — 8 是在 EBX
中加载的值,立即值。)
请注意,IMM 通常限于 32 位。 REX
可用于获取 64 位,但并非对所有指令都可用(因为任何一条指令的总字节数为 15 字节)。要在寄存器中加载 64 位,您总是从内存中加载它。这样做的一种方法是使用基于 IP 的地址。 (类似这样的东西:MOV R8, [RIP, -42]
)我还注意到,在过去,gcc 等编译器不使用该指令。但是,对于 64 位处理器,可以使用 32 位位移,因此该值几乎可以是任何地方 (±2Gb)。
加载指令
64位处理器在指令缓存中加载指令。它一次加载 16 个字节(可能因处理器而异)。然后处理器解释这些字节。根据处理器的不同,它可能会将这些字节转换为一组 RISC 指令,或者直接 .
执行指令例如,LOOP label
指令实际上几乎相当于至少两个指令:
SUB ECX, 1
JNZ label
有些处理器过去很难处理这样的问题,因此 LOOP 非常慢。一个原因是当 LOOP
改变 none.
SUB
改变了很多 EFLAGS
解释器不在寄存器中加载指令。它在 CPU 中加载它并在相应的单元(ALU、ACU、FPU 等)中处理它。不过,有指向当前指令的 RIP 寄存器。就您而言,RIP 始终指向当前指令的开头或下一条指令的开头。
具体是怎么实现的,我不知道。他们可能很快(即刻)确定涉及哪个单元并将指令推送到那里。确定大小并不复杂,因此他们可以快速获取所有字节并将它们推送到相关单元 FIFO 中,可能是 15 或 16 字节的值(即 FIFO 中的一项肯定总是 16 字节,一个字节可能被忽略,这意味着硬件甚至没有线来读取它!)这些字节每次都会位于相同的位置。因此,如果输入没有 LOCK
或 REP
,它会在该 FIFO 字节中输入 00h
。
请注意,在 FIFO 中的单元之间移动 16 个字节不算什么。多年来,GPU 一直在其 FIFO 中移动大量数据。
你可以说这些FIFO是额外的寄存器。寄存器文件与FIFO是一样的,只是它具有随机访问而不是“PUSH/POP”类型的机制。两者都使用相似的技术,a.k.a。内存,将数据保存在 FIFO 和寄存器中。
文档
我建议第一个文档,当前标题为:
来自 Intel 的有关可用指令的好读物(不是绝对的所有内容,但足以入门!)
指令获取是一个独立于数据的数据路径load/store。 使用 64 位mov
指令未完成。有专门的逻辑来处理获取和解码可变长度未对齐的 x86 指令。
单个指令可以跨越 4k 页边界,因此它的字节来自 2 个不连续的物理页!前端必须能够获取指令字节并将它们连接到缓冲区中。
即使 8086 也有一个小的指令预取缓冲区,尽管解码不一定需要它,因为在 8088 上它小于最长的指令(不包括前缀)。
查看David Kanter's Sandybridge writeup for a diagram of the front-end in Sandybridge (and Nehalem and Bulldozer). Also Agner Fog's microarch guide. See https://en.wikichip.org/wiki/amd/microarchitectures/zen#Decode了解更多关于最近 AMD 前端的信息。
在 P6 和 SnB 系列 Intel CPUs 上,代码获取和预解码(以查找 insn 边界)发生在 16 字节块中,每个周期查找多达 6 条指令的长度并消耗多达 16每个周期 x86 机器代码的字节数。如果指令运行超过块的末尾,预解码器将保留这些字节直到下一个周期。 Agner Fog 的 microarch pdf 有一些关于优化以避免预解码瓶颈的细节; x86 解码困难。例如在某些情况下,操作数大小前缀会更改指令的 rest 的长度。例如66
前缀是 add eax, imm32
(5 个字节)和 add ax, imm16
(66
+ 3 个字节)之间的唯一区别。英特尔 CPU 中的预解码器在这种情况下会停止,需要额外的周期来处理它。 (亚历克西斯的回答声称长度查找很容易。对于多年来积累的所有 ISA 扩展,肯定 不 容易,其中 VEX 前缀是另一个的无效编码例如指令。当您尝试并行执行多条指令时,它会变得更加困难,因为您必须考虑第一个指令之后的所有指令的多个起点。较早的 CPUs 过去常常很慢解码前缀,例如每个前缀或什至转义字节需要一个额外的周期。但现代主流英特尔(不是低功耗)可以处理任意数量的前缀而不会受到惩罚。)
一次最多向解码器提供 4 条指令(或 5 条或 6 条宏融合指令)。根据 uarch,这最多可以产生 7 个微操作 (uops)(Core2/Nehalem 上的 4-1-1-1 模式)、4 个(Skylake 之前的 SnB 系列)或 5 个(Skylake)。 SKL 仍然只有 4 个解码器,但允许它们产生最多 5 微指令,例如对于像 2-1-1-1 这样的模式。
并行解码 x86 指令是一个瓶颈,以至于现代 CPUs(自 SnB 系列以来的英特尔,自 Zen 以来的 AMD)缓存解码的 uops 以简化代码的热部分。 Pentium 4 的跟踪缓存是该方向的早期实验,但效果不佳(并且它没有解码器吞吐量来维持跟踪缓存未命中的可接受性能)。
另请参见关于逆向计算的 What's the relationship between early 90s Pentium microprocessor and today's Intel designs?,我的回答谈到了为什么 P4 是一个 CPU 架构的死胡同,以及 P6 系列(PPro / PIII)如何演变成英特尔当前的 Sandybridge -家庭。
所有 x86-64 CPUs 都足够新,可以通过宽内部数据路径实现高性能,但是 16 位和 32 位 CPUs 具有相同的 15-字节最大长度(包括冗余前缀)。他们可能会使用一个至少足够大的缓冲区来保存不包括前缀的指令,如果他们在查看操作码之前单独解码这些指令,modrm +额外的寻址模式字节,and/or immediate.
原始 8086 除外,其中充满一条指令的 REP 前缀的 64k 代码段 是 有效的。那时英特尔还没有对指令长度和 8086 解码前缀与指令的其余部分分开定义任何限制。
还相关:
- With variable length instructions how does the computer know the length of the instruction being fetched?
- Instruction decoding when instructions are length-variable