The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
gamexg

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

  •  
  •   gamexg · Nov 21, 2015 · 3641 views
    This topic created in 3847 days ago, the information mentioned may be changed or developed.
    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 replies    2015-11-21 23:30:03 +08:00
    semicircle21
        1
    semicircle21  
       Nov 21, 2015
    赞, 有意思的测试, 我猜测有几个问题:
    1. 在测非原子操作耗时的时候, 我不确定 go 的编译器直接优化掉, 有精力的话, 你可以试一下 1. 用 if / else 替代 for 循环, 2. 把 t+=1 封个函数.
    2. 即便真的差距这么大, 也容易用指令流水线的原理来解释.
    semicircle21
        2
    semicircle21  
       Nov 21, 2015
    go 的编译器直接优化掉 for 循环
    -- 删删改改弄错了.
    semicircle21
        3
    semicircle21  
       Nov 21, 2015
    对了, 如果要有实际应用场景的话, 是不是可以考虑用一个 go routine 来维护 t 这个变量, 即增加的时候往一个有 buffer 的 chan 里写 delta, 这样一般不会阻塞, 至于查询, 如果不需要准确值, 直接读 t 就好, 如果需要准确, 就比较棘手了.
    wheatmai
        4
    wheatmai  
       Nov 21, 2015
    对 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  
       Nov 21, 2015
    同意 @semicircle21 的猜测,在非原子操作的情况下,编译器有可能优化了 for 。但是对于原子操作,为了让`t1`对所有线程都是可见的, t1 就不会缓存在某个 cpu core 的 cache 或者其他 core 不可见的地方。同时为了线程安全,`t1`上的操作也不会与其他内存操作进行 reorder 。
    agui2200
        6
    agui2200  
       Nov 21, 2015
    yqf3139
        7
    yqf3139  
       Nov 21, 2015
    @semicircle21
    把 t+=1 封个函数后,非原子操作耗时: 3.168774395s ,原子操作耗时: 11.310976061s
    spacewander
        8
    spacewander  
       Nov 21, 2015
    试了一把加锁版本的,比原子操作慢上两倍。。
    snail1988
        9
    snail1988  
       Nov 21, 2015
    C 的原子操作也很慢, 用 OSX 的 OSAtomicAdd64 编译参数-Os 同样的测试也要 8s 多 Go 版本在我这里 10s ,但是 C 版本的非原子操作超级快,应该是编译器优化了
    chzyer
        10
    chzyer  
       Nov 21, 2015
    @yqf3139 封函数之后还要加 -gcflags '-l' 把 inline 去掉
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3096 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 61ms · UTC 15:01 · PVG 23:01 · LAX 08:01 · JFK 11:01
    ♥ Do have faith in what you're doing.