英文:
In Go sync.Map why this part of implementation is inconsistent or do I misunderstand something?
问题
sync.Map
是一个并发安全的映射实现。sync.Map
中原始映射的类型实际上是map[any]*entry
。
当我们调用Map.LoadOrStore
并且条目存在时,会调用entry.tryLoadOrStore
函数,以下是该函数的代码:
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
p := e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
// 在第一次加载后复制接口,使该方法更适合逃逸分析:
// 如果我们进入了“加载”路径或条目被清除,我们不应该费心进行堆分配。
ic := i
for {
if e.p.CompareAndSwap(nil, &ic) {
return i, false, true
}
p = e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
}
}
还有另一个函数trySwap
,当我们调用Swap
或Store
时,也会调用该函数。
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
tryLoadOrStore
可以像trySwap
一样根据其逻辑进行实现,但实际上并没有这样做。这里是我的问题:既然它们的逻辑相似,为什么它们没有以相同的方式实现?
当我试图理解时,我认为这是因为参数类型的差异。如果i
是*any
,则不需要进行复制,因为它已经是一个指针,我们不需要关心逃逸分析。但似乎没有特殊的原因来从外部调用者获取地址。
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
然后我不知道为什么这两个函数(以及其他函数)以不同的方式实现。
英文:
sync.Map
is a concurrent-safe map implementation. the type of raw maps in sync.Map
actually is map[any]*entry
.
When we call Map.LoadOrStore
and the entry exists, entry.tryLoadOrStore
is invoked and the following is the code of this function
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
p := e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if e.p.CompareAndSwap(nil, &ic) {
return i, false, true
}
p = e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
}
}
And this is another function trySwap
, when we call Swap
or Store
, this function is also invoked.
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
tryLoadOrStore
can be implemented like trySwap
just based on its logic but it doesn't. Here is my question: since their logic is similar, why they are not implemented in same way?
when I try to understand, I think its because the difference of parameter type, if I
is *any
, it doesn't need to do a copy since it's already a pointer and we does not need to care about the escape analysis. But there seems to be no special reason to get address from the outer caller.
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
Then I have no idea why this two functions (and other functions) are implemented in different way.
答案1
得分: 2
首先,这是Bryan Mills,也就是sync.Map
的原始作者的一句话:
sync.Map
的实现本身就相当复杂!
sync.Map
的代码对逃逸分析非常敏感,并且实现是基于基准测试的结果。
让我们来看一下提交历史,这应该有助于我们理解为什么它们以不同的方式实现。
CL 37342中的初始实现:
- 第一个补丁集中的草案实现非常相似:
func (e *entry) tryStore(i interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&i)) {
return true
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, clean bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&i)) {
return i, false, true
}
}
}
- 在第三个补丁集中,接口复制技巧被添加到了两个实现中:
// 复制接口以使该方法更易于逃逸分析:
// 如果我们进入“load”路径或条目已被删除,我们不应该进行堆分配。
ic := i
(*entry).tryStore
在第五个补丁集中被修改为接受一个指针:
我找不到关于这个更改的注释。很可能是逃逸分析和基准测试的结果。
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
// 复制接口以在第一次加载后使该方法更易于逃逸分析:
// 如果我们进入“load”路径或条目已被删除,我们不应该进行堆分配。
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
}
}
CL 137441中简化了(*entry).tryStore
:
这个改动防止了逃逸到堆的情况:
sync/map.go:178:26: &e.p escapes to heap
sync/map.go:178:26: from &e.p (passed to call[argument escapes]) at
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
CL 399094中将(*entry).tryStore
重命名为(*entry).trySwap
:
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
就是这样。
注意:其他一些小的CL没有列出,例如将实现切换为使用atomic.Pointer
的CL 426074。
英文:
Frirst, a quote from Bryan Mills, the original author of sync.Map
:
> sync.Map
is pretty gnarly to begin with!
The code in sync.Map
is very sensitive to escape analysis, and the implementation is driven by benchmark.
Let's dig into the commit history. It should help us understand why they're implemented in different way.
The initial implementation in CL 37342:
- The draft implementations in the first patchset are similar:
func (e *entry) tryStore(i interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&i)) {
return true
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, clean bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&i)) {
return i, false, true
}
}
}
- The interface-copying trick was added to both implementation in patchset 3:
// Copy the interface to make this method more amenable to escape analysis:
// if we hit the "load" path or the entry is expunged, we shouldn't bother
// heap-allocating.
ic := i
(*entry).tryStore
is modified to accept a pointer in patchset 5:
I can not find a comment on this change. It's most likely a result of the escape analysis and the benchmark tests.
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
}
}
(*entry).tryStore
was simplified in CL 137441:
The change prevents this escape to heap:
sync/map.go:178:26: &e.p escapes to heap
sync/map.go:178:26: from &e.p (passed to call[argument escapes]) at
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
(*entry).tryStore
was renamed to (*entry).trySwap
in CL 399094:
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
That's all.
Note: some other small CLs are not listed, for example, the CL 426074 that switches the implementation to use atomic.Pointer
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论