go - 在 Gosync.Map 中,为什么这部分实现不一致或者我误解了什么?

标签 go implementation language-implementation

sync.Map 是一个并发安全的 map 实现。 sync.Map 中原始 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
    }

    // 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
        }
    }
}

这是另一个函数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
        }
    }

然后我不知道为什么这两个函数(以及其他函数)以不同的方式实现。

最佳答案

首先,a quote来自 sync.Map 的原作者 Bryan Mills:

sync.Map is pretty gnarly to begin with!

sync.Map中的代码对逃逸分析非常敏感,并且实现由基准测试驱动。

让我们深入研究一下提交历史记录。它应该可以帮助我们理解为什么它们以不同的方式实现。

初步实现于CL 37342 :

  1. first patchset 中的实现草案相似之处:
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
        }
    }
}
  • 接口(interface)复制技巧已添加到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 被修改为接受 patchset 5 中的指针:
  • 我找不到对此更改的评论。这很可能是逃逸分析和基准测试的结果。

    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).tryStoreCL 137441 中得到简化:

    此更改可防止这种逃逸到堆的情况:

    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 未列出,例如 CL 426074将实现切换为使用atomic.Pointer。

    关于go - 在 Gosync.Map 中,为什么这部分实现不一致或者我误解了什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76186937/

    相关文章:

    haskell - 懒惰和纯洁有什么关系?

    unit-testing - Go 客户端找不到我的测试

    function - 可选参数?

    php - 什么是 https 和 SSL?它们是如何工作的?如何在 PHP 中使用它们?

    java - Java中的反射是如何实现的?

    javascript - 如何可靠地检测 JavaScript 中的外来对象?

    go - 如果代码未托管在 Internet 上,如何导入包

    go - 将固定长度的填充行写入文件 Go

    java - 实现效果不佳

    c - 启用宏的语言如何跟踪源代码以进行调试?