web-dev-qa-db-fra.com

Comment concaténer efficacement des chaînes dans Go?

Dans Go, string est un type primitif, ce qui signifie qu'il est en lecture seule et que toute manipulation de celui-ci créera une nouvelle chaîne. 

Donc, si je veux concaténer plusieurs fois des chaînes sans connaître la longueur de la chaîne résultante, quel est le meilleur moyen de le faire?

La manière naïve serait:

s := ""
for i := 0; i < 1000; i++ {
    s += getShortStringFromSomewhere()
}
return s

mais cela ne semble pas très efficace.

586

Note ajoutée en 2018

À partir de Go 1.10, il existe un type strings.Builder, veuillez consulter cette réponse pour plus de détails .

Réponse pré-201x

Le meilleur moyen consiste à utiliser le paquet bytes . Il a une Buffer type qui implémente io.Writer

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString("a")
    }

    fmt.Println(buffer.String())
}

Ceci le fait dans O(n) temps.

756
marketer

Le moyen le plus efficace de concaténer des chaînes consiste à utiliser la fonction intégrée copy . Dans mes tests, cette approche est environ 3 fois plus rapide que d'utiliser bytes.Buffer et beaucoup plus rapide (~ 12 000 x) que d'utiliser l'opérateur +. En outre, il utilise moins de mémoire.

J'ai créé un cas test pour le prouver et voici les résultats:

BenchmarkConcat  1000000    64497 ns/op   502018 B/op   0 allocs/op
BenchmarkBuffer  100000000  15.5  ns/op   2 B/op        0 allocs/op
BenchmarkCopy    500000000  5.39  ns/op   0 B/op        0 allocs/op

Ci-dessous le code pour tester:

package main

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkConcat(b *testing.B) {
    var str string
    for n := 0; n < b.N; n++ {
        str += "x"
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); str != s {
        b.Errorf("unexpected result; got=%s, want=%s", str, s)
    }
}

func BenchmarkBuffer(b *testing.B) {
    var buffer bytes.Buffer
    for n := 0; n < b.N; n++ {
        buffer.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); buffer.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s)
    }
}

func BenchmarkCopy(b *testing.B) {
    bs := make([]byte, b.N)
    bl := 0

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        bl += copy(bs[bl:], "x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); string(bs) != s {
        b.Errorf("unexpected result; got=%s, want=%s", string(bs), s)
    }
}

// Go 1.10
func BenchmarkStringBuilder(b *testing.B) {
    var strBuilder strings.Builder

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        strBuilder.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); strBuilder.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s)
    }
}
252
cd1

Le paquet de chaînes contient une fonction de bibliothèque appelée Join: http://golang.org/pkg/strings/#Join

Un regard sur le code de Join montre une approche similaire à la fonction d’ajout Kinopiko a écrit: https://golang.org/src/strings/strings.go#L420

Usage:

import (
    "fmt";
    "strings";
)

func main() {
    s := []string{"this", "is", "a", "joined", "string\n"};
    fmt.Printf(strings.Join(s, " "));
}

$ ./test.bin
this is a joined string
121
mbarkhau

Je viens de comparer la réponse la plus récente indiquée ci-dessus dans mon propre code (une promenade dans l'arborescence récursive) et l'opérateur de concat simple est en réalité plus rapide que la variable BufferString.

func (r *record) String() string {
    buffer := bytes.NewBufferString("");
    fmt.Fprint(buffer,"(",r.name,"[")
    for i := 0; i < len(r.subs); i++ {
        fmt.Fprint(buffer,"\t",r.subs[i])
    }
    fmt.Fprint(buffer,"]",r.size,")\n")
    return buffer.String()
}

Cela a pris 0,81 secondes, alors que le code suivant:

func (r *record) String() string {
    s := "(\"" + r.name + "\" ["
    for i := 0; i < len(r.subs); i++ {
        s += r.subs[i].String()
    }
    s += "] " + strconv.FormatInt(r.size,10) + ")\n"
    return s
} 

a seulement pris 0,61 secondes. Cela est probablement dû à la surcharge créée par la création de la nouvelle BufferString.

Mise à jour: J'ai également comparé la fonction join à une exécution en 0,54 seconde.

func (r *record) String() string {
    var parts []string
    parts = append(parts, "(\"", r.name, "\" [" )
    for i := 0; i < len(r.subs); i++ {
        parts = append(parts, r.subs[i].String())
    }
    parts = append(parts, strconv.FormatInt(r.size,10), ")\n")
    return strings.Join(parts,"")
}
38
JasonMc

Vous pouvez créer une grande tranche d'octets et y copier les octets des chaînes courtes à l'aide de tranches de chaînes. Il y a une fonction donnée dans "Effective Go":

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2);
        // Copy data (could use bytes.Copy()).
        for i, c := range slice {
            newSlice[i] = c
        }
        slice = newSlice;
    }
    slice = slice[0:l+len(data)];
    for i, c := range data {
        slice[l+i] = c
    }
    return slice;
}

Ensuite, lorsque les opérations sont terminées, utilisez string ( ) sur la grande tranche d'octets pour le convertir à nouveau en chaîne.

21
user181548

C'est la solution la plus rapide qui ne nécessite pasvous devez d'abord connaître ou calculer la taille totale de la mémoire tampon:

var data []byte
for i := 0; i < 1000; i++ {
    data = append(data, getShortStringFromSomewhere()...)
}
return string(data)

Par mon benchmark , il est 20% plus lent que la solution de copie (8.1ns par Append plutôt que 6.72ns) mais reste 55% plus rapide que d’utiliser bytes.Buffer.

20
rog

Mise à jour 2018-04-03

À compter de Go 1.10, il est recommandé que string.Builder remplace le bytes.Buffer. Vérifiez les notes de publication de la version 1.10

Un nouveau type Builder remplace d'octets.Buffer dans le cas d'utilisation d'accumulation de texte dans un résultat de chaîne. L'API du générateur est un sous-ensemble restreint d'octets.Buffer qui lui permet d'éviter en toute sécurité de créer une copie dupliquée des données au cours de la méthode String.

=============================================== ==========

Le code de référence de @ cd1 et d'autres réponses sont incorrects. b.N n'est pas censé être défini dans la fonction de référence. Il est défini de manière dynamique par l’outil de test pour déterminer si le temps d’exécution du test est stable.

Une fonction de référence doit exécuter le même test b.N fois et le test à l'intérieur de la boucle doit être identique pour chaque itération. Alors je le répare en ajoutant une boucle interne. J'ajoute également des points de repère pour d'autres solutions:

package main

import (
    "bytes"
    "strings"
    "testing"
)

const (
    sss = "xfoasneobfasieongasbg"
    cnt = 10000
)

var (
    bbb      = []byte(sss)
    expected = strings.Repeat(sss, cnt)
)

func BenchmarkCopyPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        bs := make([]byte, cnt*len(sss))
        bl := 0
        for i := 0; i < cnt; i++ {
            bl += copy(bs[bl:], sss)
        }
        result = string(bs)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppendPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, cnt*len(sss))
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        buf := bytes.NewBuffer(make([]byte, 0, cnt*len(sss)))
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkCopy(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Buffer
        for i := 0; i < cnt; i++ {
            off := len(data)
            if off+len(sss) > cap(data) {
                temp := make([]byte, 2*cap(data)+len(sss))
                copy(temp, data)
                data = temp
            }
            data = data[0 : off+len(sss)]
            copy(data[off:], sss)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppend(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64)
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWrite(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.Write(bbb)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWriteString(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkConcat(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < cnt; i++ {
            str += sss
        }
        result = str
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

Environnement: OS X 10.11.6, Intel Core i7 à 2,2 GHz

Résultats de test:

BenchmarkCopyPreAllocate-8         20000             84208 ns/op          425984 B/op          2 allocs/op
BenchmarkAppendPreAllocate-8       10000            102859 ns/op          425984 B/op          2 allocs/op
BenchmarkBufferPreAllocate-8       10000            166407 ns/op          426096 B/op          3 allocs/op
BenchmarkCopy-8                    10000            160923 ns/op          933152 B/op         13 allocs/op
BenchmarkAppend-8                  10000            175508 ns/op         1332096 B/op         24 allocs/op
BenchmarkBufferWrite-8             10000            239886 ns/op          933266 B/op         14 allocs/op
BenchmarkBufferWriteString-8       10000            236432 ns/op          933266 B/op         14 allocs/op
BenchmarkConcat-8                     10         105603419 ns/op        1086685168 B/op    10000 allocs/op

Conclusion:

  1. CopyPreAllocate est le moyen le plus rapide; AppendPreAllocate est assez proche du n ° 1, mais il est plus facile d'écrire le code.
  2. Concat a de très mauvaises performances à la fois en termes de vitesse et d'utilisation de la mémoire. Ne l'utilisez pas.
  3. Buffer#Write et Buffer#WriteString sont fondamentalement les mêmes en vitesse, contrairement à ce que @ Dani-Br a déclaré dans le commentaire. Considérant que string est en effet []byte dans Go, cela a du sens.
  4. bytes.Buffer utilise fondamentalement la même solution que Copy avec une tenue de livre supplémentaire et d’autres choses.
  5. Copy et Append utilisent une taille de bootstrap de 64, identique à celle de bytes.Buffer
  6. Append utilise plus de mémoire et d'allocation, je pense que cela est lié à l'algorithme de croissance utilisé. Cela ne fait pas croître la mémoire aussi vite que bytes.Buffer

Suggestion:

  1. Pour une tâche simple telle que ce que veut OP, j’utiliserais Append ou AppendPreAllocate. C'est assez rapide et facile à utiliser.
  2. Si vous devez lire et écrire le tampon en même temps, utilisez bien sûr bytes.Buffer. C'est ce pour quoi il est conçu.
18
PickBoy
package main

import (
  "fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}
16
harold ramos

Ma suggestion initiale était

s12 := fmt.Sprint(s1,s2)

Mais la réponse ci-dessus utilisant bytes.Buffer - WriteString () est le moyen le plus efficace.

Ma suggestion initiale utilise la réflexion et un commutateur de type. Voir (p *pp) doPrint et (p *pp) printArg
Il n’existe pas d’interface universelle Stringer () pour les types de base, comme je le pensais naïvement.

Au moins cependant, Sprint () en interne utilise un bytes.Buffer. Ainsi

`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`

est acceptable en termes d'allocation de mémoire.

=> La concaténation Sprint () peut être utilisée pour une sortie de débogage rapide.
=> Sinon, utilisez bytes.Buffer ... WriteString

12
Peter Buchmann

Ceci est la version actuelle de benchmark fournie par @ cd1 (Go 1.8, linux x86_64) avec les corrections de bugs mentionnés par @icza et @PickBoy.

Bytes.Buffer est seulement 7 fois plus rapide que la concaténation directe de chaînes via l'opérateur +.

package performance_test

import (
    "bytes"
    "fmt"
    "testing"
)

const (
    concatSteps = 100
)

func BenchmarkConcat(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < concatSteps; i++ {
            str += "x"
        }
    }
}

func BenchmarkBuffer(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var buffer bytes.Buffer
        for i := 0; i < concatSteps; i++ {
            buffer.WriteString("x")
        }
    }
}

Horaires:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op
2
Vitaly Isaev

goutils.JoinBetween

 func JoinBetween(in []string, separator string, startIndex, endIndex int) string {
    if in == nil {
        return ""
    }

    noOfItems := endIndex - startIndex

    if noOfItems <= 0 {
        return EMPTY
    }

    var builder strings.Builder

    for i := startIndex; i < endIndex; i++ {
        if i > startIndex {
            builder.WriteString(separator)
        }
        builder.WriteString(in[i])
    }
    return builder.String()
}
1
Xian Shu

Je le fais en utilisant ce qui suit: - 

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}
1
Krish Bhanushali
package main

import (
"fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    result := make([]byte, 0)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)

    fmt.Println(string(result))
}
0
rajni kant

Pour ceux qui viennent du monde Java où nous avons StringBuilder pour une concaténation efficace, il semble que la dernière version de go ait son équivalent et s'appelle Builder: https://github.com/golang/go/blob/master/ src/strings/builder.go

0
Joel