蛮荆

字节序概述

2023-02-20

字节序

字节序: 多字节数据在内存中的排列顺序。

大端序和小端序

大端序: 高位字节在左边,低位字节在右边

// a = 0x12345678
// 大端序表示,符合人类阅读习惯
// -----------------------------
// | 0x12 | 0x34 | 0x56 | 0x78 |
// -----------------------------
// 低地址 <---------------> 高地址

小端序: 低位字节在左边,高位字节在右边

在计算机内部,小端序 被广泛应用于现代性 CPU 内部存储数据。

// a = 0x12345678
// 小端序表示
// -----------------------------
// | 0x78 | 0x56 | 0x34 | 0x12 |
// -----------------------------
// 低地址 <---------------> 高地址

大端序优势

检查正负位

// a = 123456
// 
//           -------------------------
// 小端序表示  | 5 | 4 | 3 | 2 | 1 | - |
//           -------------------------
//           -------------------------
// 大端序表示  | - | 1 | 2 | 3 | 4 | 5 |
//           -------------------------
//           低地址 <------------> 高地址

大端序 只需要读取第 1 位,就可以确定正负,大端序 需要读取到最后 1 位,才可以确定正负。

小端序优势

检查奇偶性

// a = 123456
// 
//           -------------------------
// 小端序表示  | 6 | 5 | 4 | 3 | 2 | 1 |
//           -------------------------
//           -------------------------
// 大端序表示  | 1 | 2 | 3 | 4 | 5 | 6 |
//           -------------------------
//           低地址 <------------> 高地址

小端序 只需要读取第 1 位,就可以确定奇偶,大端序 需要读取到最后 1 位,才可以确定奇偶。

比较大小

比较 3 个数字的大小: 1, 234, 56789

// 小端序表示
// ---------------------
// | 9 | 8 | 7 | 6 | 5 |
// | 4 | 3 | 2 |   |   |
// | 1 |   |   |   |   |
// ---------------------
// 低地址 <--------> 高地址

// 大端序表示
// ---------------------
// | 5 | 6 | 7 | 8 | 9 |
// |   |   | 2 | 3 | 4 |
// |   |   |   |   | 1 |
// ---------------------
// 低地址 <--------> 高地址

小端序 只需要从左到右读取,哪个数字读取不到下一位,就可以确定是最小的数字,大端序 需要读取到最后 1 位,才可以确定最小数字。 小端序 在极端情况下 (所有数字位数相同),和大端序一样,需要读取到最后 1 位。

乘法

计算 24165 * 3841

大端乘法

上图是 大端序 的 24165 乘以 3841,大端序 的乘法是向左进位,也就是向左边扩展,必须等到每一轮的结果都出来(上例是四轮),再相加统一写入内存。

小端乘法

上图是 小端序 的 24165 乘以 3841,小端序 的乘法是向右进位,也就是向右边扩展,左边的边界不变。每一轮结果写入内存后,就不需要移动,后面有变化只需要改动对应的位就行了。

任意精度整数

任意精度整数又称 大整数,可以存放任意大小的整数。它的内部实现是把整数分成一个个较小的单位,通常是 uint32(无符号32位整数)或 uint64(无符号64位整数),按顺序组合在一起。

// 大整数内部表示
// -------------------------------------
// | uint64 | uint64 | uint64 | uint64 |
// -------------------------------------

大端序 表示的情况下,第一个 uint64 就是这个整数最大的部分,运算时,一旦这个数发生变化,需要进位,后面的所有位都必须移动和改写。

小端序 表示的情况下,发生进位时,不需要所有位移动,所以比 大端序 更快。

小端序 的另一个好处是,如果逐字节的运算从个位数开始(比如乘法和加法),可以从左到右依次运算一个个 uint64,算完上一个再读取下一个。 大端序 就不行,必须读取整个数以后再进行运算。

更改数据类型

C 语言有一种 cast 操作,可以强制改变变量的数据类型,比如把 32 位整数强行改变为 16 位整数。

更改数据类型

上图中,32 位整数 0x00000001 更改为 16 位整数 0x0001大端序 是截去前面两个字节,这时指向这个地址的指针必须向后移动两个字节。

小端序 就没有这个问题,截去的是后面两个字节,第一位的地址是不变的,所以指针不需要移动。

总结

如果需要逐位运算,或者需要到从个位数开始运算,都是 小端序 占优势。反之,如果运算只涉及到高位,或者数据的可读性比较重要,则是 大端序 占优势。

转载声明

本文转载自 字节序探析:大端与小端的比较,笔者在文字描述和图片排版上略作修改。

补充

Go 语言标准库中的 encoding/binary 包实现了数字和字节序列之间的简单转换以及编码解码。

package binary

// Read 将结构化二进制数据读取到 data
func Read(r io.Reader, order ByteOrder, data any) error {}

// Write 将结构化二进制数据写入到 w
func Write(w io.Writer, order ByteOrder, data any) error {}

...

Read 示例代码

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

func main() {
	var pi float64
	b := []byte{0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40}
	buf := bytes.NewReader(b)
	err := binary.Read(buf, binary.LittleEndian, &pi)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	fmt.Println(pi)
}

// $ go run main.go
// 3.141592653589793

Write 示例代码

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"math"
)

func main() {
	buf := new(bytes.Buffer)
	var pi = math.Pi
	err := binary.Write(buf, binary.LittleEndian, pi)
	if err != nil {
		fmt.Println("binary.Write failed:", err)
	}
	fmt.Printf("% x\n", buf.Bytes())
}

// $ go run main.go
// 18 2d 44 54 fb 21 09 40

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。