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

2016-08-29 19:37:55 +08:00
 raincious

写了一个程序,需要不停处理输入。由于输入的长度绝对不会超过 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

2535 次点击
所在节点    Go 编程语言
16 条回复
ooonme
2016-08-29 20:02:56 +08:00
单线程 IO ,跟语言没关系吧
zts1993
2016-08-29 20:20:04 +08:00
20000000 76.6 ns/op 0 B/op 0 allocs/op



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

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

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

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

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

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

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

这个意思……
raincious
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
2016-08-29 22:17:25 +08:00
@raincious
如果我没理解错它的那个 memmove 的话…… 你在 copy 的时候应该可以转成 byte 来用…… 只是创建的时候用 uint64 也是可疑的……
主要是因为内存对齐…… 在没有内存重叠,且满足 8bytes 对齐的情况下(也就是可以一次装入寄存器中), memmove 每次会移动一整个 uint64 ,直到剩下一点尾巴,再进行细微地处理,而不对齐的情况下则是一个 byte 一个 byte 地复制……
chzyer
2016-08-29 22:21:16 +08:00
如果按照 256 ns/op 的速度...

4096 * (1,000,000,000 / 256 ) = 16G/s
这个速度不算慢吧?
southwolf
2016-08-29 22:47:38 +08:00
目测内存对齐的锅吧……
raincious
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
2016-08-30 00:38:15 +08:00
FYI ,
https://groups.google.com/forum/#!topic/golang-nuts/-sAqYxebcUI

另外,你可以把每次 bench 的 cpuinfo 输出,对比 runtime.memmove 占比的变化,就能得出是否是对齐的问题。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/302530

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX