首页 > 技术文章 > 计算机最小单位,和结构体占用字节大小分析(内存对齐)

xiangshihua 2022-01-03 11:47 原文

文档查询 :https://golang.google.cn/pkg/strconv/  

GO的编码为UTF-8编码

计算机的基本的存储单元有:

   位(bit):二进制数中的一个数位,可以是0或者1,是计算机中数据的最小单位。二进制的一个“0”或一个“1”叫一位。

   字节(Byte,B):计算机中数据的基本单位,每8位组成一个字节。各种信息在计算机中存储、处理至少需要一个字节

 

ASCIIS码: 1个英文字母(不分大小写)= 1个字节的空间

       1个中文汉字 = 2个字节的空间

                    1个ASCII码 = 一个字节

UTF-8编码:1个英文字符 = 1个字节

                   英文标点  = 1个字节

                   1个中文(含繁体) = 3个字节

        中文标点 = 3个字节

Unicode编码:1个英文字符 = 2个字节

                    英文标点  = 2个字节

                    1个中文(含繁体) = 2个字节

                    中文标点 = 2个字节              

字(Word):两个字节称为一个字。汉字的存储单位都是一个字。

扩展的存储单位有:

   计算机存储容量大小以字节数来度量,1024进位制:

      1024B=1K(千)B  

 

           1024KB=1M(兆)B 

 

           1024MB=1G(吉)B 

 

           1024GB=1T(太)B 

   这是常用的五个,至于PB,EB,ZB,YB,BB,NB,DB几乎在日常使用中是不会遇到的。

Int8,Int16,Int32,Int64,后面的数字就代表这个数据类型占据的空间。

       Int8, 等于Byte, 占1个字节.

    Int16, 等于short, 占2个字节. -32768 32767

    Int32, 等于int, 占4个字节. -2147483648 2147483647

    Int64, 等于long, 占8个字节. -9223372036854775808 9223372036854775807

 

go 中  string 占16个字节

  另外, 还有一个Byte, 它等于byte, 0 - 255.

 

package main

import (
"fmt"
"unsafe"
)

type One struct {
id1 int8 //size为1
id2 int32 //size为4
id3 int8 //size为1
}
//分析 [1 0 0 0][1 1 1 1][1]
//必须为4的倍数 [1 0 0 0][1 1 1 1][1][0 0 0]

type Two struct {
id1 int32 //size为4
id2 int8 //size为1
id3 int8 //size为1
}
//分析[1 1 1 1][1][1]
//必须为4的倍数  [1 1 1 1][1][1][0 0]

func main() {
fmt.Println(unsafe.Sizeof(One{})) //size为12
fmt.Println(unsafe.Sizeof(Two{})) //size为8
fmt.Println(unsafe.Sizeof("222222222")) //size为16
fmt.Println(unsafe.Sizeof(111111111)) //size为8
fmt.Println(unsafe.Sizeof(1.1)) //size为8
}

 这样我们在写代码定义结构体变量时,注意设定他的大小和顺序

对齐内容可看一篇文章:https://zhuanlan.zhihu.com/p/53413177

  内次对齐: 数据类型的对齐边界和平台对齐边界,取较小的那个 (不同平台会有所不同)

 

 结构体字节大小:先要确定结构体每个成员的对齐边界再取最大值 得到结构体的对齐边界

 

1.存储结构体的起始地址是对齐边界的倍数,这里的对齐边界是这个类型本身的对齐边界,不是结构体的对齐边界(取边界的最大值)

2.结构体整体占用字节数需要是对齐边界的整数倍,不够的需要扩展

实例:

package main
import (
    "fmt"
    "unsafe"
)
type Part1 struct {
    a bool        //1
    b int32       //4
    c int8        //1
    d int64       //8
    e byte        //1
}
//分析 [1 0 0 0][1 1 1 1][1 0 0 0 0 0 0 0][1 1 1 1 1 1 1 1][1]
//为8的倍数 [1 0 0 0][1 1 1 1][1 0 0 0 0 0 0 0][1 1 1 1 1 1 1 1][1 0 0 0 0 0 0 0] //32 func main() { part1 :
= Part1{} fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1)) }

 

结果:

part1 size: 32, align: 8

 

分析:

成员对齐

  • 第一个成员 a
    • 类型为 bool
    • 大小/对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 b
    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 根据规则 1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此 2-4 位为 Padding。而当前数值从第 5 位开始填充,到第 8 位。如下:axxx|bbbb
  • 第三个成员 c
    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐,填充 1 个字节到第 9 位。如下:axxx|bbbb|c…
  • 第四个成员 d
    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 根据规则 1,其偏移量必须为 8 的整数倍。确定偏移量为 16,因此
      9-16 位为 Padding。而当前数值从第 17 位开始写入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五个成员 e
    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 根据规则 1,其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐,填充 1 个字节到第 25 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e…

整体对齐

在每个成员变量进行对齐后,根据规则 2,整个结构体本身也要进行字节对齐,因为可发现它可能并不是 2^n,不是偶数倍。显然不符合对齐的规则

根据规则 2,可得出对齐值为 8。现在的偏移量为 25,不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐

结果

Part1 内存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

小结

通过本节的分析,可得知先前的 “推算” 为什么错误?

是因为实际内存管理并非 “一个萝卜一个坑” 的思想。而是一块一块。通过空间换时间(效率)的思想来完成这块读取、写入。另外也需要兼顾不同平台的内存操作情况

巧妙的结构体

在上一小节,可得知根据成员变量的类型不同,其结构体的内存会产生对齐等动作。那假设字段顺序不同,会不会有什么变化呢?我们一起来试试吧 :-)

package main

import (
   "fmt"
   "unsafe"
)

type Part1 struct {
   a bool   //1
   b int32  //4
   c int8   //1
   d int64  //8
   e byte   //1
}
//分析 [1 0 0 0][1 1 1 1][1 0 0 0 0 0 0 0][1 1 1 1 1 1 1 1][1]
//为8的倍数 [1 0 0 0][1 1 1 1][1 0 0 0 0 0 0 0][1 1 1 1 1 1 1 1][1 0 0 0 0 0 0 0] //32
type Part2
struct { e byte //1 c int8 //1 a bool //1 b int32 //4 d int64 //8 } //分析 [1][1][1 0][1 1 1 1][1 1 1 1 1 1 1 1] //16 也刚好为8的倍数

func main() { part1 :
= Part1{} part2 := Part2{} fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1)) fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2)) }

输出结果:

part1 size: 32, align: 8
part2 size: 16, align: 8

通过结果可以惊喜的发现,只是 “简单” 对成员变量的字段顺序进行改变,就改变了结构体占用大小

接下来我们一起剖析一下 Part2,看看它的内部到底和上一位之间有什么区别,才导致了这样的结果?

分析流程

 

 

成员对齐

  • 第一个成员 e
    • 类型为 byte
    • 大小/对齐值为 1 字节
    • 初始地址,偏移量为 0。占用了第 1 位
  • 第二个成员 c
    • 类型为 int8
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 2。不需要额外对齐
  • 第三个成员 a
    • 类型为 bool
    • 大小/对齐值为 1 字节
    • 根据规则1,其偏移量必须为 1 的整数倍。当前偏移量为 3。不需要额外对齐
  • 第四个成员 b
    • 类型为 int32
    • 大小/对齐值为 4 字节
    • 根据规则1,其偏移量必须为 4 的整数倍。确定偏移量为 4,因此第 3 位为 Padding。而当前数值从第 4 位开始填充,到第 8 位。如下:ecax|bbbb
  • 第五个成员 d
    • 类型为 int64
    • 大小/对齐值为 8 字节
    • 根据规则1,其偏移量必须为 8 的整数倍。当前偏移量为 8。不需要额外对齐,从 9-16 位填充 8 个字节。如下:ecax|bbbb|dddd|dddd

整体对齐

符合规则 2,不需要额外对齐

结果

Part2 内存布局:ecax|bbbb|dddd|dddd

总结

通过对比 Part1 和 Part2 的内存布局,你会发现两者有很大的不同。如下:

  • Part1:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx
  • Part2:ecax|bbbb|dddd|dddd

仔细一看,Part1 存在许多 Padding。显然它占据了不少空间,那么 Padding 是怎么出现的呢?

通过本文的介绍,可得知是由于不同类型导致需要进行字节对齐,以此保证内存的访问边界

那么也不难理解,为什么调整结构体内成员变量的字段顺序就能达到缩小结构体占用大小的疑问了,是因为巧妙地减少了 Padding 的存在。让它们更 “紧凑” 了。这一点对于加深 Go 的内存布局印象和大对象的优化非常有帮

当然了,没什么特殊问题,你可以不关注这一块。但你要知道这块知识点

 

推荐阅读