Cómo concatenar cadenas de manera eficiente
-
21-09-2019 - |
Pregunta
En Go, un 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.
Solución
Nota añadida en 2018
En Ir 1.10 hay un tipo strings.Builder
, favor tome una mirada a esta respuesta para más detalles .
Respuesta Pre-201x
La mejor forma de hacerlo es utilizar el paquete de href="http://golang.org/pkg/bytes/" rel="noreferrer"> bytes
. Tiene una Buffer
tipo 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).
Otros consejos
La forma más eficiente para concatenar cadenas está utilizando la función incorporada copy
. En mis pruebas, este enfoque es ~ 3 veces más rápido que el uso de bytes.Buffer
y mucho mucho más rápido (~ 12.000 x) que el uso de la +
operador. Además, se usa menos memoria.
He creado un caso de prueba para probar esto y aquí están las 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)
}
}
A partir de Go 1.10 hay una strings.Builder
, aquí.
Un constructor se utiliza para construir eficientemente una cadena usando métodos de escritura.Minimiza la copia de memoria.El valor cero está listo para usar.
Uso:
Es casi lo mismo con 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())
}
Nota: No copie un valor de StringBuilder ya que almacena en caché los datos subyacentes.Si desea compartir un valor de StringBuilder, utilice punteros.
Métodos e interfaces de StringBuilder que admite:
Sus métodos se están implementando teniendo en cuenta las interfaces existentes para que pueda cambiar fácilmente al nuevo Builder en su código.
- Crecer(int) -> bytes.Buffer#Crecer
- Len()int -> bytes.Buffer#Len
- Reiniciar() -> bytes.Buffer#Restablecer
- cadena() cadena -> fmt.Stringer
- Escribir([]byte) (int, error) -> io.Escritor
- Error de escritura de bytes (byte) -> io.ByteWriter
- WriteRune(runa) (int, error) -> bufio.Writer#WriteRune - bytes.Buffer#WriteRune
- WriteString(cadena) (int, error) -> io.stringWriter
Uso de valor cero:
var buf strings.Builder
Diferencias con bytes.Buffer:
Sólo puede crecer o restablecerse.
En
bytes.Buffer
, se puede acceder a los bytes subyacentes de esta manera:(*Buffer).Bytes()
;strings.Builder
previene este problema.A veces, esto no es un problema y, en cambio, es lo que se desea (por ejemplo, para observar el comportamiento cuando los bytes se pasan a unio.Reader
etc).También tiene un mecanismo copyCheck incorporado que evita copiarlo accidentalmente (
func (b *Builder) copyCheck() { ... }
).
Mira su código fuente aquí.
Hay una función de biblioteca en el paquete de cuerdas llamado Join
:
http://golang.org/pkg/strings/#Join
Un vistazo al código de Join
muestra un enfoque similar para anexar la función 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
Yo sólo como punto de referencia la respuesta más común fue anunciado anteriormente en mi propio código (un paseo árbol recursivo) y el simple operador concat es más rápido que el 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 es probablemente debido a la sobrecarga de crear el nuevo BufferString
.
Actualización:. también como punto de referencia la función join
y corrió en los 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,"")
}
Se puede crear una gran porción de bytes y copiar los bytes de las cadenas cortas en ella usando rebanadas de cuerda. Hay una función dada en "Go eficaz":
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;
}
A continuación, cuando se terminan las operaciones, utilice string ( )
sobre la rebanada grande de bytes para convertirlo en una cadena de nuevo.
Esta es la solución más rápida que no requiere a conocer o calcular el tamaño total de memoria intermedia primero:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
Por mi referencia , es 20% más lenta que la solución de copia (por 8.1ns en lugar de añadir 6.72ns), pero aún 55% más rápido que usar bytes.Buffer.
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
Nota añadida en 2018
En Ir 1.10 hay un tipo strings.Builder
, favor tome una mirada a esta respuesta para más detalles .
Respuesta Pre-201x
El código de prueba de @ CD1 y otras respuestas son incorrectas. No se supone b.N
que se fijará en función de referencia. Se fija por la herramienta de prueba de marcha de forma dinámica para determinar si el tiempo de ejecución de la prueba es estable.
Una función de punto de referencia debería ejecutar los mismos tiempos b.N
prueba y la prueba dentro del bucle debe ser el mismo para cada iteración. Así lo fijo mediante la adición de 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 medio ambiente 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 de No.1, pero es más fácil de escribir el código. -
Concat
tiene muy mal rendimiento tanto para la velocidad y el uso de memoria. No lo utilice. -
Buffer#Write
yBuffer#WriteString
son básicamente los mismos en la velocidad, al contrario de lo @ Dani-Br dicho en el comentario. Teniendo en cuentastring
es de hecho[]byte
en Go, tiene sentido. - bytes.Buffer utilizan básicamente la misma solución que
Copy
con libro de mantenimiento extra y otras cosas. -
Copy
yAppend
utilizan un tamaño de bootstrap de 64, lo mismo que bytes.Buffer -
Append
utilizar más memoria y allocs, creo que está relacionado con el algoritmo de cultivo que utilice. No está creciendo la memoria tan rápido como bytes.Buffer
Sugerencia:
- Para tarea sencilla, como lo PO quiere, me gustaría utilizar
Append
oAppendPreAllocate
. Es lo suficientemente rápido y fácil de usar. - Si la necesidad de leer y escribir la memoria intermedia al mismo tiempo, utilizar
bytes.Buffer
por supuesto. Eso es lo que está diseñado para.
Mi sugerencia original
s12 := fmt.Sprint(s1,s2)
Pero por encima de respuesta utilizando bytes.Buffer - WriteString () es el más eficiente camino.
Mi sugerencia inicial utiliza la reflexión y un interruptor tipo. Ver (p *pp) doPrint
y (p *pp) printArg
No hay una interfaz universal Stringer () para los tipos básicos, como había pensado ingenuamente.
Al menos, sin embargo, Sprint () internamente utiliza un bytes.Buffer. Por lo tanto
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
es aceptable en términos de las asignaciones de memoria.
=> Sprint () concatenación se puede utilizar para la salida de depuración rápida.
=> De lo contrario usar bytes.Buffer ... WriteString
Ampliando la respuesta de CD1: Es posible usar append () en lugar de la copia (). append () hace que cada vez más grandes disposiciones anticipadas, con un costo un poco más de memoria, pero el ahorro de tiempo. Añadí dos puntos de referencia más en la parte superior de los suyos. Ejecutar localmente con
go test -bench=. -benchtime=100ms
En mis ThinkPad T400s se produce:
BenchmarkAppendEmpty 50000000 5.0 ns/op
BenchmarkAppendPrealloc 50000000 3.5 ns/op
BenchmarkCopy 20000000 10.2 ns/op
Esta es la versión actual del punto de referencia proporcionada por @ CD1 (Go 1.8
, linux x86_64
) con las correcciones de los errores mencionados por @icza y @PickBoy.
Bytes.Buffer
es únicas veces 7
más rápido que la concatenación de cadenas 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")
}
}
}
Los tiempos:
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()
}
Lo hago utilizando la 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
}
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))
}
Resultado de referencia con las estadísticas de asignación de memoria. comprobar el código de referencia en github .
strings.Builder uso 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
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))
strings.Join()
del paquete de "cadenas"
Si usted tiene una coincidencia de tipos (como si usted está tratando de unirse a un int y una cadena), hacer RANDOMTYPE (cosa que desea cambiar)
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, ""))
}
Salida:
hello all you people in here