In Go sync.Map why this part of implementation is inconsistent or do I misunderstand something?

huangapple go评论73阅读模式
英文:

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,当我们调用SwapStore时,也会调用该函数。

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中的初始实现:

  1. 第一个补丁集中的草案实现非常相似:
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
		}
	}
}
  1. 第三个补丁集中,接口复制技巧被添加到了两个实现中:
// 复制接口以使该方法更易于逃逸分析:
// 如果我们进入“load”路径或条目已被删除,我们不应该进行堆分配。
ic := i
  1. (*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.PointerCL 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:

  1. 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
		}
	}
}
  1. 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
  1. (*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.

huangapple
  • 本文由 发表于 2023年5月6日 11:07:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/76186937.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定