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.
À partir de Go 1.10, il existe un type strings.Builder
, veuillez consulter cette réponse pour plus de détails .
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.
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)
}
}
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
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,"")
}
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.
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.
À 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:
CopyPreAllocate
est le moyen le plus rapide; AppendPreAllocate
est assez proche du n ° 1, mais il est plus facile d'écrire le code.Concat
a de très mauvaises performances à la fois en termes de vitesse et d'utilisation de la mémoire. Ne l'utilisez pas.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.Copy
avec une tenue de livre supplémentaire et d’autres choses.Copy
et Append
utilisent une taille de bootstrap de 64, identique à celle de bytes.BufferAppend
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.BufferSuggestion:
Append
ou AppendPreAllocate
. C'est assez rapide et facile à utiliser.bytes.Buffer
. C'est ce pour quoi il est conçu.package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
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
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
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 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
}
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))
}
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