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

Dans Go, une ssortingng est un type primitif, ce qui signifie qu’elle est en lecture seule, et chaque manipulation crée une nouvelle chaîne.

Donc, si je veux concaténer des chaînes plusieurs fois sans connaître la longueur de la chaîne résultante, quelle est la meilleure façon 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.

Le meilleur moyen est d’utiliser le paquet d’ bytes . Il a un type de Buffer 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()) } 

Cela le fait en O (n) temps.

Note ajoutée en 2018

A partir de Go 1.10, il y a le type ssortingngs.Builder , qui le fait encore plus efficacement (pour les chaînes). L'exemple donné ici est succinct et facile à copier / adapter.

Ceci est analogue à la classe SsortingngBuilder en Java.

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

J’ai créé un scénario de 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" "ssortingngs" "testing" ) func BenchmarkConcat(b *testing.B) { var str ssortingng for n := 0; n < bN; n++ { str += "x" } b.StopTimer() if s := strings.Repeat("x", bN); 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 < bN; n++ { buffer.WriteString("x") } b.StopTimer() if s := strings.Repeat("x", bN); buffer.String() != s { b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s) } } func BenchmarkCopy(b *testing.B) { bs := make([]byte, bN) bl := 0 b.ResetTimer() for n := 0; n < bN; n++ { bl += copy(bs[bl:], "x") } b.StopTimer() if s := strings.Repeat("x", bN); 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 < bN; n++ { strBuilder.WriteString("x") } b.StopTimer() if s := strings.Repeat("x", bN); strBuilder.String() != s { b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s) } } 

Il y a une fonction de bibliothèque dans le paquetage de chaînes appelé Join : http://golang.org/pkg/ssortingngs/#Join

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

Usage:

 import ( "fmt"; "ssortingngs"; ) func main() { s := []ssortingng{"this", "is", "a", "joined", "ssortingng\n"}; fmt.Printf(ssortingngs.Join(s, " ")); } $ ./test.bin this is a joined ssortingng 

En commençant par Go 1.10, il y a un ssortingngs.Builder , ici .

Un générateur est utilisé pour créer efficacement une chaîne à l’aide des méthodes Write. Cela minimise la copie de mémoire. La valeur zéro est prête à être utilisée.


Usage:

C’est presque la même chose avec les bytes.Buffer .

 package main import ( "ssortingngs" "fmt" ) func main() { var str ssortingngs.Builder for i := 0; i < 1000; i++ { str.WriteString("a") } fmt.Println(str.String()) } 

Méthodes et interfaces SsortingngBuilder sockets en charge:

Ses méthodes sont implémentées en gardant à l'esprit les interfaces existantes afin que vous puissiez passer facilement au nouveau Générateur dans votre code.

  • Grow (int) -> bytes.Buffer # Grow
  • Len () int -> octets.Buffer # Len
  • Réinitialiser () -> octets.Buffer # Réinitialiser
  • Ssortingng () ssortingng -> fmt.Ssortingnger
  • Ecrire ([] octet) (int, erreur) -> io.Writer
  • Erreur WriteByte (byte) -> io.ByteWriter
  • WriteRune (rune) (int, erreur) -> bufio.Writer # WriteRune - bytes.Buffer # WriteRune
  • WriteSsortingng (ssortingng) (int, erreur) -> io.ssortingngWriter

Valeur zéro utilisation:

 var sb ssortingngs.Builder 

Différences par rapport aux octets.Buffer:

  • C'est immuable et il ne peut que croître ou se réinitialiser.

  • Dans bytes.Buffer les octets sous-jacents peuvent s'échapper comme ceci: (*Buffer).Bytes() ; ssortingngs.Builder empêche ce problème.

  • Il a également un mécanisme copyCheck à l'intérieur qui empêche de le copier accidentellement ( func (b *Builder) copyCheck() { ... } ).


Découvrez son code source ici .

J’ai juste comparé la première réponse affichée ci-dessus dans mon propre code (un parcours récursif) et l’opérateur concat simple est en fait plus rapide que le BufferSsortingng.

 func (r *record) Ssortingng() ssortingng { buffer := bytes.NewBufferSsortingng(""); 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.81s, alors que le code suivant:

 func (r *record) Ssortingng() ssortingng { 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 } 

seulement pris 0,61s. Cela est probablement dû à la création des nouveaux BufferSsortingngs.

Mise à jour: j'ai également évalué la fonction de jointure et elle a fonctionné dans 0.54s

 func (r *record) Ssortingng() ssortingng { var parts []ssortingng 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,"") } 

C’est la solution la plus rapide qui ne nécessite pas de connaître ou de calculer la taille de la mémoire tampon en premier:

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

D'après mon test , il est 20% plus lent que la solution de copie (8.1ns par append plutôt que 6.72ns) mais toujours 55% plus rapide que l'utilisation d'octets.Buffer.

Vous pouvez créer une grande tranche d’octets et y copier les octets des chaînes courtes à l’aide de tranches de chaîne. 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 ssortingng ( ) sur la grande tranche d’octets pour la convertir à nouveau en chaîne.

Mise à jour 2018-04-03

À partir de Go 1.10, ssortingng.Builder est recommandé pour remplacer bytes.Buffer . Vérifier les notes de version 1.10

Un nouveau type Builder remplace les octets.Buffer pour le cas d’utilisation de l’accumulation de texte dans un résultat de chaîne. L’API du générateur est un sous-ensemble restreint de bytes.Buffer qui lui permet d’éviter en toute sécurité d’effectuer une copie des données pendant la méthode Ssortingng.

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

Le code de référence de @ cd1 et d’autres réponses sont erronés. bN n’est pas censé être défini dans la fonction de référence. Il est défini par l’outil de test dynamic pour déterminer si le temps d’exécution du test est stable.

Une fonction de test devrait exécuter le même test bN fois et le test dans la boucle devrait être le même pour chaque itération. Donc, je le répare en ajoutant une boucle interne. J’ajoute également des repères pour d’autres solutions:

 package main import ( "bytes" "ssortingngs" "testing" ) const ( sss = "xfoasneobfasieongasbg" cnt = 10000 ) var ( bbb = []byte(sss) expected = ssortingngs.Repeat(sss, cnt) ) func BenchmarkCopyPreAllocate(b *testing.B) { var result ssortingng for n := 0; n < bN; 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 < bN; 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 < bN; 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 < bN; 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 = ssortingng(data) } b.StopTimer() if result != expected { b.Errorf("unexpected result; got=%s, want=%s", ssortingng(result), expected) } } func BenchmarkAppend(b *testing.B) { var result ssortingng for n := 0; n < bN; 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 < bN; 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 < bN; 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 < bN; 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) } } 

L'environnement est 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 BenchmarkBufferWriteSsortingng-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 de No.1, mais il est plus facile d'écrire le code.
  2. Concat a de très mauvaises performances tant pour la vitesse que pour l’utilisation de la mémoire. Ne l'utilise pas
  3. Buffer#Write et Buffer#WriteSsortingng sont fondamentalement les mêmes en vitesse, contrairement à ce que @ Dani-Br a dit dans le commentaire. Considérant que ssortingng est bien []byte dans Go, cela a du sens.
  4. bytes.Buffer utilise essentiellement la même solution que Copy avec une tenue de livres supplémentaire et d'autres éléments.
  5. Copy and Append utilise une taille de bootstrap de 64, identique à celle de bytes.Buffer
  6. Append utilise plus de mémoire et d'allocations, je pense que c'est lié à l'algorithme de croissance qu'il utilise. Cela ne fait pas croître la mémoire aussi vite que bytes.Buffer

Suggestion:

  1. Pour des tâches simples telles que ce que veut OP, j'utiliserais Append ou AppendPreAllocate . C'est assez rapide et facile à utiliser.
  2. Si besoin de lire et d'écrire le tampon en même temps, utilisez bytes.Buffer bien sûr. C'est pour cela qu'il est conçu.
 package main import ( "fmt" ) func main() { var str1 = "ssortingng1" var str2 = "ssortingng2" out := fmt.Sprintf("%s %s ",str1, str2) fmt.Println(out) } 

Ma suggestion initiale était

 s12 := fmt.Sprint(s1,s2) 

Mais ci-dessus, utilisez bytes.Buffer – WriteSsortingng () est le moyen le plus efficace.

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

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

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

est acceptable en termes d’allocations de mémoire.

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

Expansion sur la réponse de cd1: Vous pouvez utiliser append () au lieu de copy (). append () crée des provisions de plus en plus importantes, ce qui coûte un peu plus de mémoire, mais permet de gagner du temps. J’ai ajouté deux autres benchmarks au sumt de la vôtre. Exécuter localement avec

 go test -bench=. -benchtime=100ms 

Sur mon thinkpad T400s, cela donne:

 BenchmarkAppendEmpty 50000000 5.0 ns/op BenchmarkAppendPrealloc 50000000 3.5 ns/op BenchmarkCopy 20000000 10.2 ns/op 

Ceci est la version actuelle de benchmark fournie par @ cd1 ( Go 1.8 , linux x86_64 ) avec les correctifs 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 < bN; n++ { var str string for i := 0; i < concatSteps; i++ { str += "x" } } } func BenchmarkBuffer(b *testing.B) { for n := 0; n < bN; n++ { var buffer bytes.Buffer for i := 0; i < concatSteps; i++ { buffer.WriteString("x") } } } 

Timings:

 BenchmarkConcat-4 300000 6869 ns/op BenchmarkBuffer-4 1000000 1186 ns/op 

Pour ceux qui viennent du monde Java où nous avons SsortingngBuilder pour une concaténation de chaînes efficace, il semble que la dernière version de go a son équivalent et qu’elle s’appelle Builder : https://github.com/golang/go/blob/master/src/ cordes / constructeur.go

Je le fais en utilisant ce qui suit: –

 package main import ( "fmt" "ssortingngs" ) func main (){ concatenation:= ssortingngs.Join([]ssortingng{"a","b","c"},"") //where second parameter is a separator. fmt.Println(concatenation) //abc } 
 package main import ( "fmt" ) func main() { var str1 = "ssortingng1" var str2 = "ssortingng2" 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(ssortingng(result)) } 

Jetez un coup d’oeil à la bibliothèque strconv de golang donnant access à plusieurs fonctions AppendXX, ce qui nous permet de concaténer des chaînes avec des chaînes et d’autres types de données.

 s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2)) 

ssortingngs.Join() du paquet “ssortingngs”

Si vous avez une incompatibilité de type (comme si vous essayez de joindre un int et une chaîne), vous faites RANDOMTYPE (ce que vous voulez changer)

EX:

 package main import "ssortingngs" var intEX = 0 var ssortingngEX = "hello all you " var ssortingngEX2 = " people in here" func main() { ssortingngs.Join(ssortingngEX, ssortingng(intEX), ssortingngEX2) }