string - caracteres - ¿Cómo concatenar eficientemente cadenas en Go?
concatenar caracteres en java (20)
Actualización 2018-04-03
A partir de la here 1.10, se recomienda que bytes.Buffer
sustituya a bytes.Buffer
. Compruebe las notas de la versión 1.10
Un nuevo tipo Builder es un reemplazo de bytes.Buffer para el caso de uso de acumulación de texto en un resultado de cadena. La API del Generador es un subconjunto restringido de bytes. Es posible que evite realizar copias duplicadas de los datos durante el método String.
================================================== ==========
El código de referencia de @ cd1 y otras respuestas son incorrectos. Se supone que bN
no se establece en la función de referencia. Lo establece dinámicamente la herramienta de prueba de ir para determinar si el tiempo de ejecución de la prueba es estable.
Una función de referencia debe ejecutar la misma prueba bN
veces y la prueba dentro del bucle debe ser la misma para cada iteración. Así que lo arreglo agregando un bucle interno. También agrego puntos de referencia para algunas otras soluciones:
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)
}
}
El entorno es OS X 10.11.6, 2.2 GHz Intel Core i7
Resultados de la prueba:
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
Conclusión:
-
CopyPreAllocate
es la forma más rápida;AppendPreAllocate
está bastante cerca del número 1, pero es más fácil escribir el código. -
Concat
tiene un rendimiento realmente malo tanto para la velocidad como para el uso de la memoria. No lo uses -
Buffer#Write
yBuffer#WriteString
son básicamente lo mismo en velocidad, al contrario de lo que @ Dani-Br dijo en el comentario. Teniendo en cuenta que lastring
es de hecho[]byte
en Go, tiene sentido. - bytes.Buffer básicamente usa la misma solución que
Copy
con contabilidad adicional y otras cosas. -
Copy
yAppend
utilizan un tamaño de bootstrap de 64, lo mismo que bytes.Buffer -
Append
usar más memoria y asignaciones, creo que está relacionado con el algoritmo de crecimiento que utiliza. No está creciendo la memoria tan rápido como los bytes.
Sugerencia:
- Para tareas sencillas como lo que quiere OP, usaría
Append
oAppendPreAllocate
. Es lo suficientemente rápido y fácil de usar. - Si necesita leer y escribir el búfer al mismo tiempo, use
bytes.Buffer
por supuesto. Para eso está diseñado.
En Go, una string
es un tipo primitivo, lo que significa que es de solo lectura y cada manipulación creará una nueva cadena.
Entonces, si quiero concatenar cadenas muchas veces sin saber la longitud de la cadena resultante, ¿cuál es la mejor manera de hacerlo?
La forma ingenua sería:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
Pero eso no parece muy eficiente.
A partir de Go 1.10 hay un strings.Builder
, here .
Un generador se utiliza para construir de forma eficiente una cadena utilizando métodos de escritura. Minimiza la copia de memoria. El valor cero está listo para usar.
Uso:
Es casi lo mismo con los bytes.Buffer
. 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())
}
Métodos de StringBuilder e interfaces que soporta:
Sus métodos se implementan teniendo en cuenta las interfaces existentes para que pueda cambiar al nuevo Generador fácilmente en su código.
- Crecer (int) -> bytes.Buffer#Grow
- Len () int -> bytes.Buffer#Len
- Restablecer () -> bytes.Buffer#Reset
- Cadena () cadena -> fmt.Stringer
- Escritura ([] byte) (int, error) -> io.Writer
- Error de WriteByte (byte) -> io.ByteWriter
- WriteRune (runa) (int, error) -> bufio.Writer#WriteRune - bytes.Buffer#WriteRune
- WriteString (cadena) (int, error) -> io.stringWriter
Uso de valor cero:
var sb strings.Builder
Diferencias de bytes. Buffer:
Solo puede crecer o reiniciar.
En bytes. Los bytes subyacentes del
bytes.Buffer
pueden escapar de esta forma:(*Buffer).Bytes()
;strings.Builder
previene este problema.También tiene un mecanismo copyCheck en el interior que evita que se copie accidentalmente (
func (b *Builder) copyCheck() { ... }
).
Echa un vistazo a su código fuente here .
Acabo de comparar la respuesta principal publicada anteriormente en mi propio código (una caminata de árbol recursiva) y el operador simple de concat es en realidad más rápido que 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()
}
Esto tomó 0.81 segundos, mientras que el siguiente código:
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
}
Sólo tomó 0,61 segundos. Esto se debe probablemente a la sobrecarga de crear el nuevo BufferString
.
Actualización: También evalué la función de join
y se ejecutó en 0.54 segundos.
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,"")
}
Ampliando la respuesta de cd1: Puede usar append () en lugar de copy (). append () hace provisiones anticipadas cada vez más grandes, costando un poco más de memoria, pero ahorrando tiempo. Añadí dos puntos de referencia más en la parte superior de la suya. Ejecutar localmente con
go test -bench=. -benchtime=100ms
En mi thinkpad T400s produce:
BenchmarkAppendEmpty 50000000 5.0 ns/op
BenchmarkAppendPrealloc 50000000 3.5 ns/op
BenchmarkCopy 20000000 10.2 ns/op
Eche un vistazo a la biblioteca strconv de strconv que da acceso a varias funciones AppendXX, lo que nos permite concatenar cadenas con cadenas y otros tipos de datos.
Esta es la solución más rápida que no requiere que usted sepa o calcule primero el tamaño del búfer general:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
Según mi benchmark , es un 20% más lento que la solución de copia (8.1ns por apéndice en lugar de 6.72ns) pero aún así es un 55% más rápido que usar bytes.
Esta es la versión real de benchmark proporcionada por @ cd1 ( Go 1.8
, linux x86_64
) con las correcciones de errores mencionados por @icza y @PickBoy.
Bytes.Buffer
es solo 7
veces más rápido que la concatenación de cadena directa a través del operador +
.
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")
}
}
}
Tiempos:
BenchmarkConcat-4 300000 6869 ns/op
BenchmarkBuffer-4 1000000 1186 ns/op
Hay una función de biblioteca en el paquete de cadenas llamada Join
: http://golang.org/pkg/strings/#Join
Una mirada al código de Unión muestra un enfoque similar a la función de anexión que Kinopiko escribió: https://golang.org/src/strings/strings.go#L420
Uso:
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
La forma más eficiente de concatenar cadenas es mediante la función integrada copy
. En mis pruebas, ese enfoque es ~ 3x más rápido que usar Buffer y mucho más rápido (~ 12,000x) que usar el operador +
. Además, utiliza menos memoria.
He creado un caso de prueba para demostrar esto y aquí están los resultados:
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
A continuación se muestra el código para la prueba:
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)
}
}
La mejor manera es usar el paquete de bytes
. Tiene un tipo de Buffer
que implementa 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())
}
Esto lo hace en tiempo O (n).
Nota añadida en 2018
Desde Go 1.10 está el tipo strings.Builder , que logra esto aún más eficientemente (para cadenas). El ejemplo dado allí es sucinto y fácil de copiar / adaptar.
Esto es análogo a la clase StringBuilder en Java.
Lo hago utilizando lo siguiente:
package main
import (
"fmt"
"strings"
)
func main (){
concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.
fmt.Println(concatenation) //abc
}
Mi sugerencia original fue
s12 := fmt.Sprint(s1,s2)
Pero la respuesta anterior utilizando bytes.Buffer - WriteString () es la forma más eficiente.
Mi sugerencia inicial utiliza la reflexión y un tipo de cambio. Consulte (p *pp) doPrint
y (p *pp) printArg
No hay una interfaz universal de Stringer () para los tipos básicos, como había pensado ingenuamente.
Sin embargo, al menos, Sprint () utiliza internamente un byte. Buffer. Así
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
Es aceptable en términos de asignaciones de memoria.
=> La concatenación Sprint () se puede usar para una salida de depuración rápida.
=> De lo contrario, use bytes.Buffer ... WriteString
Para aquellos que vienen del mundo Java donde tenemos StringBuilder
para una concatenación de cadenas eficiente, parece que la última versión de go tiene su equivalente y se llama Builder
: https://github.com/golang/go/blob/master/src/strings/builder.go
Podría crear una gran porción de bytes y copiar los bytes de las cadenas cortas en ella usando porciones de cadena. Hay una función dada en "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;
}
Luego, cuando las operaciones hayan finalizado, use string ( )
en la gran porción de bytes para convertirla nuevamente en una cadena.
Resultado de referencia con estadísticas de asignación de memoria. Verifique el código de referencia en github .
utilizar cadenas.creador para optimizar el rendimiento.
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
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()
}
strings.Join()
del paquete "strings"
Si tiene una falta de coincidencia de tipo (como si está intentando unir un int y una cadena), realice RANDOMTYPE (cosa que desea cambiar)
EX:
package main
import "strings"
var intEX = 0
var stringEX = "hello all you "
var stringEX2 = " people in here"
func main() {
strings.Join(stringEX, string(intEX), stringEX2)
}
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
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))
}
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))