L03 Bit Byte Integer

6次阅读
没有评论

L03 Bit Byte Integer

对于无符号数运算,这里使用4位进行运算(0-15):

假设我们运算 13+5 = 18 实际上是 1101+0101= 1 0010
但是多出来的一位被丢弃了,因为只有四位容量,所以结果成了a+b mod 2^n n为位数

对于补码,同样以4位作为举例,最大数为0111 是+7 最小数为 1000 是-8。
那么对于 5-3 可视为 5+(-2) 也就是 0101 + 1110 = 1 0011 但是最高位被丢弃,所以得到 0011 也就是3。
对于 3-5 可视为 3+(-5) 也就是 0011+ 1011 = 1110 也就是-2

-3+(-6) 也就是 1101 + 1010 = 1 0111 但是最高位被抛弃了,也就是 7 这产生了负溢出
7+5 也就是 0111+ 0101 = 1100 这是-8 这被称作正溢出

上述两种情况被称作补码溢出,那么,对于两个整数相加,需要w+1的位数用于存储。对于两个w位的整数相乘,可能需要2w位用于存储。但是这样会将我们的内存耗尽,所以我们只能考虑截断。

对于 3 * 5 也就是 0011*0101 ,这里使用一种位移的方法来完成:

我们可以把其中一个乘数拆开:

将 5 (​​0101​​ ) 拆解为:

$$5 = 4 + 1 = 2^2 + 2^0$$

那么 $3 \times 5$ 就可以瞬间转化为:

$$3 \times 5 = 3 \times (4 + 1) = (3 \times 4) + (3 \times 1)$$

用二进制位移直接秒杀

现在我们把上面的拆解直接套进二进制里(假设是 8 位系统,3 是 0000 0011):

  1. 乘以 4​:等于把 3 左移 2 位 (3 << 2​)
    ​0000 0011 $\rightarrow$ ​0000 1100​ (十进制的 12)
  2. 乘以 1​:等于 3 本身保持不变 (3 << 0​)
    ​0000 0011 $\rightarrow$ ​0000 0011​ (十进制的 3)
  3. 把两个结果相加:

看!0000 1111​ 正好就是十进制的 ​15​。你根本不需要去算 1*1=1​、1*0=0​ 这种繁琐的竖式交叉乘法,只需要看一眼乘数里哪几位是 ​1​​ ,把被乘数左移对应的位数,然后加起来就搞定了。

计算机也是这么干的吗?

是的,甚至比这更聪明!

在现代 CPU 内部,乘法器(Multiplier)的核心逻辑就是这个。当计算机看到 0101(5)时:

  • 发现最低位(第0位)是 1 $\rightarrow$ 把 0011 留下来。
  • 发现第1位是 0 $\rightarrow$ 跳过。
  • 发现第2位是 1 $\rightarrow$ 把 0011 左移2位,和前面的结果加起来。

对于更复杂的乘法,计算机还会使用一种叫 布斯算法(Booth’s Algorithm) 的神级优化,它可以通过“有符号转换”,把连续的 1​ 变成一次减法和一次加法。例如,乘以 7(0111​)会被计算机看作 ​乘以 **$(8 – 1)$**​,也就是 (x << 3) - x,连加法都省了好几次!

但是在这里 因为是有符号数,这个数变成了-1

在硬件设计上,CPU 根本不需要为“有符号乘法”和“无符号乘法”设计两套不同的乘法器电路。 它只需要设计一个最简单的无符号乘法器,直接把两个 $w$ 位数乘起来,然后闭着眼睛直接截断,只留下低 $w$ 位。

  • 如果你代码里定义的是 unsigned,这个保留下来的低 $w$ 位就是无符号结果。
  • 如果你代码里定义的是 signed,这个保留下来的低 $w$ 位(通过补码解释)就是有符号结果。

这也就是为什么汇编语言里,在进行这种截断的乘法时,它们可以共享底层几乎相同的算术逻辑单元(ALU)的原因。

对于负数,比如 -6 是 1010 对其右移是算数位移,即1101,这是 -3 再对其除以二,也就是向右移动 这是 1110这是-2。出现了向下取整。

所以内存的最小存储单元实际上是Byte
内存的地址指向的也是每个字节
现在比如 0x0001 里面就只写一个A 那么ASCII是65 ,所以直接读取 0x0001 里面的数据得到的是 0100 0001 刚好一字节。

但是,在实际工作中,CPU 读写内存的习惯并不是一次只读 1 字节。

  • 如果 CPU 每次只读 1 字节,那么要读一个 64 位的整数,CPU 就要去内存里跑 8 趟,这太慢了。

  • 所以,CPU 内部的寄存器和数据总线是有宽度的,这个宽度就是​字长(Word Size) 。

    • 32位系统:字长为 4 字节(32 bits)
    • 64位系统:字长为 8 字节(64 bits)

为了追求速度,CPU 每次去内存拿数据,都是​整块整块地往回搬。这个“块”的大小,就是它的字长。

对于当前主流计算机,64bit的设备实际上的内存读取只能读取到 47位左右,也就是128TB。

在大端序(Big-Endian)和小端序(Little-Endian)的背后,其实是一场关于“多字节数据该怎么塞进字节内存里”的硬件设计之争。

我们刚刚聊过,像 int​(4字节)或 short​(2字节)这样的多字节变量,在内存里要连续占用多个“字节抽屉”。那么,数据的“高位”和“低位”,到底谁该睡在“低地址”的抽屉里?

这就是大小端序要解决的根本问题。

什么是大端序和小端序?

我们用一个非常直观的例子来解释。假设你有一个 32 位的十六进制整数:​0x12345678​。这个数字有 4 个字节:

  • ​12​ 是高位字节(权重最大,像百位十位)
  • ​78​ 是低位字节(权重最小,像个位)

现在我们要把它们存入从 ​0x0001​​ 到 ​0x0004​ 的 4 个连续内存抽屉中:

大端序 (Big-Endian):先存大头(高位在前)

大端序最符合人类的直觉。写字时我们先写大数(左边),存内存时我们也​把高位字节存放在低地址:

内存地址 0x0001 0x0002 0x0003 0x0004
存放的数据 ​12​ 34 56 ​78​

口诀: 高位字节存放在低地址。你从内存地址从小到大读,刚好就是 12 34 56 78,顺着读。

小端序 (Little-Endian):先存小头(低位在前)

小端序则相反,它​把低位字节存放在低地址:

内存地址 0x0001 0x0002 0x0003 0x0004
存放的数据 ​78​ 56 34 ​12​

口诀: 低位字节存放在低地址。你从内存地址从小到大读,顺序是逆序的 78 56 34 12。

  • 大端序的好处(直观、便于判断):

    • 符号判定极快:因为最高位(包含符号位)就在第一个字节(最低地址),CPU 想要判断这个数是正数还是负数,只需要看第一个字节的最高位即可,不需要读完整个数。
    • 符合人类阅读习惯:网络传输、内存直接打印时,大端序的数据排布和人类书写完全一致,调试网络数据包时非常省心。
  • 小端序的好处(便于强制类型转换):

    • 类型转换极快​:还记得我们刚才讨论的“8位扩展到16位”或者反过来“32位强制截断到8位”吗?
      假设你要把一个 32 位的 0x00000008​ 截断成 8 位的 char。

      • 在小端序里,08​ 就在起始地址 0x0001​。CPU 甚至不需要移动指针,直接从 0x0001​ 读取 1 字节,拿出来的就是 08。
      • 但在大端序里,08​ 被存在了最后一个地址 0x0004。CPU 必须先做地址偏移计算,挪动 3 个字节才能拿到它。
    • 数学运算方便:计算机做加法和乘法时,都是从“个位”(低位)开始算的。小端序先读到低位,硬件电路上可以直接边读边算,不用等高位读完。

硬件决定端序

  • x86 和 x64 架构(Intel / AMD 芯片) ​:
    它们是彻头彻尾的​小端序(Little-Endian) ​。因为个人电脑和现代服务器市场几乎被 x86 垄断,所以我们日常接触的 Windows、Linux、macOS,只要跑在 Intel/AMD 芯片上,在内存里都是​小端序。
  • ARM 架构(手机芯片、M系列 Mac 芯片) ​:
    ARM 芯片设计得非常鸡贼,它支持​双端序(Bi-Endian) ​。也就是说它既支持大端也支持小端,但在实际使用中,我们的 iOS、Android、macOS (ARM) 都会把它配置为小端序运行,以此和 x86 世界保持软件上的兼容。
  • 网络传输(网络字节序) ​:
    TCP/IP 协议族规定:​网络数据传输必须一律使用大端序​。所以,当一个小端序的 x86 电脑要发数据给另一台电脑时,操作系统必须先调用汇编指令(如 htons​、htonl)将内存中的“小端数据”翻转成“大端数据”再发出去。
正文完
 0
评论(没有评论)