为什么实施无符号数字?


12

我不知道为什么微处理器系统实现无符号数字。我猜代价只是条件分支数的两倍,因为大于等值的.etc需要与带符号的算法不同的算法,还有无符号数对它有很大好处的算法吗?

我的问题部分是为什么它们需要位于指令集中而不是由编译器支持?


27
基本上,无符号数字是标准的,带符号的实现是为了提供负数。
Pieter B

37
世界上许多数据都是非数字的。使用非符号类型可以轻松地处理非数字数据。Java没有无符号数字类型是失败的,这会导致必须处理非数字数据(例如压缩等)的许多错误。
埃里克·艾德

6
@jtw Erik说,没有负像素颜色或负字符之类的东西。因此,为此使用带符号整数会很浪费,您将放弃一半的地址空间。
马丁·马特

26
我不确定我是否一个人在这里,但是我发现在开发应用程序时需要带符号的整数非常罕见。几乎所有时间我都需要一个(无符号)自然数(通常为正数)或一个有符号浮点数。例外是诸如货币之类的东西,但很少见。对我来说,无符号整数是常态,而有符号整数是例外!
托马斯

11
从CPU的角度来看,几乎所有数字都是无符号的。一些指令可以将这些位解释为带符号的(例如,算术右移),但实际上二进制补码使CPU可以将带符号的整数视为无符号的整数,这意味着CPU不需要(或很少)特殊电路来支持这两个。
Cornstalks

Answers:


39

无符号数字是对位序列的一种解释。这也是CPU内部最简单,最常用的解释,因为地址和操作码只是位。内存/堆栈寻址和算术是微处理器处理的基础。在抽象金字塔之上,对位的另一种常见解释是作为字符(ASCII,Unicode,EBCDIC)。然后还有其他解释,例如IEEE浮点,图形的RGBA等。这些都不是简单的带符号数字(IEEE FP并不简单,使用这些数字的算法非常复杂)。

同样,使用无符号算法,很容易(如果不是最有效的话)实现其他算法。反之则不正确。


3
EBCDIC只有一个“ I”。
Ruslan

4
@Ruslan-但音像它有两个。<g>
皮特·贝克尔

5
@PeteBecker不,不是。EBCDIC的发音为eb -see-dick。
Mike Nakis '16

19

比较操作的大部分硬件成本是减法。用于比较的减法输出本质上是三个状态位:

  • 是否所有位都为零(即相等条件),
  • 结果的符号位
  • 减法的进位位(即32位计算机上的第33个高位)

通过在减法运算之后测试这三个位的适当组合,我们可以确定所有带符号的关系运算以及所有无符号的关系运算(这些位也是如何检测溢出,有符号还是无符号)。可以共享相同的基本ALU硬件来实现所有这些比较(更不用说减法指令了),直到对这三个状态位进行最终检查为止,这根据所需的关系比较而有所不同。因此,它不是很多额外的硬件。

唯一的实际成本是需要对指令集体系结构中的其他比较模式进行编码,这可能会稍微降低指令密度。尽管如此,硬件具有许多给定语言未使用的指令还是很正常的。


1
比较无符号数字不需要减法。它可以通过从左到右按位比较来实现。
乔纳森·罗森

10
@JonathanRosenne但这不是处理器实现它的方式。相反,对于2的补码处理器在其ALU中不执行减法(带或不带进位/借位)几乎是不可想象的。设计师立即想到的是,使用这种必要的ALU杀死另一只具有相同石头的鸟,以作比较。然后,比较只是简单地减去了,结果没有写回到寄存器文件中。
Iwillnotexist Idonotexist

4
+1:这是对所提问题的正确答案。总结:因为在您已经实现signed时,实现无符号操作几乎是免费的
Periata Breatta '16

10
@PeriataBreatta它也可以反过来工作。现代CPU中的带符号和无符号数字几乎相同,这是OP无法识别的要点。甚至有符号和无符号的比较指令都是相同的-这就是二进制补码赢得有符号整数战争的原因之一:)
Luaan

3
@svidgen>就像其他答案所说的那样,它的工作方式却相反。主要关注的是无符号数字,它基本上用于所有内容(内存地址,io /端口,字符表示等)。一旦您未签名,带签名的数字就会变得便宜,并且在罕见的情况下派上用场。
频谱

14

因为,如果您需要计数始终为的值 >= 0,则不必要使用带符号整数将计数空间减少一半。

考虑一下您可能要放在数据库表中的自动递增的INT PK。如果你使用一个有符号整数那里,你的表存储一半的记录,因为它可能为同一字段大小而没有益处。

或RGBa颜色的八位字节。我们不想笨拙地开始将这个自然为正数的概念计为负数。一个有符号的数字会破坏思维模式或使我们的空间减半。无符号整数不仅与概念匹配,而且提供两倍的分辨率。

从硬件的角度来看,无符号整数很简单。它们可能是执行数学运算的最简单的位结构。而且,毫无疑问,我们可以通过在编译器中模拟整数类型(甚至是浮点!)来简化硬件。那么,为什么在硬件中同时实现无符号和有符号整数

好吧…… 表现!

在硬件中实现带符号整数比在软件中实现效率更高。可以指示硬件在一条指令中对任一整数类型执行数学运算。这非常好,因为硬件或多或少并行地将位粉碎在一起。如果您尝试在软件中进行模拟,那么选择“模拟”的整数类型无疑将需要许多指令,而且速度明显慢。


2
沿着这些思路,您可以在进行数组边界检查时节省自己的操作。如果使用无符号整数,则只需检查提供的索引是否小于数组大小(因为它不能为负数)。
riwalk

2
@ dan04当然可以...但是,如果您使用的是从0或1开始的自动递增int,这是很常见的做法,那么您将无法使用一半的可用数字。并且可以想象,虽然您可以从-2 ^ 31(或其他任何值)开始计数,但是在id空间的中间会出现一个潜在的“边缘”情况。
svidgen '16

1
但是,将自己的领域缩小一半是一个很弱的论点。如果您的应用程序需要的数量超过20亿,也有可能超过40亿。
corsiKa '16

1
@corsiKa:因此,如果需要的数量超过4,则可能需要8,然后是16,等等。它在哪里结束?
whatsisname

1
@whatsisname通常,您使用8、16、32或64位的整数类型。之所以说说无符号的比较好,是因为在大多数情况下,在签名字节中获得所有32位而不是正整数空间的31位的有限范围并不重要。
corsiKa '16

9

您的问题包括两部分:

  1. 无符号整数的用途是什么?

  2. 无符号整数值得麻烦吗?

1.无符号整数的用途是什么?

简单来说,无符号数字代表一类负数没有意义的数量。当然,您可能会说“我有多少个苹果?”这个问题的答案。如果您欠某人一些苹果,则可能为负数,但是“我有多少内存”的问题呢?-您的内存容量不能为负。因此,无符号整数非常适合表示此类数量,并且它们的优点是能够表示正值范围的两倍。例如,您可以使用16位带符号整数表示的最大值是32767,而使用16位无符号整数可以表示的最大值65535。

2.无符号整数值得吗?

无符号整数并不真正代表任何麻烦,因此,是的,它们是值得的。您会看到,它们不需要额外的“算法”集;实现它们所需的电路是实现有符号整数所需的电路的子集。

CPU没有带符号整数的乘数,而没有符号整数的乘数。它只有一个乘法器,根据运算的性质,其工作方式略有不同。支持有符号乘法比无符号乘法需要更多的电路,但是由于无论如何都需要支持,无符号乘法实际上是免费的,因此它包含在软件包中。

至于加减法,电路完全没有区别。如果您读过所谓的整数的二进制补码表示法,就会发现它的设计是如此巧妙,以至于无论整数的性质如何,这些运算都可以完全相同的方式执行。

比较也以相同的方式工作,因为它只不过是减除结果,所以唯一的区别是条件分支(跳转)指令,该指令通过查看由CPU设置的不同标志来工作。前面的(比较)指令。在以下答案中:https : //stackoverflow.com/a/9617990/773113您可以找到有关它们如何在Intel x86架构上工作的解释。发生的情况是将条件跳转指令指定为有符号还是无符号取决于它检查的标志。


1
我的问题假设了所有这些,通过算法我意味着小于等大于等的规则是不同的。我看到的成本是有很多额外的说明。如果高级程序喜欢将数据视为位模式,则可以很容易地由编译器实现
jtw 16/10/14

3
@jtw-但关键是这些额外的指令实际上与带符号的数字所需的指令非常相似,并且几乎所有需要的电路都可以共享。实现这两种类型的额外成本几乎为零。
Periata Breatta '16

1
是的,这回答了我的问题,添加额外的分支指令成本很小,而且在实践中通常很有用
jtw

1
“在进行除法和乘法运算时,“无符号运算需要一些额外的处理”。使用无符号值,乘法和除法更容易。处理带符号的操作数需要额外的处理。
科迪·格雷

@CodyGray我知道有人会说出来。你是对的,当然。这就是我的陈述背后的原因,为了简洁起见,我最初将其省略:CPU无法提供仅无符号的乘法和除法,因为有符号的版本是如此有用。实际上,必须进行有符号的乘法和除法;无符号是可选的。因此,如果提供无符号的,则可以认为这需要更多的电路。
Mike Nakis

7

微处理器本质上是无符号的。带符号的数字是实现的东西,而不是相反的东西。

计算机可以并且确实可以在没有带正负号的情况下正常工作,但是对我们来说,需要负数的人类因此发明了带正负号。


4
许多微处理器具有用于各种操作的已签名和未签名指令。
whatsisname

1
@whatsisname:相反:许多微处理器只有未签名的指令。一个很少有签名的说明。这是因为使用2s补码算法时,无论天气如何,该位值都是相同的,该数字是带符号的还是无符号的,并且如何读取该数字仅是解释的问题-因此,将其实现为编译器功能更为容易。通常,只有假定程序员不使用编译器的旧微型计算机才具有花哨的已签名指令,以使汇编代码可读。
slebetman '16

3

因为它们还有一点可以轻松存储,所以您不必担心负数。没有什么比这更多的了。

现在,如果您需要一个示例,说明需要在何处使用此额外功能,那么如果您看的话,可以找到很多东西。

我最喜欢的示例来自国际象棋引擎中的位板。国际象棋棋盘上有64个正方形,因此unsigned long可以为围绕移动生成的各种算法提供完美的存储。考虑到您需要进行二进制运算(以及移位运算!)的事实,很容易理解为什么不用担心如果设置了MSB会发生什么特殊的事情。可以使用长签名来完成,但是使用未签名容易得多。


3

具有纯数学背景,对于任何有兴趣的人来说,这都是数学上的一点点。

如果我们以一个8位有符号和无符号整数开始,那么就加法和乘法而言,我们所拥有的基本上是模256的整数,前提是使用2的补码表示负整数(这就是每个现代处理器的方式) 。

区别在两个地方:一个是比较操作。在某种意义上,最好将整数256模作为一个数字圈(就像在老式的模拟表盘上以12的整数模一样)。为了使数值比较(x <y)有意义,我们需要确定哪些数字小于其他数字。从数学家的角度来看,我们希望以某种方式将256模整数嵌入所有整数的集合中。显而易见,将二进制表示形式为全零的8位整数映射为整数0。然后,我们可以映射其他对象,以使“ 0 + 1”(将寄存器清零,例如ax,并通过“ inc ax”将其递增1的结果)变为整数1,依此类推。我们可以对-1进行同样的操作,例如将“ 0-1”映射到整数-1和“ 0-1-1” 到整数-2。我们必须确保此嵌入是一个函数,因此不能将单个8位整数映射到两个整数。因此,这意味着如果我们将所有数字映射到整数集中,则将有0,还有一些小于0且大于0的整数。使用8位整数(根据到您想要的最小值(从0到-255)。然后可以根据“ 0 <y-x”来定义“ x <y”。

有两种常见的用例,对它们的硬件支持是明智的:一种使用所有非零整数都大于0的情况,一种使用约50/50围绕0的整数。通过使用额外的“加号”转换数字可以轻松地模拟所有其他可能性。和sub'之前的操作,对此的需求如此之少,以至于我无法想到现代软件中的一个明确示例(因为您可以使用更大的尾数(例如16位))。

另一个问题是将8位整数映射到16位整数空间中的问题。-1会变成-1吗?如果0xFF表示-1,这就是您想要的。在这种情况下,符号扩展是明智的选择,因此0xFF变为0xFFFF。另一方面,如果0xFF表示255,则希望将其映射到255,因此映射到0x00FF,而不是0xFFFF。

这也是“移位”和“算术移位”操作之间的区别。

但是,最终归结为以下事实:软件中的int不是整数,而是二进制的表示形式,并且只能表示某些形式。设计硬件时,必须选择在硬件中本机执行的操作。由于使用2的补码,加法和乘法运算是相同的,因此以这种方式表示负整数是有意义的。然后,仅取决于二进制表示要表示哪个整数的运算问题。


我喜欢数学方法,但我不认为只考虑将其提升为更大的特定大小,而是对无限长的二进制数进行运算更好地归纳为更好。从最右边的k位为0的任何数字中减去1,结果的最右边的k位为1,可以通过归纳证明如果一个人用无限数量的位进行数学运算,则每个位都将为1。对于无符号数学中,除了数字的最低位以外,其他所有位都忽略。
超级猫

2

让我们检查一下将无符号整数添加到具有现有带符号整数的CPU设计的实现成本。

典型的CPU需要以下算术指令:

  • ADD(添加两个值并在操作溢出时设置标志)
  • SUB(从另一个值中减去一个值并设置各种标志-我们将在下面讨论)
  • CMP(本质上是'SUB并丢弃结果,仅保留标志')
  • LSH(左移,在溢出时设置标志)
  • RSH(右移,如果移出1,则设置标志)
  • 上述所有指令的变体,用于处理标志的进位/借位,因此使您可以方便地将指令链接在一起,以对比CPU寄存器更大的类型进行操作
  • MUL(乘法,设置标志等-并非通用)
  • DIV(除法,设置标志等-许多CPU体系结构都缺少此功能)
  • 从较小的整数类型(例如16位)移动到较大的整数类型(例如32位)。对于有符号整数,通常将其称为MOVSX(带符号扩展的移动)。

它还需要逻辑指令:

  • 零分
  • 更大的分支
  • 少分支
  • 溢出分支
  • 上述所有内容的否定版本

要对有符号整数比较执行上述分支,最简单的方法是让SUB指令设置以下标志:

  • 零。如果减法得出零值,则设置。
  • 溢出。设置减法是否从最高有效位借入了一个值。
  • 标志。设置为结果的符号位。

然后算术分支的实现如下:

  • 零分支:如果设置了零标志
  • 较少分支:如果符号标志与溢出标志不同
  • 更大的分支:如果符号标志等于溢出标志,并且清零标志。

从这些实施方式的角度来看,显然应该否定它们。

因此,您现有的设计已经为带符号整数实现了所有这些功能。现在,考虑添加无符号整数所需要做的事情:

  • ADD-ADD的实现是相同的。
  • SUB-我们需要添加一个额外的标志:当从寄存器的最高有效位之外借入一个值时,将设置进位标志。
  • CMP-不变
  • LSH-不变
  • RSH-带符号值的右移保留了最高有效位的值。对于无符号值,我们应该将其设置为零。
  • MUL-如果您的输出大小与输入大小相同,则不需要特殊处理(x86 确实具有特殊处理,但这仅是因为它已将输出输出到寄存器对中,但是请注意,实际上很少使用此功能,因此比无符号类型更明显的候选者可以退出处理器)
  • DIV-无需更改
  • 从较小的类型移动到较大的类型-需要添加MOVZX,零扩展移动。注意,MOVZX的实现非常简单。
  • 零分支-不变
  • 较少分支-设置进位标志时跳转。
  • 更大的分支-如果进位标志和零都清除则跳转。

请注意,在每种情况下,修改都是非常简单的,并且可以通过打开或关闭一小部分电路,或者添加一个新的标志寄存器来实现,而该标志寄存器不能由需要计算的值控制。无论如何,指令的实现。

因此,添加未签名指令的成本非常小。至于为什么要这样做,请注意,内存地址(和数组中的偏移量)本质上是无符号的值。由于程序要花费大量时间来处理内存地址,因此具有正确处理它们的类型可以使程序更易于编写。


谢谢你,这回答了我的问题,成本很小,说明经常有用
jtw

1
无符号双倍乘法在执行多精度算术时必不可少,并且在执行RSA加密时可能比总体速度提高2倍更好。同样,在有符号和无符号的情况下除法是不同的,但是由于无符号的情况更容易且除法非常稀少和足够慢,因此添加几条指令不会带来太大的伤害,所以最简单的方法是实现无符号除法然后使用一些符号处理逻辑对其进行包装。
超级猫

2

存在无符号数主要用于处理需要环绕的代数环的情况(对于16位无符号类型,它将是整数模65536的整数环)。取一个值,添加小于模量的任何数量,两个值之间的差即为所添加的数量。举一个实际的例子,如果一个公用事业电表在一个月初读数为9995,而一个人使用23个单位,则该电表在月底读数为0018。使用代数环类型时,无需执行任何特殊处理即可处理溢出。从0018减去9995将得到0023,恰好是使用的单位数。

在PDP-11(最初为其实现C的机器)上,没有无符号整数类型,但是有符号类型可用于模块化算术,该算法封装在32767和-32768之间,而不是在65535和0之间。但是,平台无法将内容包裹得很干净;而不是要求实现必须模拟PDP-11中使用的二进制补码整数,该语言改为添加了无符号类型,这些类型大多数必须表现为代数环,并允许有符号整数类型在发生溢出的情况下以其他方式运行。

在C的早期,有很多数量可以超过32767(公共INT_MAX),但不能超过65535(公共UINT_MAX)。因此,使用无符号类型来保存这样的数量(例如size_t)变得很普遍。不幸的是,语言中没有什么可以区分应该表现得像带正数位的数字的类型和应该表现出像代数环一样的类型。相反,该语言使小于“ int”的类型表现为数字,而全尺寸类型表现为代数环。因此,调用如下函数:

uint32_t mul(uint16_t a, uint16_t b) { return a*b; }

与(65535,65535)在int具有16位(即返回1)的系统上具有一个已定义的行为,对于int具有33位或更大(返回0xFFFE0001)的系统具有不同的行为,而在“ int”位于以下位置的系统上具有未定义的行为-之间(请注意,gcc 通常会产生算术上正确的结果,结果介于INT_MAX + 1u和UINT_MAX之间,但有时会为上述函数生成代码,但该值失败!)。不是很有帮助。

尽管如此,缺少始终像数字一样运行或始终像代数环那样运行的类型并不能改变代数环类型对于某些编程几乎不可缺少的事实。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.