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

Golang copy 的速度很慢,怎么办?有替代么?

  •  
  •   raincious · 2016-08-29 19:37:55 +08:00 · 2497 次点击
    这是一个创建于 3008 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写了一个程序,需要不停处理输入。由于输入的长度绝对不会超过 N 且这段数据不需要考虑并发,聪明伶俐的楼主为了复用 Buffer ,决定用make([]byte, N)申请一段大[]byte,然后修改其中的内容。

    然后,为了一次能一次修改大段内容,用到了copy。但是测试一下,发现copy在从src复制大段数据的时候,速度真太慢了。代码:

    package main
    
    import (
    	"testing"
    )
    
    func BenchmarkCopy(b *testing.B) {
    	data := make([]byte, 4096)
    	replace := make([]byte, 4050)
    
    	b.ResetTimer()
    	b.ReportAllocs()
    
    	for i := 0; i < b.N; i++ {
    		copy(data[2:], replace)
    	}
    }
    

    在我的机器上测试的结果:

    #Go 1.7
    [rain@localhost golang_copy_test]$ go test -bench . -cpuprofile cpu.out 
    testing: warning: no tests to run
    BenchmarkCopy-2   	 1000000	      1990 ns/op	       0 B/op	       0 allocs/op
    PASS
    ok  	_/home/rain/Develpment/Meta/golang_copy_test	2.016s
    

    复制一段数据需要 1990 纳秒简直握草。 pprof 的结果显示时间大都消耗在了runtime.memmove上。

    换了台机器,结果是这样:

    # Go 1.6
    BenchmarkCopy-8  5000000               256 ns/op               0 B/op          0 allocs/op
    ok      _/home/ubuntu/workspace/go_tests/copy_test      1.552s
    

    但, 256 纳秒也不是很快啊。

    况且,累积效应之后,在楼主真正的代码里,速度啪啪噗的看起来是这样:

    BenchmarkWriter-2   	 1000000	     12745 ns/op	       0 B/op	       0 allocs/op
    PASS
    

    (就是它的错,箭头 men 坚决的说到)

    当然,考虑到楼主是个渣的实际情况,或许是楼主把事情搞错了,于是来求教下解决办法。

    如果真的实在没有办法让copy变快,那么有没有其他办法可以让楼主欢快的直接修改buffer里的大段数据呢?这个需求表述起来应该就像:

    buffer[i:i+1024] = newInput[:1024]
    

    // 那么楼主,为什么你不用for呢:因为更慢啊亲 // 那么楼主,你可以建个 0 Len , N Cap 的 Buffer 来append啊:但是这样也没快多少啊而且之后还需要 reset

    第 1 条附言  ·  2016-08-30 00:04:31 +08:00

    看来是内存对齐的锅。根据 @yangff 的提示做了一些测试,测试代码改成了这样:

    func BenchmarkCopy(b *testing.B) {
        data := make([]byte, 8192)
        replace := make([]byte, 4096)
    
        b.ResetTimer()
        b.ReportAllocs()
    
        for i := 0; i < b.N; i++ {
            copy(data[0:], replace)
        }
    }
    

    所以我可以通过改 copy(data[0:], replace) 这一行改改变对齐,于是:

    当 copy(data[0:], replace):

    BenchmarkCopy-2   	 5000000	       300 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[2:], replace):

    BenchmarkCopy-2   	  500000	      3218 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[4:], replace):

    BenchmarkCopy-2   	  500000	      2960 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[8:], replace):

    BenchmarkCopy-2   	  500000	      2831 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[16:], replace):

    BenchmarkCopy-2   	 1000000	      1398 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[32:], replace):

    BenchmarkCopy-2   	 2000000	       717 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[64:], replace):

    BenchmarkCopy-2   	 3000000	       432 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[128:], replace):

    BenchmarkCopy-2   	 5000000	       290 ns/op	       0 B/op	       0 allocs/op
    

    当 copy(data[256:], replace):

    BenchmarkCopy-2   	 5000000	       291 ns/op	       0 B/op	       0 allocs/op
    
    第 2 条附言  ·  2016-08-30 12:41:49 +08:00
    16 条回复    2016-08-30 00:38:15 +08:00
    ooonme
        1
    ooonme  
       2016-08-29 20:02:56 +08:00 via iPhone
    单线程 IO ,跟语言没关系吧
    zts1993
        2
    zts1993  
       2016-08-29 20:20:04 +08:00   ❤️ 1
    20000000 76.6 ns/op 0 B/op 0 allocs/op



    LZ 感觉你应该再换一台机器试试...
    wweir
        3
    wweir  
       2016-08-29 20:23:23 +08:00 via Android   ❤️ 1
    mem copy 慢,我猜栽在了 CPU 的 numa 上。
    不妨试试利用 runtime 锁定协程所在的线程,或者用 gccgo 编译。
    wweir
        4
    wweir  
       2016-08-29 20:24:56 +08:00 via Android   ❤️ 1
    @wweir 错了,锁定协程所在的核。手机码字,思绪都乱了
    raincious
        5
    raincious  
    OP
       2016-08-29 20:29:07 +08:00
    @ooonme
    能更明确一点么……

    @zts1993
    震惊,看来如果实在没法修好这个问题,我可以先暂时忽略它。

    @wweir
    好的,我先研究下这个,感谢。
    pubby
        6
    pubby  
       2016-08-29 20:53:12 +08:00   ❤️ 1
    BenchmarkCopy-4 10000000 193 ns/op 0 B/op 0 allocs/op
    PASS
    ok go_tests 2.206s
    rrfeng
        7
    rrfeng  
       2016-08-29 21:12:28 +08:00
    扔到另一个 goroutine 里 copy 哈哈哈

    不然你嫌弃它慢也没有什么意啊

    --- 一本正经的瞎说。
    yangff
        8
    yangff  
       2016-08-29 21:19:10 +08:00   ❤️ 1
    你尝试用 uint64 类型试试?
    raincious
        9
    raincious  
    OP
       2016-08-29 21:35:14 +08:00
    @yangff
    是这样么?:
    data := make([]byte, uint64(4096))
    replace := make([]byte, uint64(4050))
    但是并没有改善。

    @wweir
    试了下在 Benchmark 的开始加入
    runtime.LockOSThread()
    看似没啥效果 :(

    看起来跟计算机本身有关系。我决定暂时先把这个问题放一边好了,先把程序写出来然后再看是怎么回事。
    yangff
        10
    yangff  
       2016-08-29 21:52:06 +08:00   ❤️ 1
    @raincious
    data := make([]uint64, 4096 / 8)
    replace := make([]uint64, 4050 / 8 + 1)

    这个意思……
    raincious
        11
    raincious  
    OP
       2016-08-29 22:13:39 +08:00
    @yangff
    棒极了!
    BenchmarkCopy-2 5000000 278 ns/op 0 B/op 0 allocs/op
    PASS
    ok _/home/rain/Develpment/Meta/golang_copy_test 1.682s

    不过这就意味着如果我直接去用这样的方法,得手动每 8 个 byte 合并成一个 uint64 ,这也就不见得快了。

    但,也奇怪啊,这两个数据尺寸是一样大的,为什么 copy 速度会不一样( runtime.memmove 的代码是 ASM ,已槽懵)。
    yangff
        12
    yangff  
       2016-08-29 22:17:25 +08:00   ❤️ 1
    @raincious
    如果我没理解错它的那个 memmove 的话…… 你在 copy 的时候应该可以转成 byte 来用…… 只是创建的时候用 uint64 也是可疑的……
    主要是因为内存对齐…… 在没有内存重叠,且满足 8bytes 对齐的情况下(也就是可以一次装入寄存器中), memmove 每次会移动一整个 uint64 ,直到剩下一点尾巴,再进行细微地处理,而不对齐的情况下则是一个 byte 一个 byte 地复制……
    chzyer
        13
    chzyer  
       2016-08-29 22:21:16 +08:00   ❤️ 1
    如果按照 256 ns/op 的速度...

    4096 * (1,000,000,000 / 256 ) = 16G/s
    这个速度不算慢吧?
    southwolf
        14
    southwolf  
       2016-08-29 22:47:38 +08:00   ❤️ 1
    目测内存对齐的锅吧……
    raincious
        15
    raincious  
    OP
       2016-08-29 23:18:06 +08:00
    @chzyer
    故事是这样的:楼主原先写了个 1 allocs/op, 16 B/op 的函数。

    缺点你也看到了,一个 1 allocs ,同时需要建立很多的[]byte{}来 append ,之后 mallocgc 耗时会比较高。

    然后那个函数的执行速度是 190 ns/op 。然后热爱性能的楼主决定优化一下那个函数,让它更快。这个帖子发生在优化后……

    @yangff
    仍然在消化这些知识。先感谢。
    hooluupog
        16
    hooluupog  
       2016-08-30 00:38:15 +08:00   ❤️ 1
    FYI ,
    https://groups.google.com/forum/#!topic/golang-nuts/-sAqYxebcUI

    另外,你可以把每次 bench 的 cpuinfo 输出,对比 runtime.memmove 占比的变化,就能得出是否是对齐的问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   917 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 21:28 · PVG 05:28 · LAX 13:28 · JFK 16:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.