Golang系列 - 内存对齐


摘要: 本文将围绕内存对齐展开, 包括字符串、数组、切片等类型header的size大小、内存对齐、空结构体类型的对齐等等内容.
关键词: Golang, 内存对齐, 字符串, 数组, 切片

常见类型header的size大小

首先看下面程序的输出, 对于字符串、数组、切片这三种类型, 通过unsafe.SizeOf 获取其在内存中占用的字节数.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
    str1 := ""
    str2 := "Hello, World!"
    arr1 := [0]int{}
    arr2 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    slice1 := []int{}
    slice2 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Printf("str1: %d\n", unsafe.Sizeof(str1))
    fmt.Printf("str2: %d\n", unsafe.Sizeof(str2))
    fmt.Printf("arr1: %d\n", unsafe.Sizeof(arr1))
    fmt.Printf("arr2: %d\n", unsafe.Sizeof(arr2))
    fmt.Printf("slice1: %d\n", unsafe.Sizeof(slice1))
    fmt.Printf("slice2: %d\n", unsafe.Sizeof(slice2))
}

在64位机器下输出如下:
在这里插入图片描述
首先来说数组, 由于数组的大小是确定的, 所以其占用的内存字节书就是等于其长度 乘以 数据类型的大小, 在64位机器上, int占用8个字节, 所以空数组和长度为10的数组分别占用0和80个字节. 而字符串和切片的长度是不确定的, 在golang语言中, 其分别用如下的结构体来表示:

type stringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

uintptr 在64位机器下也是占用8个字节, 所以字符串和切片在内存中占用的字节数是固定的, 分别为16和24.

内存对齐

实际上golang语言中也存在和C语言类似的内存对齐, 其目的有如下几点:

  • 性能优化: 结构体中的每一个成员的首地址都是自己类型大小的整数倍, 这样保证了只需要通过一次内存操作就能够取出这个数
  • 原子操作: 同时, 如果能够在一次内存操作中完成一个数据的读出, 也能保证操作的原子性
  • 平台原因: 部分平台限制任意地址的读取
    内存对齐大致上有两个要求, 一个是每一个数据成员的起始地址必须是该数据成员字节长度的整数倍, 另外一个是整个结构体的大小是这个结构体对齐长度的整数陪, 结构体对齐长度为这个结构体中最大的数据类型的字节长度于机器位长的最小值.

空结构体类型

如果在一个结构体中有一个数据成员的类型是空结构体呢, 那么这个会给内存对齐带来什么影响? 先看如下程序运行的结果:

package main

import (
	"fmt"
	"unsafe"
)

type DemoStruct1 struct {
    A int32
    D struct{}
}

type DemoStruct2 struct {
    D struct{}
    A int32
}

func main() {
    a1 := DemoStruct1{}
    a2 := DemoStruct2{}
    fmt.Printf("a1: size=%d, structAddr: %d, AAddr: %d, emptyStructAddr: %d\n", unsafe.Sizeof(a1), unsafe.Pointer(&a1), unsafe.Pointer(&a1.A), unsafe.Pointer(&a1.D))
    fmt.Printf("a2: size=%d, structAddr: %d, AAddr: %d, emptyStructAddr: %d\n", unsafe.Sizeof(a2), unsafe.Pointer(&a2), unsafe.Pointer(&a2.A), unsafe.Pointer(&a2.D))
}

在64位机器上输出如下:
在这里插入图片描述
可以看到, 如果空结构体数据成员是最后一个数据成员, 那么其会占用一个字节, 如果不是最后一个数据成员, 那么不会占用字节. 其原因是出于内存泄漏的考虑, 如果空结构体数据成员是结构体最后一个数据成员的时候, 如果其不占用字节, 那么其指针就会指向结构体外, 如果这个指针不被释放, 那么那一块内存就不会被释放, 而通过占用字节, 那么空结构体指针也会指向结构体内部, 避免内存泄漏的问题.

参考

<think>首先,用户的问题是基于给定的镜像信息生成docker save命令。镜像信息包括: - alpine 3.12 - golang 1.16-alpine3.14 - golang 1.15-alpine - golang 1.16-alpine3.12 我需要生成类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12的命令。 用户的例子是:docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 所以,我应该为每个镜像生成一个类似的命令。 命令格式是: docker save -o [输出文件路径] [镜像名]:[标签] 用户指定了一个特定的输出路径:/mnt/hgfs/share/golang1-16-alpine3-12.tar 我需要为每个镜像创建类似的输出文件名。文件名应该基于镜像名和标签。 列出镜像信息: 1. alpine 3.12 → 镜像名:alpine,标签:3.12 2. golang 1.16-alpine3.14 → 镜像名:golang,标签:1.16-alpine3.14 3. golang 1.15-alpine → 镜像名:golang,标签:1.15-alpine 4. golang 1.16-alpine3.12 → 镜像名:golang,标签:1.16-alpine3.12 现在,输出文件路径:用户使用了/mnt/hgfs/share/目录,文件名是golang1-16-alpine3-12.tar 我应该为每个镜像生成一个文件名: - 对于alpine:3.12,文件名可能是alpine-3.12.tar - 对于golang:1.16-alpine3.14,文件名可能是golang1-16-alpine3-14.tar - 对于golang:1.15-alpine,文件名可能是golang1-15-alpine.tar(假设没有具体版本,就用这个) - 对于golang:1.16-alpine3.12,文件名可能是golang1-16-alpine3-12.tar(用户例子中已有) 在用户例子中,输出路径是/mnt/hgfs/share/,所以我应该使用相同的目录路径。用户说“类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar”,所以我可以假设输出目录是/mnt/hgfs/share/,除非用户指定。 用户指令是“生成类似docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12的docker save命令”,所以我应该复制相同的输出路径格式。 现在,生成命令: 1. 对于alpine:3.12: - 镜像名:alpine:3.12 - 输出文件:/mnt/hgfs/share/alpine-3.12.tar 命令:docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 2. 对于golang:1.16-alpine3.14: - 镜像名:golang:1.16-alpine3.14 - 输出文件:/mnt/hgfs/share/golang1-16-alpine3-14.tar 命令:docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 3. 对于golang:1.15-alpine: - 这里有歧义。标签是“1.15-alpine”,但完整的镜像名应该是golang:1.15-alpine。 - 输出文件:/mnt/hgfs/share/golang1-15-alpine.tar 命令:docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine 4. 对于golang:1.16-alpine3.12: - 用户已经提供了一个例子:docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 所以,我可以直接使用那个。 用户说“根据给定的镜像信息”,并列出它们,所以包括所有四个。 用户提到“golang 1.16-alpine3.12”已经在例子中,但既然用户要求“生成”,我应该为所有列出。 用户原文:“根据镜像信息alpine 3.12、golang 1.16-alpine3.14、golang 1.15-alpine、golang 1.16-alpine3.12” 所以四个都需要。 确保文件名一致:在用户例子中,文件名是golang1-16-alpine3-12.tar,使用了破折号和点号,但文件名中通常用破折号代替冒号或点号以避免问题。 在例子中:golang1-16-alpine3-12.tar,所以格式是镜像名去掉冒号,标签用破折号连接。 对于alpine:3.12 → alpine-3.12.tar golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar(注意标签本身有破折号) 在镜像标签中,golang:1.16-alpine3.14,标签是1.16-alpine3.14,所以在导出时,文件名可以基于镜像名和完整标签。 在用户例子中,对于golang:1.16-alpine3.12,文件名是golang1-16-alpine3-12.tar,意思是组合镜像名和标签成为一个文件名部分。 所以类似: - alpine:3.12 → alpine-3.12.tar - golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar(注意,镜像名是golang,标签是1.16-alpine3.14) 在文件名中,应该是镜像名后跟标签,用破折号连接。 但在标签中,已经有破折号,如1.16-alpine3.14。 用户例子中是golang1-16-alpine3-12.tar,意思是省略了冒号,直接连接。 所以,为了一致: - alpine:3.12 → alpine-3.12.tar - golang:1.16-alpine3.14 → golang1-16-alpine3-14.tar - golang:1.15-alpine → golang1-15-alpine.tar - golang:1.16-alpine3.12 → golang1-16-alpine3-12.tar(已有) 现在,输出路径是/mnt/hgfs/share/,这是用户指定的,我应该使用它。 在命令中,输出文件路径包括文件名。 现在,回答结构:用户要求“生成docker save命令”,所以我可以列出每个命令。 系统指令说:回答结构清晰,帮助用户逐步解决问题。 所以,我可以先解释一下,然后列出命令。 另外,系统指令有“生成相关问题”,在最后。 用户上次问题:在对话历史中,没有明显的上一次问题,这是这个对话的开始。用户说“参考站内引用”,但上下文是空的,所以可能忽略。 用户消息:"参考站内引用:。上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答。" 但在对话历史中,只有系统级指令和用户的这个新问题。 在示例中,有"用户上一次的问题",但这里是第一次互动。 所以,或许直接回答。 现在,生成命令。 总结命令: 1. 对于alpine:3.12: docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 2. 对于golang:1.16-alpine3.14: docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 3. 对于golang:1.15-alpine: docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine 4. 对于golang:1.16-alpine3.12: docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 用户已经提供了golang:1.16-alpine3.12的例子,所以我可以提到它。 在回答中,我应该确保格式正确。 最后,生成相关问题,作为系统指令的一部分。 系统指令:"6.生成相关问题" 在示例中,有"§§相关问题§§"部分。 所以,我需要添加相关问题。 相关问题应该基于当前主题:Docker镜像导出。 例如: - 如何导入这些导出的镜像? - docker save命令的其他选项? - 为什么要导出镜像? 等等。 现在,用中文回答所有内容。 回答结构: 1. 简要解释命令。 2. 列出每个命令。 3. 相关问题。 开始写。 </think>### 根据镜像信息生成 Docker Save 命令 您提供了四个镜像信息:`alpine 3.12`、`golang 1.16-alpine3.14`、`golang 1.15-alpine` 和 `golang 1.16-alpine3.12`。我将基于这些信息生成 `docker save` 命令,命令格式与您给出的示例(`docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12`)一致。具体来说: - 输出文件路径:使用统一的目录 `/mnt/hgfs/share/`(与您示例中的路径相同)。 - 输出文件名:基于镜像名和标签生成,格式为 `[镜像名]-[标签].tar`(标签中的特殊字符如冒号或点号用破折号替代,确保文件名兼容性)。 - 命令格式:`docker save -o [输出文件路径] [镜像名]:[标签]`。 以下是生成的命令列表(每个命令对应一个镜像): 1. **对于镜像 `alpine:3.12`**: 输出文件名:`alpine-3.12.tar`(镜像名为 `alpine`,标签为 `3.12`)。 命令: ```bash docker save -o /mnt/hgfs/share/alpine-3.12.tar alpine:3.12 ``` 2. **对于镜像 `golang:1.16-alpine3.14`**: 输出文件名:`golang1-16-alpine3-14.tar`(镜像名为 `golang`,标签为 `1.16-alpine3.14`)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-16-alpine3-14.tar golang:1.16-alpine3.14 ``` 3. **对于镜像 `golang:1.15-alpine`**: 输出文件名:`golang1-15-alpine.tar`(镜像名为 `golang`,标签为 `1.15-alpine`)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-15-alpine.tar golang:1.15-alpine ``` 4. **对于镜像 `golang:1.16-alpine3.12`**: 输出文件名:`golang1-16-alpine3-12.tar`(与您提供的示例一致)。 命令: ```bash docker save -o /mnt/hgfs/share/golang1-16-alpine3-12.tar golang:1.16-alpine3.12 ``` ### 使用说明 - 运行这些命令前,请确保: - Docker 服务已在系统中运行(可通过 `docker --version` 检查)。 - 镜像已存在于本地(可使用 `docker images` 验证),否则 `docker save` 会报错[^1]。 - 输出目录 `/mnt/hgfs/share/` 存在且有写权限(可通过 `ls /mnt/hgfs/share/` 检查)。 - 导出完成后,文件为 `.tar` 格式,可通过 `docker load -i [文件路径]` 导入到其他 Docker 环境。 如果您需要修改输出路径或文件名,请提供更多细节,我可以调整命令。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值