专栏名称: GoCN
最具规模和生命力的 Go 开发者社区
目录
相关文章推荐
河北交通广播  ·  【992 | ... ·  昨天  
河北交通广播  ·  【992 | ... ·  昨天  
河北交通广播  ·  【992 | ... ·  昨天  
河北交通广播  ·  范县凌晨突发地震!网友:被震醒了… ·  昨天  
Excel之家ExcelHome  ·  AI时代Excel会被淘汰?LOOKUP函数 ... ·  4 天前  
51好读  ›  专栏  ›  GoCN

充分利用每个字节:Go 的内存打包秘密揭晓!

GoCN  · 公众号  ·  · 2024-04-19 12:03

正文

Go 以其简单的设计而闻名,受到云原生应用程序的青睐。它拥有独特的天赋和特殊功能,由于其幕后的工程奇迹,常常使天平偏向偏袒。

为什么要讨论结构中的填充?

在快节奏的软件开发世界中,生成式人工智能可以在几分钟内生成文章,因此深入研究人工智能可能掩盖的主题至关重要。本博客旨在阐明 Go 的一个看似小众但关键的方面——结构填充。这个聪明的功能可以非常有效地优化内存使用,以至于它可能会为考虑转换的 Java 开发人员带来好处。就像 Go 说的:“为简单而来,为内存管理而留下!”

在 Go 中,结构体是数据组织的基本构建块。结构是一种用户定义的类型,它将各种数据类型的元素分组在一个名称下,类似于数据库中的记录。这使得它对于数据传输和一起管理相关数据非常有用,就像每个工具都有特定角色的自定义工具包一样。

让我们看一个比经典 Car 结构更现代、更相关的示例 - 考虑用户身份验证模块:

type UserSession struct {    userID    uint64  // 8 bytes    timestamp uint64  // 8 bytes    isActive  bool    // 1 byte    isLoggedIn bool   // 1 byte}

结构填充解释

现在,让我们深入研究结构填充。Go 中的结构填充可确保结构内的字段根据 CPU 的字大小对齐,以促进更快的访问。但是,这是什么意思?让我们来分解一下:

  • 字大小:指CPU一次可以处理的字节数。在 64 位系统上,字大小为 8 字节,这意味着 CPU 在一次操作中可以处理 8 字节的数据。

  • 对齐:为了让CPU最有效地处理数据,数据需要根据字大小在内存中对齐。这意味着数据类型理想地应该从其大小的倍数的内存地址开始,直到字大小。

例如,在 64 位系统上:

  • 1 字节布尔字段应与 1 字节边界(任何地址)对齐。

  • 4 字节 int32 字段应与 4 字节边界对齐(地址可被 4 整除)。

  • 8 字节 uint64 字段应与 8 字节边界对齐(地址可被 8 整除)。


当结构体字段没有自然地与这些边界对齐时,Go 编译器将自动在字段之间插入“填充”——额外的空间。这种填充可确保每个字段从与其大小一致的地址开始,从而优化运行时的内存访问。

例如,考虑一个虚构的应用程序高性能用户会话管理器
type UserSession struct {    isActive  bool    // 1 byte    // 7 bytes of padding here to align the next field    userID    uint64  // 8 bytes    isAdmin   bool    // 1 byte    // 7 bytes of padding here to align the next field    timestamp uint64  // 8 bytes}
  • 在此示例中,isActive 和 isAdmin 字段各为 1 字节,但后面跟着 8 字节 uint64 字段(用户 ID 和时间戳)。

    为了确保 userID 和 timestamp 字段正确对齐,Go 编译器在 isActive 和 isAdmin 字段后分别添加 7 个字节的填充。

  • 此填充确保 CPU 可以有效地访问 8 字节字段,因为它们现在与内存中的 8 字节边界对齐。虽然填充看起来像是浪费内存,但这是 Go 编译器为优化内存访问性能而做出的权衡。


使用不安全包进行初步分析

使用 unsafe 包,我们可以检查该结构的大小和对齐方式:
package main
import ( "fmt" "unsafe")
func main() { var s UserSession fmt.Println("Size of Session struct:", unsafe.Sizeof(s)) fmt.Println("Alignment of Session struct:", unsafe.Alignof(s)) fmt.Println("Offset of isActive:", unsafe.Offsetof(s.isActive)) fmt.Println("Offset of userID:", unsafe.Offsetof(s.userID)) fmt.Println("Offset of isAdmin:", unsafe.Offsetof(s.isAdmin)) fmt.Println("Offset of timestamp:", unsafe.Offsetof(s.timestamp))
}

该脚本将输出 Session 结构的大小以及每个字段的偏移量。最初,我们可能会发现该结构使用了比所需更多的内存,因为在 bool 字段之后添加了填充以将结构对齐到 8 字节(因为它是结构中最大的对齐要求)。

以下是所提供的代码片段中每个函数调用的作用:
  • unsafe.Sizeof(s) :此函数返回 struct 的总大小(以字节为单位),包括 Go 添加的用于对齐内存中字段的任何填充。

  • unsafe.Alignof(s) :该函数返回 struct 的对齐方式。对齐指示结构体的开头应如何在内存中定位。通常,这是结构中任何字段的最大对齐方式,这有助于确保所有字段都满足其对齐要求。

  • unsafe.Offsetof(s.userID) :该函数返回 struct s 中字段 userID 的字节偏移量。偏移量是从结构体的开头到字段的开头的距离。

  • unsafe.Offsetof(s.timestamp) :与 userID 的偏移量类似,这给出了从结构体开头到时间戳字段的距离。


这些函数对于理解结构体的内存布局特别有用,这有助于优化性能,特别是在系统编程中,或者在与需要精确控制内存布局的硬件或操作系统 API 交互时。

用于结构布局分析的 Go 工具

虽然 unsafe 包提供了一种检查结构布局的低级方法,但 Go 提供了专门为此目的而设计的更方便的工具:go tool structlayout 。该工具可用于可视化结构的内存布局,包括字段偏移和填充字节。这是使用 go tool structlayout 的示例:

go tool structlayout -layout UserSession

此命令将打印 UserSession 结构布局的详细细分,从而更容易识别编译器引入的任何填充。

优化结构布局

为了最大限度地减少内存使用量,我们可以对字段重新排序,将所有 8 字节字段放在前面,然后是较小的字段,从而减少填充的需要:

type OptimizedSession struct {    userID    uint64  // 8 bytes    timestamp uint64  // 8 bytes    isActive  bool    // 1 byte    isAdmin   bool    // 1 byte    // Padding of 6 bytes here to align to 8 bytes}

替代优化:对小字段使用指针

在某些情况下,根据这些字段的访问方式,如果较小的字段(isActive 和 isAdmin)经常与较大的字段一起访问,则另一种优化技术可能是使用指针。这有助于减少总体内存占用,特别是在处理大量结构时。但是,在做出此决定时,重要的是要考虑内存使用量和代码复杂性之间的权衡。

分析内存影响

我们可以使用Go中的基准测试来比较优化前后的内存使用情况。该测试将创建大量会话结构并测量使用的总内存:

package main
import ( "testing" "unsafe")
func BenchmarkOriginalSession(b *testing.B) { for i := 0; i < b.N; i++ { _ = UserSession{ isActive: true, userID: 123456789012345, isAdmin: false, timestamp: 1609459200, } }}






请到「今天看啥」查看全文