计算机中的整数运算

前言

本文章主要介绍整数运算的内存表示,不是介绍运算的过程,具体运算的过程需要CPU逻辑处理单元的介入,这个后面的文章在介绍。本文主要分为下面几个方面:
无符号整数的加法
有符号整数的加法
无符号整数的乘法
有符号整数的乘法
乘以常数的优化
无符号整数的除法
有符号整数的除法

前置

  1. 无论是计算两个有符号整数之和还是计算两个无符号整数之和,对于大部分计算机来说,使用相同的指令,也就是说对于逻辑处理单元来说是一样的。由此我们能够推断出,如果计算两个有符号整数之和,我们可以按照如下的策略:
    • 把补码按照原码进行解释,也就是有符号数变成了无符号数
    • 然后把两个无符号数进行相加
    • 把相加的结果按照补码解释,就得到正确的结果了
  2. 无论是计算两个有符号整数乘积还是计算两个无符号整数乘积,对于大部分计算机来说,使用相同的指令,也就是说对于逻辑处理单元来说是一样的。
  3. 在大多数机器上,整数乘法指令相当慢,需要10 个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运算和移位) 只需要1个时钟周期。

无符号整数的加法

先看下面的例子:

unsigned short a = 40000;
unsigned short b = 40000;
unsigned short c = a + b;
printf("a = %hu, b = %hu, c = %hu\n",a,b,c);

运行结果如下:

a = 40000, b = 40000, c = 14464

为什么c=14464呢? ↓ ↓ ↓ \downarrow\downarrow\downarrow ↓↓↓

对于无符号整数,都有一个范围,比如short类型占用两个字节,那么对于unsigned short a a a,有
0000000000000000 ≤ a ≤ 1111111111111111 0000000000000000\le a \le1111111111111111 0000000000000000a1111111111111111
⇒ 0 ≤ a ≤ 2 16 − 1 \Rightarrow 0\le a \le2^{16}-1 0a2161
如果还有另一个unsigned short b b b,则
0 ≤ c 1 = a + b ≤ 2 17 − 2 0\le c_1 = a+b \le2^{17}-2 0c1=a+b2172
显然当 c 1 > 2 16 − 1 c_1>2^{16}-1 c1>2161时,两个字节无法容纳 c 1 c_1 c1的值,这个时候就发生了溢出,计算机对这种溢出的处理非常简单,就是直接舍弃,或者截断。所以对这种情况, c c c的值应该是: c = c 1 − 2 16 c = c_1 - 2^{16} c=c1216
我们对无符号整数的运算进行一下总结:
假设 a a a b b b都是占用 w w w位的无符号整数,则 a + b a+b a+b的运算结果如下:
c = { a + b , a + b < 2 w a + b − 2 w , 2 w ≤ a + b ≤ 2 w + 1 − 2 c= \begin{cases} a + b,&a+b<2^w\\ a+b-2^w,&2^w\le a+b \le2^{w+1}-2 \end{cases} c={a+b,a+b2w,a+b<2w2wa+b2w+12

简单的一句话概括就是 c = ( a + b ) m o d    2 w c=(a+b)\mod {2^w} c=(a+b)mod2w

我们看最开始的例子:
a = 40000 = 0 b 1001110001000000 a=40000=0b1001 1100 0100 0000 a=40000=0b1001110001000000
b = 40000 = 0 b 1001110001000000 b=40000=0b1001 1100 0100 0000 b=40000=0b1001110001000000
a + b = 40000 = 0 b 10011100010000000 a+b=40000=0b1 0011 1000 1000 0000 a+b=40000=0b10011100010000000
舍弃最高位后
c = 0 b 0011100010000000 = 14464 c = 0b0011 1000 1000 0000 = 14 464 c=0b0011100010000000=14464

如何判断无符号整数相加发生了溢出

如果两个无符号整数相加 ( a + b > a   ∣ ∣   a + b > b ) (a+b>a\ ||\ a+b>b) (a+b>a ∣∣ a+b>b),我们就说发生了溢出

bool isFull(unsigned short a,unsigned short b)
{
    unsigned short c = a + b;
    return (c>a) || (c>b);
}

有符号整数的加法

两个有符号整数相加情况复杂一些,看下面的例子:

signed short a = 20000;
signed short b = 20000;
signed short c = a + b;
printf("a = %hd, b = %hd, c = %hd\n",a,b,c);
    
signed short a1 = -20000;
signed short b1 = -20000;
signed short c1 = a1 + b1;
printf("a1 = %hd, b1 = %hd, c1 = %hd\n",a1,b1,c1);

运行结果如下:

a = 20000, b = 20000, c = -25536
a1 = -20000, b1 = -20000, c1 = 25536
  • 为什么两个负数相加会变成整数?
  • 为什么两个正数相加会变成负数?
  • 这个结果是怎么计算出来的?
    ↓ ↓ ↓ \downarrow\downarrow\downarrow ↓↓↓

对于一个占用 w w w位内存的有符号整数 a 、 b a、b ab,取值范围满足
− 2 w − 1 ≤ a , b ≤ 2 w − 1 − 1 -2^{w-1}\le a,b \le 2^{w-1}-1 2w1a,b2w11
并且 a 、 b a、b ab在内存中是采用补码表示的。

根据前置1的说明,有符号整数的相加可以通过解释成无符号整数相加,然后在把结果按照有符号整数解释出来即可。根据补码的计算公式(可参照深入理解计算机中的整数
a = − x w − 1 × 2 w − 1 + ∑ i = 0 w − 2 ( x i 2 i ) a=-x_{w-1}\times2^{w-1} + \sum_{i=0}^{w-2}(x_i2^i) a=xw1×2w1+i=0w2(xi2i)
如果按照无符号整数解释
a 1 = x w − 1 × 2 w − 1 + ∑ i = 0 w − 2 ( x i 2 i ) = a + x w − 1 2 w − 1 + x w − 1 2 w − 1 = a + x w − 1 2 w \begin{aligned} a_1&=x_{w-1}\times2^{w-1} + \sum_{i=0}^{w-2}(x_i2^i)\\ &= a + x_{w-1}2^{w-1}+x_{w-1}2^{w-1} \\[1.5ex] &=a + x_{w-1}2^w\\ \end{aligned} a1=xw1×2w1+i=0w2(xi2i)=a+xw12w1+xw12w1=a+xw12w
同理
b 1 = b + x w − 1 2 w b_1= b + x_{w-1}2^w b1=b+xw12w
⇒ a 1 + b 1 = a + x w − 1 2 w + b + x w − 1 2 w \Rightarrow a_1+b_1= a+x_{w-1}2^w+b+ x_{w-1}2^w a1+b1=a+xw12w+b+xw12w
⇒ a 1 + b 1 = a + b + m 2 w \Rightarrow a_1+b_1=a+b+m2^w a1+b1=a+b+m2w

  1. 如果 − 2 w ≤ a + b < − 2 w − 1 -2^w\le a+b<-2^{w-1} 2wa+b<2w1,此时 m > 0 m>0 m>0并且 0 ≤ a + b + 2 w < 2 w − 1 0\le a+b+2^w <2^{w-1} 0a+b+2w<2w1
    ⇒ a 1 + b 1 = ( a + b + 2 w ) + ( m − 1 ) 2 w \Rightarrow a_1+b_1=(a+b+2^w)+(m-1)2^w a1+b1=(a+b+2w)+(m1)2w
    因为 a 1 + b 1 a_1+b_1 a1+b1是无符号整数
    所以取模后 a 1 + b 1 = a + b + 2 w a_1+b_1 = a+b+2^w a1+b1=a+b+2w 并且 二进制的最高位为0
    所以按照有符号位的解释方式计算最终结果也是 a + b + 2 w a+b+2^w a+b+2w

  2. 如果 − 2 w − 1 ≤ a + b < 0 -2^{w-1}\le a+b<0 2w1a+b<0,此时 m > 0 m>0 m>0并且 2 w − 1 ≤ a + b + 2 w < 2 w 2^{w-1}\le a+b+2^w <2^{w} 2w1a+b+2w<2w
    ⇒ a 1 + b 1 = ( a + b + 2 w ) + ( m − 1 ) 2 w \Rightarrow a_1+b_1=(a+b+2^w)+(m-1)2^w a1+b1=(a+b+2w)+(m1)2w
    因为 a 1 + b 1 a_1+b_1 a1+b1是无符号整数
    所以取模后 a 1 + b 1 = a + b + 2 w a_1+b_1 = a+b+2^w a1+b1=a+b+2w 并且 二进制的最高位为1
    按照有符号位的解释方式计算最终结果也是 a + b + 2 w − 2 w = a + b a+b+2^w-2^w = a+b a+b+2w2w=a+b

  3. 如果 0 ≤ a + b < 2 w − 1 , a 1 + b 1 = a + b 0\le a+b<2^{w-1}, a_1+b_1=a+b 0a+b<2w1,a1+b1=a+b并且 二进制的最高位为0
    所以按照有符号位的解释方式计算最终结果也是 a + b a+b a+b

  4. 如果 2 w − 1 ≤ a + b < 2 w , a 1 + b 1 = a + b 2^{w-1}\le a+b<2^{w}, a_1+b_1=a+b 2w1a+b<2w,a1+b1=a+b并且 二进制的最高位为1
    按照有符号位的解释方式计算最终结果也是 a + b − 2 w = a + b − 2 w a+b-2^w = a+b-2^w a+b2w=a+b2w

根据上面四种情况,我们总结有符号整数的加法公式如下:
假设 a a a b b b都是占用 w w w位的有符号整数,则 a + b a+b a+b的运算结果如下:
c = { a + b − 2 w , a + b ≥ 2 w − 1 正溢出 a + b , − 2 w − 1 ≤ a + b < 2 w − 1 正常 a + b + 2 w , a + b < − 2 w − 1 负溢出 c= \begin{cases} a + b-2^w,&a+b\ge2^{w-1}&正溢出\\ a+b,&-2^{w-1}\le a+b<2^{w-1}&正常\\ a+b+2^w,&a+b <-2^{w-1}&负溢出 \end{cases} c= a+b2w,a+b,a+b+2w,a+b2w12w1a+b<2w1a+b<2w1正溢出正常负溢出

接下来,分析一下我们刚开始的例子:
a = 20000 = 100111000100000 a = 20000=100 1110 0010 0000 a=20000=100111000100000
b = 20000 = 100111000100000 b = 20000=100 1110 0010 0000 b=20000=100111000100000
a + b = 40000 > 2 15 a+b=40000>2^{15} a+b=40000>215, 正溢出
⇒ c = a + b − 2 16 = 40000 − 65536 = − 25536 \Rightarrow c = a+b-2^{16} = 40000-65 536=-25536 c=a+b216=4000065536=25536

a 1 = − 20000 = 1111011000100000 a_1 = -20000=1111 0110 0010 0000 a1=20000=1111011000100000
b 1 = − 20000 = 1111011000100000 b_1 = -20000=1111 0110 0010 0000 b1=20000=1111011000100000
a 1 + b 1 = − 40000 < − 2 15 a_1+b_1=-40000<-2^{15} a1+b1=40000<215, 负溢出
⇒ c 1 = a 1 + b 1 + 2 16 = − 40000 + 65536 = 25536 \Rightarrow c_1 = a_1+b_1+2^{16} = -40000+65 536=25536 c1=a1+b1+216=40000+65536=25536

有符号整数加法溢出判断

假设 a a a b b b都是占用 w w w位的有符号整数,并且 c = a + b c = a+b c=a+b,
当且仅当 a > 0 , b > 0 , c ≤ 0 a>0,b>0,c\le0 a>0,b>0,c0时,发生了正溢出
当且仅当 a < 0 , b < 0 , c ≥ 0 a<0,b<0,c\ge0 a<0,b<0,c0时,发生了负溢出

无符号整数的乘法

有符号整数的乘法和加法一样,存在两种情况,一种是正常未溢出,一种是正溢出。而计算机的处理方式也是一致的,都是截断处理,所以我们直接给出有符号整数相乘的计算公式:
假设 a a a b b b都是占用 w w w位的无符号整数,则 a × b a\times b a×b的运算结果如下:
c = ( a × b ) m o d    2 w c=(a\times b)\mod 2^w c=(a×b)mod2w
看下面的例子:

unsigned short a = 300;
unsigned short b = 300;
printf("a = %hu, b = %hu, c = %hu\n",a,b,a*b);

运行结果如下:

a = 300, b = 300, c = 24464

因为 c = a × b = 300 × 300 = 90000 > 2 16 c = a\times b=300\times 300=90000>2^{16} c=a×b=300×300=90000>216
所以 c = 90000 − 2 16 = 90000 − 65536 = 24464 c = 90000-2^{16} = 90000-65536 = 24464 c=90000216=9000065536=24464

有符号整数的乘法

知道了无符号整数的乘法,根据前置2的条件,我们可以很容易计算有符号整数的乘法

  1. 按照无符号解释有符号整数
  2. 无符号整数进行相乘,得到无符号整数,取模得到截断后的无符号整数
  3. 按照有符号数解释无符号结果,得到最终结果

还是看一个例子:

signed short a = -400;
signed short b = 300;
printf("a = %hd, b = %hd, c = %hd\n",a,b,a*b);
    
signed short a1 = -400;
signed short b1 = -300;
printf("a1 = %hd, b1 = %hd, c1 = %hd\n",a1,b1,a1*b1);

运行结果如下:

a = -400, b = 300, c = 11072
a1 = -400, b1 = -300, c1 = -11072

我们来分析一下:
a = − 400 a=-400 a=400,无符号解释为 − 400 + 65536 = 65136 -400+65536=65136 400+65536=65136
b = 300 b=300 b=300,无符号解释为 300 300 300
c 1 = a × b = 65136 × 300 m o d    2 16 = 19540800 m o d    2 16 = 11072 c^1 = a\times b = 65136\times 300 \mod2^{16}= 19540800\mod2^{16} = 11072 c1=a×b=65136×300mod216=19540800mod216=11072
c = c 1 c = c^1 c=c1的有符号解释 11072 11072 11072

a 1 = − 400 a_1=-400 a1=400,无符号解释为 − 400 + 65536 = 65136 -400+65536=65136 400+65536=65136
b 1 = − 300 b_1=-300 b1=300,无符号解释为 − 300 + 65536 = 65236 -300+65536 = 65236 300+65536=65236
c 1 1 = a 1 × b 1 = 65136 × 65236 m o d    2 16 = 4249212096 m o d    2 16 = 54464 c_1^1 = a_1\times b_1 = 65136\times 65236 \mod2^{16}= 4249212096\mod2^{16} = 54464 c11=a1×b1=65136×65236mod216=4249212096mod216=54464
c 1 = c 1 1 c_1 = c_1^1 c1=c11的有符号解释 54464 − 65536 = − 11072 54464 - 65536=-11072 5446465536=11072

乘以常数的优化

通过前置3,我们可以知道,如果乘法可以通过加法、减法、位级运算和移位来实现,程序会运行的更好。
通常情况下,如果程序中存在乘以常数的代码,编译器在编译过程中通常会把代码转换为一系列加法、减法、位级运算和移位的指令,我们下面就来感受一下该转换的魅力。

首先,如果我们将一个无符号整数乘以一个2的正次幂的常数,我们很容易通过移位获取到结果,比如:
a × 2 k a\times 2^k a×2k,其中 a , k a,k a,k是无符号整数,我们通过 a a a<< k k k即得到结果。
那么对于a是一个有符号整数呢,该方法还适用吗?
答案:适用。
因为每次左移其实是相当于乘以2,我们可以把有符号整数解释成无符号数,然后左移,然后在把结果解释成有符号数即可,也就是说移位的操作对无符号和有符号整数都适用,只不过数据的解释方式不一样,看下面的例子:

signed short a = -1;
printf("a = %hd, c = %hd\n",a,a<<1);

运行结果如下:

a = -1, c = -2

a a a在内存的表示为 0 b 1111111111111111 0b11111111 11111111 0b1111111111111111
a a a<<1后内存表示为 0 b 1111111111111110 0b11111111 11111110 0b1111111111111110
移动后的补码表示的数值为 c = − 2 c = -2 c=2

到这里,我们终于发现了补码的魅力了,补码这个东西不影响加法、减法、乘法、位级运算和移位的结果,只影响解释方式,不影响机器级的表示

接下来,考虑任意无符号整数常数 K K K,即将 a × K a\times K a×K转换为加法、减法和位级运算

  • 第一种方式:我们可以遍历 K K K的每一位,如果为1,我们令 a a a<< n n n n n n为当前位的位置,最后把每个移位后的值相加,这种方式最多会有 w ∗ 2 − 1 w*2-1 w21个周期的操作( w w w K K K占据的位数)
  • 第二种方式:我们找出 K K K中连续为1的位数,比如开始位置 n n n,结束位置 m m m,我们可以通过 ( a (a (a<< ( n + 1 ) − a (n+1)-a (n+1)a<< m ) m) m)三个周期就能得到结果

当然,这两种方式不一定比乘法来的效率高,所以具体还得看实际情况而定。

这里给出一个算法题:

// 提示:乘法运算可以转化为移位和加减运算的组合
// 假设进行一个乘法运算需要10个运行周期
// 假设加法、减法、移位、位运算需要1个周期
// 设计算法计算当一个整数乘以一个常数的时候最少需要几个周期
// val:常数值
// return:最小需要的周期数量
int multiOptimization(int val)
{
	//todo
}
int multiOptimization(int val)
{
    int len = sizeof(val) * 8;
    int m=-1;
    int n=-1;
    int c1=0;
    int c2=0;
    for(int i = len-1;i>=0;i--)
    {
        if(((1<<i)&val)>0)
        {
            if(m>=0)
            {
                n=i;
            }
            else
            {
                m=n=i;
            }
            c2++;
        }
        else if(m>=0)
        {
            c1+=3;
            m = n =-1;
        }
    }
    if(m>=0)
    {
        c1+=3;
    }
    return c1<c2?(c1<10?c1:10):(c2<10?c2:10);
}

int main(int argc, const char * argv[])
{
	// 251的二进制表示11111011
	// 341的二进制表示101010101
	// 87551的二进制表示10101010111111111
    int a1 =multiOptimization(251);
    int a2 =multiOptimization(341);
    int a3 =multiOptimization(87551);
    printf("a1 = %d\n", a1);
    printf("a2 = %d\n", a2);
    printf("a3 = %d\n", a3);
    return 0;
}

运行结果为:

a1 = 6
a2 = 5
a3 = 10

无符号整数的除法

在大多数机器上,整数除法要比整数乘法更慢,需要30 个或者更多的时钟周期,除以2的幂也可以用移位运算来实现,只不过我们用的是右移,而不是左移。无符号和有符号整数分别使用逻辑移位算术移位来达到目的。

  • 逻辑左移:不考虑符号位,全部位都左移,低位用0补齐
  • 逻辑右移:不考虑符号位,全部位都右移,高位用0补齐
  • 算术左移:符号位不变,其余位都左移,低位用0补齐
  • 算术右移:符号位不变,其余位都右移,高位用符号位补齐

接下来,先介绍一下向上取整和向下取整,因为整数的除法结果一定是整数。
向上取整:取整结果是大于等于当前整数的最小整数,比如
⌈ 3.14 ⌉ = 4 \lceil 3.14\rceil=4 3.14=4
⌈ − 3.14 ⌉ = 3 \lceil -3.14\rceil=3 3.14=3
⌈ 3 ⌉ = 3 \lceil 3\rceil=3 3=3
向下取整:取整结果是小于等于当前整数的最大整数,比如
⌊ 3.14 ⌋ = 3 \lfloor 3.14\rfloor=3 3.14=3
⌊ − 3.14 ⌋ = 4 \lfloor -3.14\rfloor=4 3.14=4
⌊ 3 ⌋ = 3 \lfloor 3\rfloor=3 3=3

计算机关于整数除法的规则为:如果结果大于0,向下取整,如果结果小于0,向上取整

对于无符号整数除以2的无符号整数次幂是非常容易理解的,因为不涉及算术移位
对于 a ÷ 2 k a\div 2^k a÷2k,其中 a , k a,k a,k是无符号整数,我们通过 a a a>> k k k即得到结果,当然这个结果一定是向下取整了,这里不举例子了。

有符号整数的除法

对于有符号整数 a a a除以2的无符号整数次幂,我们有两种取整方式:

  1. 当前 a ≥ 0 a\ge 0 a0时,向下取整。
  2. 当前 a < 0 a<0 a<0时,向上取整。

根据不同的取整方式,有不同的处理方法:
一种是比较好想到的,采用算数右移的方式
对于 a ÷ 2 k a\div 2^k a÷2k,其中 a a a是有符号整数, k k k是无符号整数,我们通过 a a a>> k k k(算术移位)即得到结果,该结果是向下取整的。
证明:
对于 a ≥ 0 a\ge 0 a0 a a a表现出来就是一个无符号整数,所以满足向下取整
对于 a < 0 a<0 a<0 a a a算数右移k位之后 a 1 a_1 a1的位级表示成 [ 1 , 1 , 1 ⋯   , x w − 1 , x w − 2 , ⋯   , x k ] [1,1,1\cdots,x_{w-1},x_{w-2},\cdots,x_k] [1,1,1,xw1,xw2,,xk]
⇒ a 1 = ∑ i = k i = w − 1 ( x i ∗ 2 i − k ) − 2 w − k \Rightarrow a_1=\sum_{i=k}^{i=w-1}(x_i*2^{i-k})-2^{w-k} a1=i=ki=w1(xi2ik)2wk
⇒ a 1 2 k = ∑ i = k i = w − 1 ( x i ∗ 2 i ) − 2 w \Rightarrow a_12^{k}=\sum_{i=k}^{i=w-1}(x_i*2^{i})-2^{w} a12k=i=ki=w1(xi2i)2w
⇒ a 1 2 k = a − ∑ i = 0 i = k − 1 ( x i ∗ 2 i ) \Rightarrow a_12^{k}=a-\sum_{i=0}^{i=k-1}(x_i*2^{i}) a12k=ai=0i=k1(xi2i)
假设 b = ∑ i = 0 i = k − 1 ( x i ∗ 2 i ) b = \sum_{i=0}^{i=k-1}(x_i*2^{i}) b=i=0i=k1(xi2i),则 0 ≤ b < 2 k 0\le b<2^k 0b<2k
⇒ a = a 1 2 k + b \Rightarrow a = a_12^k+b a=a12k+b
⇒ a ÷ 2 k = a 1 + b ÷ 2 k \Rightarrow a\div 2^k = a_1+b\div 2^k a÷2k=a1+b÷2k
因为舍弃的部分 b ÷ 2 k b\div 2^k b÷2k大于0,所以是向下取整

另一种方式是对于 a ÷ 2 k a\div 2^k a÷2k,其中 a a a是有符号整数, k k k是无符号整数,我们通过 ( a + ( 1 (a+(1 (a+(1<< k ) − 1 ) k)-1) k)1)>> k k k(算术移位)即得到结果,该结果是向上取整的。
这个听起来很玄幻,其实原理很简单,就是给 a a a添加一个偏移量,然后在采用第一种方式。
很简单的一个同理的问题就是:

int a = 10;
int b = 9;
计算a/b向上取整?
就是比如
a=9,a/b=1
a=10,a/b=2

这种问题一般我们的求解方法就是

int c = (a+b-1)/b;

( a + ( 1 (a+(1 (a+(1<< k ) − 1 ) k)-1) k)1)>> k k k跟这个是一样的。

根据计算机除法原理:如果结果大于0,向下取整,如果结果小于0,向上取整,我们得出2次幂除法公式应该是:
( a < 0 ? ( a + ( 1 < < k ) − 1 ) : a ) > > k (a<0?(a+(1<<k)-1):a)>>k (a<0?(a+(1<<k)1):a)>>k

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值