web-dev-qa-db-fra.com

Golang slice append vs assign performance

Pour accélérer l'opération d'ajout de tranche, nous devons allouer suffisamment de capacité. Il y a deux façons d'ajouter une tranche, voici le code:

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
    }
}

Et le résultat est:

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 Est plus rapide que a = append(a, i) et je veux savoir pourquoi?

13
Bryce

a[i] = i Affecte simplement la valeur i à a[i]. Ce n'est pas ajouté, c'est juste un simple affectation .

Maintenant, ajoutez:

a = append(a, i)

En théorie, ce qui suit se produit:

  1. Cela appelle la fonction intégrée append() . Pour cela, il doit d'abord copier la tranche a (en-tête de tranche, le tableau de support ne fait pas partie de l'en-tête), et il doit créer une tranche temporaire pour le paramètre variadic qui contiendra la valeur i.

  2. Ensuite, il doit redimensionner a s'il a suffisamment de capacité (il a dans votre cas) comme a = a[:len(a)+1] - ce qui implique d'assigner la nouvelle tranche à a à l'intérieur de la fonction append().
    (Si a n'aurait pas la capacité suffisante pour faire l'ajout "sur place", un nouveau tableau devrait être alloué, le contenu de la tranche copié, puis l'assignation/l'ajout serait exécuté - mais ce n'est pas le cas ici.)

  3. Affecte ensuite i à a[len(a)-1].

  4. Retourne ensuite la nouvelle tranche de append(), et cette nouvelle tranche est affectée à la variable locale a.

Beaucoup de choses se produisent ici par rapport à une simple affectation. Même si bon nombre de ces étapes sont optimisées et/ou intégrées, comme ajout minimum à l'attribution de i à un élément de la tranche, la variable locale a du type de tranche (qui est un en-tête de tranche) doit être mis à jour à chaque cycle de la boucle .

Lecture recommandée: The Go Blog: Tableaux, tranches (et chaînes): La mécanique de 'append'

12
icza

Il semble que certaines améliorations du compilateur Go ou du runtime aient été introduites depuis la publication de cette question, alors maintenant (Go 1.10.1) il n'y a pas de différence significative entre append et l'affectation directe par index.

De plus, j'ai dû modifier légèrement vos repères en raison des paniques du MOO.

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()
    }
}

Résultats:

➜  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
6
Vitaly Isaev