前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >再谈Golang内存对齐

再谈Golang内存对齐

作者头像
LA0WAN9
发布2021-12-14 09:12:19
4980
发布2021-12-14 09:12:19
举报
文章被收录于专栏:火丁笔记火丁笔记

关于 Golang 内存对齐,昨天已经写了一篇「浅谈Golang内存对齐」,可惜对一些细节问题的讨论语焉不详,于是便有了今天这篇「再谈Golang内存对齐」。

让我们回想一下 groupcachesync.WaitGroup 中的做法,为了规避在 32 位环境下 atomic 操作 64 位数的 BUG,它们采取了截然不同的做法:

代码语言:javascript
复制
// groupcache
type Group struct {
	name string
	getter Getter
	peersOnce sync.Once
	peers PeerPicker
	cacheBytes int64
	mainCache cache
	hotCache cache
	loadGroup flightGroup
	_ int32 // force Stats to be 8-byte aligned on 32-bit platforms
	Stats Stats
}

// sync.WaitGroup
type WaitGroup struct {
	noCopy noCopy

	// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
	// 64-bit atomic operations require 64-bit alignment, but 32-bit
	// compilers do not ensure it. So we allocate 12 bytes and then use
	// the aligned 8 bytes in them as state, and the other 4 as storage
	// for the sema.
	state1 [3]uint32
}

func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
	if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
		return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
	} else {
		return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
	}
}

问题:为什么 groupcache 不用考虑外部地址,只要内部对齐就可以实现 64 位对齐?

为了搞清楚这个问题,让我们回想一下 atomic 文档最后关于 64 位对齐的相关描述:

On ARM, 386, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

其中我们关心的是最后一句话:变量、结构体、数组、切片的第一个字是 64 位对齐的。为了验证这一点,我构造了一个包含 int64 的 struct,看它的地址是否是 8 的倍数:

代码语言:javascript
复制
package main

import (
	"fmt"
	"time"
	"unsafe"
)

type foo struct {
	bar int64
}

// GOARCH=386 go run main.go
func main() {
	for range time.Tick(time.Second) {
		f := &foo{}
		p := uintptr(unsafe.Pointer(f))
		fmt.Printf("%p: %d, %d\n", f, p, p%8)
	}
}

按照常理来说,当我们在 32 位环境(GOARCH=386)下运行的时候,struct 的地址应该只能满足 32 位对齐,也就是 4 的倍数,不过测试发现,struct 的地址竟然满足 64 位对齐,也就是是 8 的倍数。既然外部已经是对齐的了,那么只要内部对齐就可以实现 64 位对齐。

问题:为什么 sync.WaitGroup 不像 groupcache 那样实现 64 位对齐。

两者之所以采用了不同的 64 位对齐实现方式,是因为两者的使用场景不同。在实际使用的时候,sync.WaitGroup 可能会被嵌入到别的 struct 中,因为不知道嵌入的具体位置,所以不可能通过预先加入 padding 的方式来实现 64 位对齐,只能在运行时动态计算。而 groupcache 则不会被嵌入到别的 struct 中,如果你硬要嵌入,可能会出问题:

代码语言:javascript
复制
package main

import (
	"github.com/golang/groupcache"
)

type foo struct {
	bar int32
	g groupcache.Group
}

// GOARCH=386 go run main.go
func main() {
	f := foo{}
	f.g.Stats.Gets.Add(1)
}

当我们在 32 位环境(GOARCH=386)下运行的时候,会看到如下 panic 信息:

panic: unaligned 64-bit atomic operation

当我们在 32 位环境,按 4 字节对齐,所以 g 的偏移量是 4 而不是 8,如此一来,虽然 groupcache 内部通过 _ int32 实现了相对的 64 位对齐,但是因为外部没有实现 64 位对齐,所以在执行 atomic 操作的时候,还是会 panic(如果 bar 是 int64 就不会 panic)。

问题:为什么 sync.WaitGroup 中的 state1 不换成 一个 int64 加一个 int32?

更新:之前解释有误,现在新版已经换了 🙂

问题:为什么 sync.WaitGroup 中的 state1 不换成 一个12byte?

原方案中 state1 的类型是 3uint32,取两个 uint32 做 statep,剩下的一个 uint32 做 semap。为什么不换成 12byte,取 8 个 byte 做 statep,剩下的 4 个 byte 做 semap?

想要搞清楚这个问题,我们需要回顾一下 golang 关于内存对齐保证的描述:

  • For a variable x of any type: unsafe.Alignof(x) is at least 1.
  • For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
  • For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array’s element type.

其中的重点是:对 struct 而言,它的对齐取决于其中所有字段对齐的最大值;对于 array 而言,它的对齐等于元素类型本身的对齐。因为 noCopy 的大小是 0,所以 struct 的对齐实际上就取决于 state1 字段的对齐。

  • 当 state1 的类型是 3uint32 的时候,那么 struct 的对齐就是 4。
  • 当 state1 的类型是 12byte 的时候,那么 struct 的对齐就是 1。

如果 state1 换成 12byte,那么因为 struct 的对齐是 1,会导致 struct 的地址不再是 4 的倍数,uintptr(unsafe.Pointer(&wg.state1))%8 的结果可能是从 0 到 7 的任意数,如此一来需要 padding 的时候,你就无法实现剩余空间恰好可以充当 padding 的效果了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档