V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
sunkai0609
V2EX  ›  Go 编程语言

Golang sync.Map tryLoadOrStore 函数看不懂其中的 ic := i

  •  1
     
  •   sunkai0609 · 2021-10-17 23:08:30 +08:00 · 2030 次点击
    这是一个创建于 1114 天前的主题,其中的信息可能已经有所发展或是发生改变。
    // tryLoadOrStore atomically loads or stores a value if the entry is not
    // expunged.
    //
    // If the entry is expunged, tryLoadOrStore leaves the entry unchanged and
    // returns with ok==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
    		}
    	}
    }
    

    为什么不用 ic := i 就会去堆上申请内存呢? 有巨佬或者彦祖知道吗~

    整个函数感觉可以直接都放在 for 循环中,为什么要把前两个 if 判断单独拿出来?是因为放在 for 循环外有更好的性能吗

    10 条回复    2021-10-19 16:30:35 +08:00
    caviar
        1
    caviar  
       2021-10-17 23:32:00 +08:00   ❤️ 2
    并不是特别了解 go,纯粹从已有代码的注释上推断,可能有误:
    这两个问题是相关的,把那两句 if 判断单独拿出来 + 显式的复制一次来避免 entry 已经存在或者是 expunged 状态下的 heap allocation 。

    如果只有一个循环,那么 `atomic.CompareAndSwapPointer` 这里会让 `ic` 也就是 `i` escape,导致 `i` 永远需要 heap allocation 即便 `i` 并没有真正的替换进去。

    而现在这种写法,`i` 可以在 stack 上,`ic` 需要在 heap 上,如果 entry 符合那两种不需要更新 map 的情况,`ic := i` 不会执行到,也就不会有 heap allocation 了。
    Co1a
        2
    Co1a  
       2021-10-17 23:36:40 +08:00
    今天刚好复习了一下[Gopher Con](
    )
    感觉和当中的主题十分类似,Golang 当中的 Stack Frame 是以 function 划分的,其中每个 function 对应着不同的内存区域,当 i 传递至 tryLoadOrStore 本质上是重新开辟了一层内存空间? ic 在当前当前作用于结束后(return)不再被使用,于是乎就直接分配在栈上没有再向上传递?不知道自己理解的对不对,还请大佬上来捞一下
    caviar
        3
    caviar  
       2021-10-17 23:45:43 +08:00
    @caviar 总的来说,这里是个取舍吧,对于 `tryLoadOrStore` 来说, `load` path 需要越快越好,而 `store` path 可以稍微 costly 一些。
    bruce0
        4
    bruce0  
       2021-10-18 10:32:55 +08:00   ❤️ 2
    `为什么不用 ic := i 就会去堆上申请内存呢` 这个 我感觉是内存逃逸的问题吧, i 是一个 interface 类型的变量, 可以看做是传指针的, ```atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic))``` 直接传 `i` 的话, 因为 i 是一个外部变量, 函数作用域结束后还会存在, 所以 编译器在做内存逃逸分析的时候, 会分配到堆上
    XTTX
        5
    XTTX  
       2021-10-18 18:30:19 +08:00
    @bruce0 我是真的没有弄明白为什么要用 ic: = i 。· tryLoadOrStore(i interface{}) · 这里用的应该是 value semantic, i 是一个新的拷贝才对。 这是我的理解, 不对的地方请指出。
    bruce0
        6
    bruce0  
       2021-10-18 22:25:52 +08:00
    @XTTX @sunkai0609 不好意思,上午的时候 没有仔细分析 说的有点问题。刚去翻了源码看了一下。又想了一下。

    第一点,上午我说的 interface 类型的,可以看做指针是有问题的。其实 interface 也是一个类型。不能简单当做指针理解。

    重点,为什么不用 ic := i 就会去堆上申请内存呢, 其实,这里还是内存逃逸的问题。

    如果 `i` 传的是一个 非指针类型的变量, 那 在调用 tryLoadOrStore() 时,是复制的,

    这里 `atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic))` 是取地址的,就会导致内存逃逸。

    而加上 `ic := i` 之后, 后面取的是 `ic`的地址,就不会导致 `i` 逃逸了

    上一段代码就好理解了


    ```
    func main() {
    }

    func fun1(i1 interface{}) {
    atomic.CompareAndSwapPointer(nil, nil, unsafe.Pointer(&i1))
    }

    func fun2(i2 interface{}) {
    ic := i2
    atomic.CompareAndSwapPointer(nil, nil, unsafe.Pointer(&ic))
    }
    ```

    用 ` go build -gcflags="-m -l" .\main.go` 命令 做内存逃逸分析

    结果是

    .\main.go:11:11: moved to heap: i1
    .\main.go:15:11: leaking param: i2
    .\main.go:16:2: moved to heap: ic

    i1 分配到堆上了 ic 分配到堆上了 i2 是在栈上的, 相当于通过一次复制, 阻断了内存逃逸

    我也是菜鸡 说的可能也不对 目前来看 这样似乎能解释的通
    SorcererXW
        7
    SorcererXW  
       2021-10-18 22:31:16 +08:00
    简单说就是:
    如果没有 ic:=i,无论是在 fast path 还是 slow path,i 必然都会逃逸到 heap 上。
    但是在后面加上 ic := i,只会在 slow path 上,才会将 ic 分配在 heap 上。因为这个时候实际上是将 ic move to heap
    XTTX
        8
    XTTX  
       2021-10-18 22:47:29 +08:00
    @bruce0 谢谢回复。 有空了我多了解一下这方面的知识。
    lance6716
        9
    lance6716  
       2021-10-19 16:21:41 +08:00
    非科班。感觉是 `i` 是传参进来的,一开始它的地址一定在栈上,如果要保存走 unsafe 的 `&i` 的话需要 “手动”移动到堆上。

    `ic` 一开始编译器就知道放在堆上,不需要手动操作。
    lance6716
        10
    lance6716  
       2021-10-19 16:30:35 +08:00
    然后注释是说,如果在函数的前 7 行就返回了,还省下了在堆上申请 ic (相比于一上来就是一个 for )
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5527 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 08:27 · PVG 16:27 · LAX 00:27 · JFK 03:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.