当前位置: 首页 > 知识库问答 >
问题:

为什么任何现代 x86 掩码都会将计数转换为 CL 中的 5 个低位

穆仲卿
2023-03-14

我正在研究x86 ASM中的左移和右移操作,如<code>shl eax、cl

来自IA-32英特尔架构软件开发人员手册3

所有IA-32处理器(从Intel 286处理器开始)都会将移位计数屏蔽为5位,导致最大计数为31。在所有操作模式(包括虚拟8086模式)下都会进行屏蔽,以减少指令的最大执行时间。

我试图理解这个逻辑背后的原因。也许它是这样工作的,因为在硬件级别上,很难使用1个周期为寄存器中的所有32(或64)位实现移位?

任何详细的解释都会有很大帮助!

共有3个答案

苏涛
2023-03-14

尽管英特尔目前的手册说,掩盖班次计数在186年是新的。例如,reverse-engineering.SE 上的 CPU 检测代码使用该事实来区分 8086/88 和 80186/88。也许英特尔没有计算186,因为它不是100%与IBM-PC兼容,而是用于嵌入式系统?或者英特尔目前的手册是错误的;不会是第一次。

在x86从简单的微编码8086发展到186、286和386的过程中,这是一个非常武断的设计决策,但是我们可以看到一些动机。386有桶形移位器(恒定时间移位),186和286没有。IDK如果ISA设计决策是在硬件设计决策之前或之后确定的。

ARM选择了不同的方式,使移位计数饱和,而不是换行。移位寄存器宽度或更多会使值归零。

x86 SIMD 位移,如表皮字符 xmm0、32表皮环 xmm1、xmm0 等,使计数饱和;您可以使用 MMX/SSE/AVX 移位移出每个元素的所有位,或者使用 AVX2 vpsllvd/q 在每个元素的基础上移出每个元素的所有位,如果您正在使用 c-192、c-128c-64c 或其他方式计算每个元素的偏移计数,这可能是很好的。 OTOH AVX512VBMI2 VPSHRDVw/d/q SIMD 双移位确实将计数屏蔽到操作数大小 -1,使得不可能让某些元素一直移过边界,而在目标元素中只留下来自 src2 的位。如下文所讨论的386标量shrd所示,这将需要更宽的枪管移位器,或一些高计数的特殊外壳。

186/286具有O(n)移位/旋转(无桶形移位器),因此掩蔽限制了最坏情况下的移位性能。

8086:SHL AX,CL每移位一位需要8个时钟和4个时钟。CL=255的最坏情况是1028个循环。286:5 n,最坏情况下5 31=36个循环。

如果移位不能中止中间指令并且没有任何更慢的指令,则移位计数屏蔽还可以限制多任务系统的最坏情况中断延迟。(286引入了其版本的保护模式,因此可能英特尔正在考虑多用户设置,恶意的非特权用户试图拒绝系统服务。)或者动机可能是真正的代码意外(?)用于大移位计数。此外,如果移位未完全微码化,则无需在专用移位硬件中使计数输入宽度大于5位。建立一个更宽的计数器,这样就可以花费更长的时间,这是没有用的。

更新:186 中的屏蔽计数是新的,排除了多用户公平性,但使用允许大班次计数零寄存器的软件,仍然可以避免最坏情况下的 IRQ 延迟。

16位寄存器的186/286行为需要与现有软件的8086保持足够的向后兼容性。这可能就是为什么屏蔽是5位计数(%32),而不是%16的原因。(对于8位操作数大小,不使用%16%8,也可能使移位计数器HW更简单,而不是根据操作数大小将高位复用为0。)

向后兼容是x86的主要卖点之一。据推测,没有广泛使用的(在8086上)软件依赖于移位计数大于32仍将寄存器归零,否则英特尔可能会通过检查所有高位的零并复用仅使用低4位的移位器的结果来使计数饱和。

但请注意,旋转使用相同的计数掩码,因此检测到高计数的假设硬件必须避免将旋转的结果归零,并且仍然必须使 FLAGS 正确用于正好 32 的偏移和旋转携带。

16位186屏蔽到%32的另一个可能重要的原因是旋转-通过-进位(rcl/rcr),在8086上,计数为16可能是有意义的。(计数mod 9或17将是等效的。)但是,32位rcl不能旋转32;仍然屏蔽到%32。但这不是向后比较的问题;如果任何代码一开始使用RCL/RCR超过1,则可能旋转16到31。(绝对是更晦涩的指令之一。)

因此,可能186的cl%32设计足够兼容,并实现了所需的硬件简化/移位周期上限。

186显然是为嵌入式使用而设计的,并且有些集成设备的地址与IBM-PC冲突,所以英特尔可能觉得他们可以在186中尝试这种变化,看看它是否会引起问题。由于它没有(?),他们保留了286年?这是一个完全虚构的猜测,基于从其他人的评论中提取的一些随机事实。直到Linux在P-MMX奔腾上运行,我才开始使用PC,我只是对这段历史感到好奇,而不是一个逆向计算爱好者。说到这,你https://retrocomputing.stackexchange.com/可能是询问186设计决策的好地方。

为什么386仍然能够用<code>shl eax,32</code>移出所有位?

没有使用32位寄存器的现有软件需要386向后兼容。32位模式(以及16位模式中的32位操作数大小)是386的新增功能。所以386可以选择任何32位移位。(但8和16位移位的工作方式与186/286完全相同,以确保兼容性。)

我不知道Intel是否认为屏蔽移位计数作为一项功能非常有用。屏蔽到与16位移位相同的%32可能是他们最容易实现的,并且可用于32位移位。

根据一些随机的SO评论,386有O(1)个桶形移位器移位。支持更大的移位计数需要更宽的桶形移位器。

386还引入了应该/shrd双精度移位,即从另一个寄存器移位,而不是0或符号位的副本。如果能够将所有位移出并使用应该eax,edx,37作为具有错误依赖关系的复制和移位,那将是很整洁的。但支持很重要

SHLD / SHRD将他们的计数与其他班次的计数区别对待是不一致的,并且除了< code>% 32之外的任何计数都会使其更难构建。

我不确定这个论点是否成立:shld ax, dx,25在理论上会做些什么,但是英特尔目前的手册说,如果计数大于操作数大小,结果是未定义的。(我没有测试实际的硬件来看看会发生什么。)如果其他班次允许更广泛的计数,英特尔可以简单地对386中的32位shld/shld说同样的话。

随机的想法:旋转携带很慢,并且在现代CPU上进行微编码计数!= 1。IDK是否会是另一个复杂因素。

葛海阳
2023-03-14

对于电子产品;如果移位计数是恒定的,您可以什么都不做来移位(就像将“输入位0”的电线连接到“输出位1”的电线,等等)。

您可以将一个变量移位计数分解为多个“带常量计数的移位”操作,最终得到的结果大致如下:

if( (count & 1) != 0) { v = v << 1; }
if( (count & 2) != 0) { v = v << 2; }
if( (count & 4) != 0) { v = v << 4; }
if( (count & 8) != 0) { v = v << 8; }
if( (count & 16) != 0) { v = v << 16; }

当然,这些条件也变得不算什么(它更像是“计数的位0是使能/禁用对1进行恒定偏移的电路的启用/禁用标志”)。问题在于,每个“按常数移位”都取决于前一个“常数移位”的值,因此在“步骤 N”完成之前,您无法开始“步骤 N 1”。步骤之间的同步需要时间,因此更多的步骤(支持更大的计数)会使它更慢。大于寄存器中位数的计数很少见。而且您并不想让常见案例变慢以支持罕见案例。

沈旻
2023-03-14

编辑以更正语句re: 80386,(令我惊讶的是)确实有一个桶形移位器。

很高兴听到286被描述为“现代”:-)

8086运行SHL AX,CL在8个时钟中,每位移位4个时钟。因此,如果CL = 255,这是一个非常缓慢的指令!

所以286帮了大家一个忙,把计数屏蔽为0..31.将指令限制为最多5 ^ 31个时钟。这对于16位寄存器来说是一个有趣的折衷。

[我找到了“80186/80188、80C186/80C188硬件参考手册”(订单号270788-001),其中说这项创新首先出现在那里。SHL等人运行了5个时钟(用于寄存器操作),与286.FWIW相同,186还添加了PUSHA/POPA、PUSH immed、INS/OUTS、BIND、ENTER/LEAVE、INUL immed和SHL/ROL等immed。我不知道为什么186看起来是非个人。]

对于386,他们保留了相同的掩码,但这也适用于32位寄存器移位。我找到了一份“80386程序员参考手册”(订单号230985-001),它为所有寄存器移位提供了3个时钟计数。“英特尔80386硬件参考手册”(订单号231732-002)第2.4节“执行单元”说执行单元包括:

数据单元包含ALU、8个32位通用寄存器文件和64位桶形移位器(在一个时钟中执行多个位移位)。

所以,我不知道他们为什么不屏蔽32位移位到0..63。在这一点上,我只能提出历史的错误理论。

我同意这是一个遗憾,没有一个(GPR)的转变,返回任何计数为零

[我没有尝试过,但我怀疑使用<code>PSLLQ

不管怎样……这种行为的原因似乎是历史。

 类似资料:
  • 我在将netCDF文件(这是一个掩码numpy数组)转换为csv文件时遇到了问题。netcdf文件由12个单独的文件压缩而成,形成一个12 x 52 x 39的3D阵列,其中12对应于月份,52对应于纬度,29对应于经度。 更新:我想要的csv输出是4列,具有网格号(0-437)、纬度、经度和总降水量。例如: 我想总结我12个月的降水量,并将数据浓缩成一列。(我在这里没有找到任何答案来帮助我做这件

  • 我有一个低效的递归硬币变化函数,它计算出给定数量的硬币组合数量。如果可能的话,我想把它转换成一个更有效的迭代函数。 一个问题是,我正在使用回溯来尝试一个叫做面额的数组中的不同硬币。我也在使用记忆法,但当数量很大时,它不会加快速度。 这是我的密码: 有什么想法可以做到这一点吗?我知道有解决硬币更换问题的DP解决方案,但我的解决方案并不容易。我可以有半个便士。 *更新:我将函数改为迭代函数,并将其放大

  • 问题内容: 这应该很简单: 这将导致错误: 0 strconv.ParseInt:解析“ 1250000.0000”:无效的语法 有什么线索吗? 问题答案: 仅适用于可解析为整数的字符串。 您需要的是parseFloat

  • 题目描述 Java 数组扩容问题:实现动态的给数组添加元素效果,实现对数组扩容 原始数组 int[] arr = {1,2,3} 增加的元素 4,直接放在数组的最后 arr = {1,2,3,4} 题目来源及自己的思路 定义 arr1 定义 arr2,比 arr1 的长度长 1 在 arr1 的长度内,把 arr1 的值赋值给 arr2 arr2 的最后一个位置赋值为 4,也就是要加入的数据 因为

  • 问题内容: 缩小转换是指将可以容纳较大值的数据类型放入可以容纳较小值的数据类型。 但是,我不明白为什么将short转换为char会缩小转换范围,但是我有直觉,这与这两种数据类型的有符号/无符号有关,但我无法解释原因。 看起来这将是一个扩大的转换,或者至少不会缩小或扩大,因为它们都是16位并且可以容纳相同数量的值。 问题答案: 这是因为a 可以保持负值,而您可能从中看不到。让我举几个例子。 A(负)

  • 问题内容: 我发现一些代码我工作的地方的点被强制转换,因为它是传递给方法。 为什么要这样做? 我知道这个问题涉及重载的方法,并使用类型转换来确定要调用的方法的版本。 但是,如果不执行强制类型转换,如果使用空参数调用该方法,那么是否会重选带有其他类型的参数的重载方法呢?那么演员阵容还能完成什么呢? 问题答案: 如果 未 执行转换,则将选择 最具体的 版本。 可以是type 或type 的空引用。因此