什么是Complement(补码)?(转)

http://www.tutorialspoint.com/computer_logical_organization/complement_arithmetic.htm

https://www.cnblogs.com/PI3141592657/p/7134474.html
大学上过计算机原理课程的朋友都接触过补码这个概念,不过当时书上所教授的内容都是以二进制作为前提,即所谓的2的补码(2's Complement)。近来看TCP/IP Volume 1时,又接触到“1的补码”这个概念,忽然发现其实还不太明白补码到底是什么意思,故查阅资料记录之。

资料来源:维基百科

术语解释:Radix —— 基数,在本篇文章的范畴内等价于“进制”

定义:给出长度为n的数值y,则y的以基数b的补码为: b^n - y (即 b的补码)

水平有限,翻译的比较拗口,不过公式还是很简洁的,实际上补码从定义上来说并没有什么难懂的地方。不过有些地方需要加以说明,补码这个概念是建立在进制(即基数)的基础上的,至少在计算机科学的术语中,补码定义中的基数b是一定等于当前进制的,也就是说以上定义可以简化为

给出长度为n的数值y且该数值为b进制,则y的补码为: b^n - y

下面以十进制的数作为例子

给出数值y = 1234,很明显长度n = 4,基数b = 10(你说不知道10从哪来的?论审题的重要性)

根据补码的定义那么 y的补码为:10^4 - 1234 = 8766

用途:补码的作用是什么呢?你走运啦,补码的用途很专一 —— 用加法操作来代替减法操作

听起来匪夷所思,你几乎要脱口而出:“不可能!!!”,不过按照国际惯例,我们先来看看到底怎么回事吧

这里要引入另一个很简单但是英文又很有bigger的术语: 缩小基数补码 (diminished radix complement),看到 diminished 这个词我立马打个激灵,想起了久未谋面的缩小增量排序(diminished increment sort),啥?你说老师没教过这个?噢,它的另一个名字叫希尔排序,它是。。。咳咳,不好意思跑题了。

缩小基数补码实际上就是 (bn - 1) - y,就是说你可以通过往缩小基数补码上加个 1 来得到基数补码,也就是说

bn - y = (bn - 1) - y + 1

是不是想大喊一声:这TM不是废话么??

这个概念有什么用?其实在纯数学的范畴内,这个纯属多余,没有任何用处。然而到了我这个年纪,就会明白一切看起来无意义的东西,肯定一定必须存在一个让它拥有意义的上下文环境。在本文内,这个环境就是“一个数值的长度”,你要知道在数学范围内,你想把一个数写多长就有多长,但是在计算机内数值长是固定的,譬如Java语言的int数值长度为32位,你无法用32位的数去表示33位数,当然34位就更不行了!

回到基数b这个关键字上面来,还是以十进制数为例子

假设数值长度为固定的4,给出数值y = 1234,如果说你真的要按照定义在计算机内去获得补码,你是做不到的,因为根据定义补码为104 - 1234,然而104 = 10000,已经超出了4位数所能表示的范围,你明白了么?缩小基数补码就是为了能在固定的数值长度中去获得补码,所以退一步海阔天空啊

104 - 1 = 9999
9999 - 1234 = 8765
8765 + 1 = 8766
然而,聪明如你一定发现了,说是补码的用途是用加法代替减法,可是在以上第二步获得补码的关键步骤里,不还是要进行减法??这有毛区别?哈,这个就是最让人兴奋的地方,在二进制世界里,你不需要再用减法了,下面以二进制为例子

给出数值y = 1011,那么很明显,按照最新的补码求解步骤,补码 = ( 24 - 1 ) - 1011 + 1,猛然一看,这哪里履行了补码的承诺,用加法代替减法?那么我们就以二进制的视角去看

y    = 1011

24 - 1 = 1111

实际上你已经发现了,这个缩小基数补码是固定的——给定计算机数值长度n,则缩小基数补码可以直接写出:pppp...ppp(n个p,p = 进制 - 1),而关键的

1111 - 1011,这一步实际上已经不需要作减法操作了,直接对 y 取反再加上 1 就能得到补码了(是不是对“取反加一”感到特别耳熟?你是个上课听课的好孩纸),在此不得不感谢伟大的二进制!需要再次声明下,这么流畅的操作只有二进制能完成,其他进制想要获得补码,依然需要减法。

终章:在神奇的二进制世界中,获得一个数的补码只需要简单的取反再加上一就可以了。那么最后我们来看看,补码到手之后,又怎么能代替减法呢?

假设需要求解 x - y ( x >= y),那么分为以下步骤

求得y的补码 bn - y
x - y = x + ( bn - y ) = x - y + bn (这一步用补码的加法代替了原数值的减法)
显然 x - y + bn >= bn,然而bn已经超出了数的表示范围(overflow),直接被丢弃了,最后的结果就等于x - y
1的补码:这个世界是没有1进制的,所以1的补码是一个缩小基数补码,也就是直接对一个二进制数值取反

标签: 补码

整数二进制补码的数学原理(two's complement)
最近重新学习CPU体系结构,对使用二进制补码原理来消除带符号数和无符号数计算差异,以及整合减法运算器到加法运算器,从而简化CPU硬件设计的原理很感兴趣,所以特地思考了下,查看了一些网上关于two's complement的文章,但大部分还是太过学术,经过整理,我想以一种比较简洁的方式表达出来。为了简单起见,我使用了4位字长的寄存器作为例子,32位和64位道理一样。想了解补码更为科学的数学原理可以参考wikipedia关于one's complement、two's complement的相关文章。

硬件设计以简洁为目标,所以整数的运算最好只有加法,而且不用对符号位进行特殊处理,能达到这个目标吗?当然可以,那就是使用补码(two's complement),所谓补码其实是针对带符号数来说的,其意思就是正数使用原码,而负数使用2的字长的指数减负数的绝对值表示,即x = pow(2, word_length) - abs(x),这个补码的简单计算方法就是我们计算机书中常说的,将x绝对值取反加1。现在你知道补码的真正计算方法了吧。为什么要将负数表示成这样呢?这是有数学原理的,这正是本文需要阐释的内容,充分了解后对CPU常用指令编程就打下了坚实的基础(general purpose instructions都是针对整数的),以后可能还会增加关于浮点数计算规范的文章。

现在我们来看一个减法:
7 - 6 (式1)
能把它变成一个加法吗?我来试试:
7 + (16 - 6)- 16 (式2)
16是4位寄存器最小的溢出数(24 表示pow运算),以上两个式子是完全等价的,在我们看来比较繁琐的第二个式子却正是CPU内部整数计算单元所采用的方法,由于一些特殊原因,CPU只需要计算第一个加法,其余两个减法分别由编译器、人或寄存器自动截断完成了。
经过前面的叙述,我们知道了16 - 6就是-6在4位字长机器上的补码,这步计算一般是编译器完成的,将负数直接存储为补码形式,这里是1010。我们来看看CPU如何计算:
0 1 1 1 (7)

  • 1 0 1 0 (10)
    ----------- ----------
    1 0 0 0 1 (17)

以上式子完成了式(2)中前两步计算,还需要减16才能得到正确结果,神奇的地方到了,因为机器是4位字长,所以第五位1直接丢弃掉了,就是溢出,这相当于自动减了16,所以最后结果就是0001,等于1,完成了式2个所有计算,得到了正确结果,现在你应该明白了为什么会选择最小溢出数所为补码转换中的被减数了吧,就是为了完成自动溢出,从而实现最后的减法。

再来看看2个负数相加,看看CPU是如何把它们当纯粹二进制运算而结果却丝毫不差的:
(-6) + (-7) (式3)
依据上面的规律换成如下式子
[(16 - 6) - 16] + [(16 - 7) - 16] (式4)
其中(16 - 6)和(16 - 7)部分已经由编译器完成,就是对应负数的补码,让我们来看看CPU的计算内容:
1 0 1 0 (10)

  • 1 0 0 1 (9)
    ----------- ------
    1 0 0 1 1 (19)

式4中还需要减2个16,这里第5位已经自动溢出减了一个16,我们还要减一个16才能得到正确结果,可是寄存器中结果0011,光凭这个结果,我不知道这到底是最终值还是还需要减16,这可太糟糕了,产生这个问题的原因是如果使用全部4位寄存器存储值时,会产生带符号数二进制歧义问题,打个比方,-9用二进制补码表示是(16 - 9),二进制为0111,居然和整数的7是一样的,光凭这串二进制我无法知道它是-9还是7,好吧,我确实聪明,想到了一个办法,嘿嘿,让我们来看看4位寄存器能存储的二进制有哪些:
0 0 0 0
0 0 0 1
0 0 1 0
0 0 1 1
0 1 0 0
0 1 0 1
0 1 1 0
0 1 1 1
1 0 0 0
1 0 0 1
1 0 1 0
1 0 1 1
1 1 0 0
1 1 0 1
1 1 1 0
1 1 1 1
我可以将最高位数字当作解释数字符号的标志,如果是0我就当正数解释,如果是1我就当负数解释,当正数解释后不用再减16,直接就是最终结果,而如果是负数则还需要减个16才是最终结果,因为我们是用16-x来表示-x的,正好正数负数对半(假设0是正数),再回到上面那个问题,(-6) + (-7)CPU寄存器中最终为0011,我当应该当正数解释,正数不用减16,所以最后等于3,不对!应该是-13,还需要减个16才对,可我们刚说了正数不用减。到底哪里出问题了?大家思考下。
原来如果我们将二进制用以上阐述的方式解释,决定了4位二进制表示的数的范围只能是[-8~7],实际上寄存器如果从左端溢出的话,其值是在[...0\1\2\3\4\5\6\7-8-7-6-5-4-3-2-1\0...]不断循环的,也就是说刚才的-13从-8向左数5位,又循环回到了3,我们必须有办法判断溢出情况,如果我们把寄存器中的二进制当无符号数解释,那很简单,只要最高为产生进位那就溢出了,可如果当带符号数解释,如何看最后的值是否溢出呢?
这是个补码数学原理的精髓所在,有了这个推理,CPU才能做到同等处理带符号数和无符号数,我们来仔细分析下数学上的原理,在CPU看来寄存器中的就是纯粹的二进制,相当于无符号数,如果两个数相加时,最高位产生了进位,则表示结果肯定位于[16~30],如果次高位向最高位产生了进位则表示结果低3位相加结果范围位于[8~14],由于最高位溢出被丢弃,表示对最终结果减了16,而次高位向最高位产生进位,表示最终结果最小为8,现在就有如下几种结论:
(1)最高位有进位,次高位有进位,则最终结果位于[-8~6]
(2)最高位有进位,次高位无进位,则最终结果位于[-16~-9]
(3)最高位无进位,次高位有进位,则最终结果位于[8~14]
(4)最高位无进位,次高位无进位,则最终结果位于[0~7]
而我们带符号的解释方法,决定了数的范围为[-8~7],怎么样一眼就看出该如何判断带符号数计算是否溢出了吧!

然我们来看看CPU EFLAGS寄存器中最常用的6个标志位CF,PF,AF,ZF,SF,OF,我只解释CF和OF,其余的4个都很好理解,CF表示两个操作数进行二进制整数计算时最高位发生了进位,很显然可以用来判断无符号数是否溢出,而OF是寄存器次高位是否向最高位发生进位的标志(进位1,否则为0)与CF位的XOR值,是不是很神奇,就是我们最后阐述的四项规则,正好用来判断带符号整数是否溢出。

是不是无法想象在一般书上一笔带过的整数计算用的补码规范后面却隐藏了这么多原理,正是这些特性,决定了处理器设计时采用二进制补码进行整数计算,他使带符号数和无符号数的加减运算全部用无符号数加法运算实现,使电路实现大为简化,增加了处理器效率,减少了设计制造成本。但整数的乘法/除法运算却无法这样处理,这就是为什么有带符号的乘除指令而加法和减法却没有,从一定意义上讲,其实减法指令只是加法指令的一个包装,因为CPU内部没有减法逻辑,只有加法。
https://www.cnblogs.com/effulgent/archive/2011/10/30/two_s_complement.html

猜你喜欢

转载自www.cnblogs.com/yasepix/p/11490926.html