话说今天在用 uintptr 进行指针运算的时候, 突然想起来有个内存对齐的东西, 那么对这个 uintptr 计算是否会有影响?
带着疑问, 开始吧。
你将获得以下知识点:
在想象中内存应该是一个一个独立的字节组成的。像这样:
事实上, 人家是这样的:
内存是按照成员的声明顺序, 依次分配内存, 第一个成员偏移量是 0, 其余每个成员的偏移量为指定数的整数倍数 (图中是 4)。像这样进行内存的分配叫做内存对齐。
原因有两点:
并不是所有的硬件平台都能访问任意地址上的任意数据, 会直接报错的!
(解释: 比如说有的 cpu 读取 4 个字节数据, 要是没有内存对齐, 从 1 开始那么内存就需要把 0-7 字节的全部取出来, 再剔除掉 1/5/6/7
, 增加了额外的操作, cpu 不一定能这么搞, 自然就报错了)
访问未对齐的内存, 需要访问两次; 如果对齐的话就只需要一次了。
(解释: 比如取 int64, 按照 8 个位对齐好了, 那获取的话直接就是获取 8 个字节就好了, 边界好判断)
二个原则:
结构体是平时写代码经常用到的。相同的成员, 不同的排列顺序, 会有什么区别吗?
举个例子:
func main() {
fmt.Println(unsafe.Sizeof(struct {
i8 int8
i16 int16
i32 int32
}{}))
fmt.Println(unsafe.Sizeof(struct {
i8 int8
i32 int32
i16 int16
}{}))
}
输出:
1 8
2 12
what? 竟然不一样。
分析一波: 需要内存对齐的话, 因为最大是 int32, 所以最终记过必须是 4 个字节的倍数才能对齐。
当 8-16-32
的时候, 类似这样 |x-xx|xxxx|
。
当 8-32-16
的时候, 类似这样 |x—|xxxx|xx–|
。
一眼就看出了大小了。
这里的为什么是 x-xx
而不是 xxx-
需要说明下。因为当 int8 放入内存的时候, 其占坑 1 个字节, 对齐值为 1, 而 int16 占坑 2 个字节, 对齐值为 2, 所以说会先偏移 2 个字节从第三个字节才开始放 int16 的数
现在对结构体 Test 通过指针计算的方式进行赋值。
Test 内存情况: |x-xx|xxxx|
。需要注意的是这里的 -
需要多计算一个字节才行。
type Test struct {
i8 int8
i16 int16
i32 int32
}
func main() {
var t = new(Test)
// 从 0 开始
var i8 = (*int8)(unsafe.Pointer(t))
*i8 = int8(10)
// 偏移 int8+1 的字节数, 注意这里有个 1! ! !
var i16 = (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(t))+ uintptr(1) + uintptr(unsafe.Sizeof(int8(0)))))
*i16 = int16(10)
// 偏移 int8+1+int16 + 的字节数, 注意这里有个 1! ! !
var i32 = (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(t)) + uintptr(1) + uintptr(unsafe.Sizeof(int8(0))+uintptr(unsafe.Sizeof(int16(0))))))
*i32 = int32(10)
fmt.Println(*t)
}
输出:
1 | {10 10 10}
附上两个神器:
功能 | 函数 |
---|---|
获取对齐值 | unsafe.Alignof(t.i16) |
获取偏移值 | unsafe.Offsetof(t.i16) |
根据计算对齐值进行成员顺序的拼凑, 可以一定程度上缩小结构体占用的内存。
通过分析偏移量和对齐值, 准确计算每个成员所偏移的位数, 避免算错。