Golang sync.Map tryLoadOrStore 函数看不懂其中的 ic := i - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
sunkai0609
V2EX    Go 编程语言

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

  •  1
     
  •   sunkai0609 2021-10-17 23:08:30 +08:00 2466 次点击
    这是一个创建于 1455 天前的主题,其中的信息可能已经有所发展或是发生改变。
    // 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     Solana     840 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 20:47 PVG 04:47 LAX 13:47 JFK 16:47
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86