你正在使用哪个版本的Go( go version
)?
$ go version
go version go1.18.3 linux/amd64
这个问题在最新版本中是否会重现?
是的,在 go version devel go1.19-d3ffff2790 Tue Jun 28 13:01:41 2022 +0000 linux/amd64
中会重现。
你正在使用什么操作系统和处理器架构( go env
)?
go env
输出
$ go env
GO111MODULE="on"
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/ainar/.cache/go-build"
GOENV="/home/ainar/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/ainar/go/pkg/mod"
GONOPROXY="REMOVED"
GONOSUMDB="REMOVED"
GOOS="linux"
GOPATH="/home/ainar/go"
GOPRIVATE="REMOVED"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/ainar/go/go1.18"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/ainar/go/go1.18/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.18.3"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1722739114=/tmp/go-build -gno-record-gcc-switches"
发现/建议
slices.Clone
目前被定义为:
// Clone returns a copy of the slice.
// The elements are copied using assignment, so this is a shallow clone.
func Clone[S ~[]E, E any](s S) S {
// Preserve nil in case it matters.
if s == nil {
return nil
}
return append(S([]E{}), s...)
}
在这里似乎优化了输出本质上是 make
+ copy
的内容。但是如果我们明确地这样做:
// Clone returns a copy of the slice.
// The elements are copied using assignment, so this is a shallow clone.
func Clone[S ~[]E, E any](s S) (clone S) {
// Preserve nil in case it matters.
if s == nil {
return nil
}
clone = make(E, len(s))
copy(clone, s)
return clone
}
然后对其进行基准测试(参见 https://go.dev/play/p/WHvD0zJ33S1 ),那么新的函数通常表现得更好:
goos: linux
goarch: amd64
cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
BenchmarkFoo/10/clone_std-16 11532111 95.56 ns/op 80 B/op 1 allocs/op
BenchmarkFoo/10/clone_our-16 16797295 70.03 ns/op 80 B/op 1 allocs/op
BenchmarkFoo/100/clone_std-16 2828976 425.5 ns/op 896 B/op 1 allocs/op
BenchmarkFoo/100/clone_our-16 3188673 370.0 ns/op 896 B/op 1 allocs/op
BenchmarkFoo/1000/clone_std-16 369156 3092 ns/op 8192 B/op 1 allocs/op
BenchmarkFoo/1000/clone_our-16 407413 2963 ns/op 8192 B/op 1 allocs/op
BenchmarkFoo/10000/clone_std-16 49088 23761 ns/op 81920 B/op 1 allocs/op
BenchmarkFoo/10000/clone_our-16 51192 23345 ns/op 81920 B/op 1 allocs/op
BenchmarkFoo/100000/clone_std-16 3909 267590 ns/op 802821 B/op 1 allocs/op
BenchmarkFoo/100000/clone_our-16 4609 255535 ns/op 802822 B/op 1 allocs/op
BenchmarkFoo/1000000/clone_std-16 879 1197480 ns/op 8003595 B/op 1 allocs/op
BenchmarkFoo/1000000/clone_our-16 918 1186598 ns/op 8003594 B/op 1 allocs/op
PASS
ok command-line-arguments 15.665s
从汇编代码来看,当前版本使用的是 runtime.growslice
,而新版本使用的是 runtime.mallocgc
。汇编代码中还有其他一些变化。
4条答案
按热度按时间rslzwgfq1#
我相信我们使用的是
append
版本,这样我们可以让运行时向上取整到下一个大小类。这将允许调用者在不立即触发重新分配和复制的情况下向结果追加一些项目。append
版本也知道它不需要在用副本覆盖分配之前将其归零。考虑到这些,如果
append
版本不是更慢就好了。看起来对于大数组来说并不是这样,但对于小数组还是有一些开销的。j7dteeu82#
如果这里有什么需要修复的,那就是在编译器里,所以改个标题。
ozxc1zmp3#
追加版本也知道在用副本覆盖它之前不需要将分配值归零。
对于make+copy也是如此:https://go-review.googlesource.com/c/go/+/146719
我认为我们应该做的是检测
append(X, s...)
的情况,其中X在编译器中已知具有长度和容量为0,并将其重写为makeextended+copy,其中makeextended是一种make变体,可以像append一样计算新的切片大小,但可以假设比通用append情况更多的约束。这应该比make+copy稍慢。优化是否能显著提高速度以证明增加的复杂性需要进行基准测试。twh00eeo4#
我的两分钱建议是编写可读的库代码,而不是"酷炫的一行代码",即选项2(make+copy)是一个更好的实现方式,可以更快地阅读 - 而且碰巧更快,所以...太棒了!