スライス追加操作を高速化するには、十分な容量を割り当てる必要があります。スライスを追加するには2つの方法があります。コードは次のとおりです。
_func BenchmarkSliceAppend(b *testing.B) {
a := make([]int, 0, b.N)
for i := 0; i < b.N; i++ {
a = append(a, i)
}
}
func BenchmarkSliceSet(b *testing.B) {
a := make([]int, b.N)
for i := 0; i < b.N; i++ {
a[i] = i
}
}
_
そして結果は:
BenchmarkSliceAppend-4 200000000 7.87 ns/op 8 B/op 0 allocs/op
BenchmarkSliceSet-4 300000000 5.76 ns/op 8 B/op
_a[i] = i
_はa = append(a, i)
より高速であり、理由を知りたいですか?
a[i] = i
は、i
という値をa[i]
に割り当てるだけです。これはnot追加であり、単純な assignment です。
今、追加:
a = append(a, i)
理論的には次のことが起こります:
これは組み込みの append()
関数を呼び出します。そのためには、最初にa
スライス(スライスヘッダー、バッキング配列はヘッダーの一部ではありません)をコピーする必要があり、値i
を含む可変引数パラメーターの一時スライスを作成する必要があります。
次に、a = a[:len(a)+1]
のように十分な容量がある場合はa
をスライスし直す必要があります(この場合、append()
内のa
に新しいスライスを割り当てる必要があります)。
(a
に「インプレース」で追加を行うのに十分な容量がない場合、新しい配列を割り当て、スライスのコンテンツをコピーしてから、割り当て/追加を実行する必要がありますが、ここではそうではありません。)
次に、i
をa[len(a)-1]
に割り当てます。
次に、append()
から新しいスライスを返し、この新しいスライスがローカル変数a
に割り当てられます。
ここでは、単純な割り当てに比べて多くのことが起こります。これらの手順の多くが最適化またはインライン化されている場合でも、i
をスライスの要素に割り当てるための最低限の追加として、スライスタイプのローカル変数a
(スライスヘッダー)- ループの各サイクルで更新する必要があります。
この質問が投稿されてから、Goコンパイラまたはランタイムのいくつかの改善が導入されたようですので、(Go 1.10.1
)append
とインデックスによる直接割り当ての間に大きな違いはありません。
また、OOMパニックのため、ベンチマークを少し変更する必要がありました。
package main
import "testing"
var result []int
const size = 32
const iterations = 100 * 1000 * 1000
func doAssign() {
data := make([]int, size)
for i := 0; i < size; i++ {
data[i] = i
}
result = data
}
func doAppend() {
data := make([]int, 0, size)
for i := 0; i < size; i++ {
data = append(data, i)
}
result = data
}
func BenchmarkAssign(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAssign()
}
}
func BenchmarkAppend(b *testing.B) {
b.N = iterations
for i := 0; i < b.N; i++ {
doAppend()
}
}
結果:
➜ bench_slice_assign go test -bench=Bench .
goos: linux
goarch: AMD64
BenchmarkAssign-4 100000000 80.9 ns/op
BenchmarkAppend-4 100000000 81.9 ns/op
PASS
ok _/home/isaev/troubles/bench_slice_assign 16.288s