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

golang 单线程原子操作性能怎么这么差?

  •  
  •   gamexg · 2015-11-21 11:23:09 +08:00 · 3069 次点击
    这是一个创建于 3293 天前的主题,其中的信息可能已经有所发展或是发生改变。
    package main
    import (
        "sync/atomic"
        "fmt"
        "time"
    )
    
    
    func main() {
    
        var t1 uint64 = 0
        var t2 uint64 = 0
    
        endChan := make(chan int)
        for i := 0; i < 1000; i++ {
            go func() {
                for i := 0; i < 10000; i++ {
                    atomic.AddUint64(&t1, 1)
                    t2 += 1
                }
                endChan <- 1
            }()
        }
    
        for i := 0; i < 1000; i++ {
            <-endChan
        }
    
        // 测试非原子操作造成的值不正确
        // t1= 10000000
        // t2= 8513393
        fmt.Println("t1=", t1)
        fmt.Println("t2=", t2)
    
    
        // 性能测试
        func() {
            var t1 uint64 = 0
    
            startTime := time.Now()
            for i := 0; i < 1000000000; i++ {
                t1 += 1
            }
            endTime := time.Now()
            fmt.Println("非原子操作耗时:", endTime.Sub(startTime))
            // 非原子操作耗时: 535.0303ms
    
        }()
    
        func() {
            var t1 uint64 = 0
    
            startTime := time.Now()
            for i := 0; i < 1000000000; i++ {
                atomic.AddUint64(&t1, 1)
            }
            endTime := time.Now()
            fmt.Println("原子操作耗时:", endTime.Sub(startTime))
            //原子操作耗时: 14.7758413s
        }()
    }
    

    原子操作的实现不是锁总线?单线程应该锁总线应该不会影响性能吧?

    10 条回复    2015-11-21 23:30:03 +08:00
    semicircle21
        1
    semicircle21  
       2015-11-21 13:01:00 +08:00
    赞, 有意思的测试, 我猜测有几个问题:
    1. 在测非原子操作耗时的时候, 我不确定 go 的编译器直接优化掉, 有精力的话, 你可以试一下 1. 用 if / else 替代 for 循环, 2. 把 t+=1 封个函数.
    2. 即便真的差距这么大, 也容易用指令流水线的原理来解释.
    semicircle21
        2
    semicircle21  
       2015-11-21 13:01:36 +08:00
    go 的编译器直接优化掉 for 循环
    -- 删删改改弄错了.
    semicircle21
        3
    semicircle21  
       2015-11-21 13:06:51 +08:00
    对了, 如果要有实际应用场景的话, 是不是可以考虑用一个 go routine 来维护 t 这个变量, 即增加的时候往一个有 buffer 的 chan 里写 delta, 这样一般不会阻塞, 至于查询, 如果不需要准确值, 直接读 t 就好, 如果需要准确, 就比较棘手了.
    wheatmai
        4
    wheatmai  
       2015-11-21 13:20:50 +08:00
    对 go 的内存模型不是很了解,这里原子操作,`atomic.AddUint64`的[实现]( https://github.com/golang/go/blob/master/src/sync/atomic/64bit_arm.go#L27)其实就是一条[`CMPXCHGQ`]( https://github.com/golang/go/blob/master/src/sync/atomic/asm_amd64.s#L55)指令,即 CAS ,`Q`代表 quadword 。
    wheatmai
        5
    wheatmai  
       2015-11-21 13:38:50 +08:00
    同意 @semicircle21 的猜测,在非原子操作的情况下,编译器有可能优化了 for 。但是对于原子操作,为了让`t1`对所有线程都是可见的, t1 就不会缓存在某个 cpu core 的 cache 或者其他 core 不可见的地方。同时为了线程安全,`t1`上的操作也不会与其他内存操作进行 reorder 。
    agui2200
        6
    agui2200  
       2015-11-21 16:34:56 +08:00
    yqf3139
        7
    yqf3139  
       2015-11-21 18:30:22 +08:00
    @semicircle21
    把 t+=1 封个函数后,非原子操作耗时: 3.168774395s ,原子操作耗时: 11.310976061s
    spacewander
        8
    spacewander  
       2015-11-21 19:08:37 +08:00
    试了一把加锁版本的,比原子操作慢上两倍。。
    snail1988
        9
    snail1988  
       2015-11-21 22:41:08 +08:00
    C 的原子操作也很慢, 用 OSX 的 OSAtomicAdd64 编译参数-Os 同样的测试也要 8s 多 Go 版本在我这里 10s ,但是 C 版本的非原子操作超级快,应该是编译器优化了
    chzyer
        10
    chzyer  
       2015-11-21 23:30:03 +08:00
    @yqf3139 封函数之后还要加 -gcflags '-l' 把 inline 去掉
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1128 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 236ms · UTC 18:59 · PVG 02:59 · LAX 10:59 · JFK 13:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.