Comment concaténer efficacement les chaînes
-
21-09-2019 - |
Question
Go, un string
est un type primitif, ce qui signifie qu'il est en lecture seule, et chaque manipulation de celui-ci va créer une nouvelle chaîne.
Donc, si je veux concaténer des chaînes à plusieurs reprises sans connaître la longueur de la chaîne résultante, quelle est la meilleure façon de le faire?
La façon naïve serait:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
mais cela ne semble pas très efficace.
La solution
Note ajoutée en 2018
A partir de 1.10 Go il y a un type de strings.Builder
, s'il vous plaît prendre un coup d'oeil à cette réponse pour plus de détails .
Réponse pré-201x
La meilleure façon est d'utiliser le package bytes
. Il a un type 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())
}
Ce qu'il fait en O (n).
Autres conseils
La façon la plus efficace de concaténer des chaînes utilise la fonction copy
. Dans mes tests, cette approche est ~ 3 fois plus rapide que d'utiliser bytes.Buffer
et beaucoup plus rapide (~ 12 000 x) à l'aide de la +
de l'opérateur. En outre, il utilise moins de mémoire.
J'ai créé un test pour 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
est le code ci-dessous pour le test:
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)
}
}
À partir de 1.10 Go il y a un strings.Builder
, .
Un Builder est utilisé pour construire efficacement une chaîne en utilisant des méthodes d'écriture. Il minimise la copie de la mémoire. La valeur zéro est prêt à l'emploi.
Utilisation:
Il est presque la même chose avec bytes.Buffer
.
package main
import (
"strings"
"fmt"
)
func main() {
var str strings.Builder
for i := 0; i < 1000; i++ {
str.WriteString("a")
}
fmt.Println(str.String())
}
Remarque: Ne pas copier une valeur StringBuilder comme il met en cache les données sous-jacentes. Si vous souhaitez partager une valeur StringBuilder, utiliser des pointeurs.
méthodes de StringBuilder et des interfaces, il prend en charge:
Ses méthodes sont mises en œuvre avec les interfaces existantes à l'esprit de sorte que vous pouvez passer à la nouvelle Builder facilement dans votre code.
- Cultivez (int) -> bytes.Buffer # grandir
- Len () int -> bytes.Buffer # Len
- Reset () -> bytes.Buffer # Réinitialiser
- string String () -> fmt.Stringer
- Ecrire ([] octets) (int, erreur) -> io.Writer
- WriteByte (octet) erreur -> io.ByteWriter
- WriteRune (rune) (int, erreur) -> bufio.Writer # WriteRune - bytes.Buffer # WriteRune
- WriteString (string) (int, erreur) -> io.stringWriter
Utilisation de la valeur zéro:
var buf strings.Builder
Les différences de bytes.Buffer:
-
Il ne peut se développer ou réinitialiser.
-
Dans
bytes.Buffer
, on peut accéder aux octets sous-jacents comme celui-ci:(*Buffer).Bytes()
;strings.Builder
empêche ce problème. Parfois, ce n'est pas un problème si et souhaité à la place (par exemple, pour jeter un oeil comportement lorsque les octets sont transmis à unio.Reader
etc). -
Il a également un mécanisme intégré de copyCheck qui empêche la copie accidentially il (
func (b *Builder) copyCheck() { ... }
).
Consultez son code source ici .
Il y a une fonction de bibliothèque dans le paquet de cordes appelé Join
:
http://golang.org/pkg/strings/#Join
Un regard sur le code de Join
montre une approche similaire à la fonction Append Kinopiko a écrit: https : //golang.org/src/strings/strings.go#L420
Utilisation:
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
Je viens benchmarkée la réponse ci-dessus dans le dessus affiché mon propre code (un parcours d'arbre récursif) et le simple opérateur concat est plus rapide que le 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()
}
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. Ceci est probablement dû à la surcharge de la création du nouveau BufferString
.
Mise à jour:. J'ai aussi benchmarkée la fonction join
et il a couru en 0,54 secondes
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,"")
}
Vous pouvez créer une grande tranche d'octets et copier les octets des chaînes courtes en tranches à l'aide de cordes. Il y a une fonction donnée dans « efficace 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 en une chaîne à nouveau.
Ceci est la solution la plus rapide qui ne nécessite pas vous savez ou calculer la taille globale du premier tampon:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
Par mon , il est 20% plus lent que la solution de copie (8.1ns par ajouter plutôt que 6.72ns), mais encore 55% plus rapide que l'utilisation bytes.Buffer.
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
Note ajoutée en 2018
A partir de 1.10 Go il y a un type de strings.Builder
, s'il vous plaît prendre un coup d'oeil à cette réponse pour plus de détails .
Réponse pré-201x
Le code de référence de @ CD1 et d'autres réponses sont fausses. b.N
n'est pas censé être mis en fonction de référence. Il est fixé par l'outil de test de passer de façon dynamique afin de 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 de temps b.N
et le test à l'intérieur de la boucle doit être la même pour chaque itération. Donc je résoudre ce problème en ajoutant une boucle intérieure. Je voudrais également ajouter des repères 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)
}
}
L'environnement est OS X 10.11.6, 2,2 GHz Intel Core i7
Les résultats du 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:
-
CopyPreAllocate
est le moyen le plus rapide;AppendPreAllocate
est assez proche de n ° 1, mais il est plus facile d'écrire le code. -
Concat
a vraiment mauvaise performance à la fois pour la vitesse et l'utilisation de la mémoire. Ne l'utilisez pas. -
Buffer#Write
etBuffer#WriteString
sont fondamentalement les mêmes vitesse, contrairement à ce que @ Dani-Br dit dans le commentaire. Considérantstring
est en effet[]byte
dans Go, il est logique. - bytes.Buffer utiliser essentiellement la même solution que
Copy
avec la tenue de livres supplémentaires et d'autres choses. -
Copy
etAppend
utilisent une taille bootstrap de 64, le même que bytes.Buffer -
Append
utiliser plus de mémoire et allocs, je pense qu'il est lié à l'algorithme de culture qu'il utilise. Il ne grandit pas aussi vite que la mémoire bytes.Buffer
Suggestion:
- Pour la tâche simple, comme ce que l'OP veut, j'utiliser
Append
ouAppendPreAllocate
. Il est assez rapide et facile à utiliser. - Si besoin de lire et d'écrire le tampon en même temps, utilisez
bytes.Buffer
bien sûr. C'est ce qu'il est conçu pour.
Ma suggestion originale était
s12 := fmt.Sprint(s1,s2)
Mais réponse ci-dessus en utilisant bytes.Buffer - WriteString () est le plus efficace manière.
Ma suggestion initiale utilise la réflexion et un commutateur de type. Voir (p *pp) doPrint
et (p *pp) printArg
Il n'y a pas d'interface Stringer () universelle pour les types de base, comme je l'avais pensé naïvement.
Au moins cependant, Sprint () interne utilise un bytes.Buffer. Ainsi,
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
est acceptable en termes d'allocations de mémoire.
=> Sprint () concaténation peut être utilisé pour la sortie de débogage rapide.
=> Sinon, utilisez bytes.Buffer ... WriteString
L'expansion sur la réponse CD1: Vous pouvez utiliser append () au lieu de copier (). append () rend toujours plus grandes dispositions à l'avance, ce qui coûte un peu plus de mémoire, mais un gain de temps. J'ai ajouté deux autres points de repère en haut de la vôtre. Exécuter localement avec
go test -bench=. -benchtime=100ms
Sur mes T400s il donne thinkpad:
BenchmarkAppendEmpty 50000000 5.0 ns/op
BenchmarkAppendPrealloc 50000000 3.5 ns/op
BenchmarkCopy 20000000 10.2 ns/op
est une version réelle de référence fournie par @ CD1 (Go 1.8
, linux x86_64
) avec les corrections de bugs mentionnés par @icza et @PickBoy.
Bytes.Buffer
est seulement temps de 7
plus rapide que la concaténation de chaîne directe par l'opérateur de +
.
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")
}
}
}
synchronisations:
BenchmarkConcat-4 300000 6869 ns/op
BenchmarkBuffer-4 1000000 1186 ns/op
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()
}
Je le fais en utilisant les éléments suivants: -
package main
import (
"fmt"
"strings"
)
func main (){
concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.
fmt.Println(concatenation) //abc
}
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))
}
Résultat de référence avec les statistiques d'allocation de mémoire. vérifier le code de référence à github .
utilisation strings.Builder pour optimiser les performances.
go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/hechen0/goexp/exps
BenchmarkConcat-8 1000000 60213 ns/op 503992 B/op 1 allocs/op
BenchmarkBuffer-8 100000000 11.3 ns/op 2 B/op 0 allocs/op
BenchmarkCopy-8 300000000 4.76 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 1000000000 4.14 ns/op 6 B/op 0 allocs/op
PASS
ok github.com/hechen0/goexp/exps 70.071s
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))
strings.Join()
du paquet "strings"
Si vous avez une incompatibilité de type (comme si vous essayez de joindre un entier et une chaîne), vous ne RANDOMTYPE (chose que vous voulez changer)
EX:
package main
import (
"fmt"
"strings"
)
var intEX = 0
var stringEX = "hello all you "
var stringEX2 = "people in here"
func main() {
s := []string{stringEX, stringEX2}
fmt.Println(strings.Join(s, ""))
}
Sortie:
hello all you people in here