Как эффективно объединить строки
В Go string
является примитивным типом, что означает, что она доступна только для чтения, и каждая манипуляция с ней создаст новую строку.
Так что, если я хочу многократно объединять строки, не зная длины полученной строки, каков наилучший способ сделать это?
Наивным способом будет:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
но это не кажется очень эффективным.
Ответы
Ответ 1
Примечание добавлено в 2018 году
Начиная с strings.Builder
Go 1.10, есть strings.Builder
Тип strings.Builder
, пожалуйста, посмотрите на этот ответ для более подробной информации.
Ответ до 201x
Лучший способ - использовать пакет bytes
. Он имеет тип Buffer
который реализует 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())
}
Это происходит за O (n) раз.
Ответ 2
Наиболее эффективный способ объединения строк - использование встроенной функции copy
. В моих тестах этот подход примерно в 3 раза быстрее, чем при использовании bytes.Buffer
и намного быстрее (~ 12 000 раз), чем при использовании оператора +
. Кроме того, он использует меньше памяти.
Я создал контрольный пример, чтобы доказать это, и вот результаты:
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
Ниже приведен код для тестирования:
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)
}
}
Ответ 3
Начиная с Go 1.10, есть strings.Builder
, здесь.
Построитель используется для эффективного построения строки с использованием методов записи. Это минимизирует копирование памяти. Нулевое значение готово к использованию.
Использование:
Это почти то же самое с 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())
}
Примечание. Не копируйте значение StringBuilder, поскольку оно кэширует базовые данные. Если вы хотите поделиться значением StringBuilder, используйте указатели.
Методы и интерфейсы StringBuilder, которые он поддерживает:
Его методы реализуются с учетом существующих интерфейсов, поэтому вы можете легко переключаться на новый Builder в своем коде.
Использование с нулевым значением:
var buf strings.Builder
Отличия от байтов. Буфер:
Он может только расти или сбрасываться.
В bytes.Buffer
можно получить доступ к нижележащим байтам следующим образом: (*Buffer).Bytes()
; strings.Builder
предотвращает эту проблему. Иногда это не является проблемой, хотя и желательно вместо этого (например, для просмотра поведения при передаче байтов в io.Reader
и т.д.).
Он также имеет встроенный механизм copyCheck, который предотвращает его случайное копирование (func (b *Builder) copyCheck() { ... }
).
Проверьте его исходный код здесь.
Ответ 4
В пакете строк есть библиотечная функция Join
: http://golang.org/pkg/strings/#Join.
Взгляд на код Join
показывает похожий подход к функции Append, которую написал Kinopiko: https://golang.org/src/strings/strings.go#L420
Использование:
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
Ответ 5
Я только что проверил верхний ответ, опубликованный выше, в своем собственном коде (рекурсивный BufferString
дерева), и простой оператор BufferString
самом деле быстрее, чем 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()
}
Это заняло 0,81 секунды, тогда как следующий код:
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
}
заняло всего 0,61 секунды. Вероятно, это связано с накладными расходами на создание новой BufferString
.
Обновление: я также протестировал функцию join
и она запустилась за 0,54 секунды.
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,"")
}
Ответ 6
Вы можете создать большой кусок байтов и скопировать в него байты коротких строк с использованием строковых срезов. В "Эффективном Go" есть функция:
func Append(slice, data[]byte) []byte {
l := len(slice);
if l + len(data) > cap(slice) { // reallocate
// Allocate double what 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;
}
Затем, когда операции закончены, используйте string ( )
в большом куске байтов, чтобы снова преобразовать его в строку.
Ответ 7
Это самое быстрое решение, которое не требует
вы сначала должны знать или рассчитать общий размер буфера:
var data []byte
for i := 0; i < 1000; i++ {
data = append(data, getShortStringFromSomewhere()...)
}
return string(data)
По моему тегу он на 20% медленнее, чем решение для копирования (8.1ns per
append, а не 6.72ns), но все же на 55% быстрее, чем использование байтов .Buffer.
Ответ 8
package main
import (
"fmt"
)
func main() {
var str1 = "string1"
var str2 = "string2"
out := fmt.Sprintf("%s %s ",str1, str2)
fmt.Println(out)
}
Ответ 9
Примечание добавлено в 2018 году
Начиная с strings.Builder
Go 1.10, есть strings.Builder
Тип strings.Builder
, пожалуйста, посмотрите на этот ответ для более подробной информации.
Ответ до 201x
Код теста @cd1 и другие ответы неверны. bN
не должен быть установлен в контрольной функции. Он устанавливается инструментом динамического тестирования, чтобы определить, стабильно ли время выполнения теста.
Функция эталонного теста должна запускать один и тот же тест bN
раз, и тест внутри цикла должен быть одинаковым для каждой итерации. Поэтому я исправляю это, добавляя внутренний цикл. Я также добавлю тесты для некоторых других решений:
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)
}
}
Окружающая среда - OS X 10.11.6, 2,2 ГГц Intel Core i7
Результаты теста:
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
Заключение:
-
CopyPreAllocate
- самый быстрый способ; AppendPreAllocate
довольно близок к № 1, но легче написать код. -
Concat
очень Concat
производительность как по скорости, так и по использованию памяти. Не используйте это. -
Buffer#Write
и Buffer#WriteString
в основном одинаковы по скорости, в отличие от того, что @Dani-Br сказал в комментарии. Учитывая, что string
действительно []byte
в Go, это имеет смысл. - bytes.Buffer в основном использует то же решение, что и
Copy
с дополнительным ведением бухгалтерского учета и другими вещами. -
Copy
и Append
используют размер начальной загрузки 64, такой же, как в байтах. Буфер -
Append
использовать больше памяти и выделяет, я думаю, это связано с алгоритмом роста, который он использует. Это не растущая память так быстро, как байты.
Предложение:
- Для простых задач, таких как то, что хочет OP, я бы использовал
Append
или AppendPreAllocate
. Это достаточно быстро и просто в использовании. - Если необходимо одновременно прочитать и записать буфер, используйте
bytes.Buffer
. Вот для чего он предназначен.
Ответ 10
Мое первоначальное предложение было
s12 := fmt.Sprint(s1,s2)
Но выше ответ, используя bytes.Buffer - WriteString() является наиболее эффективным способом.
В моем первоначальном предложении используется отражение и переключатель типа. См. (p *pp) doPrint
и (p *pp) printArg
Для базовых типов нет универсального интерфейса Stringer(), как я наивно думал.
По крайней мере, хотя Sprint() внутренне использует bytes.Buffer. Таким образом,
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`
является приемлемым с точки зрения распределения памяти.
= > Конкатенация Sprint() может использоваться для быстрого вывода отладки.
= > В противном случае используйте bytes.Buffer... WriteString
Ответ 11
Расширение ответа cd1:
Вы можете использовать append() вместо copy().
append() делает все более значительные предварительные положения, обходится немного больше памяти, но экономит время.
Я добавил еще два теста в верхней части экрана.
Запустите локально с помощью
go test -bench=. -benchtime=100ms
На моем Thinkpad T400s он дает:
BenchmarkAppendEmpty 50000000 5.0 ns/op
BenchmarkAppendPrealloc 50000000 3.5 ns/op
BenchmarkCopy 20000000 10.2 ns/op
Ответ 12
Это фактическая версия теста, предоставленная @cd1 (Go 1.8
, linux x86_64
) с исправлениями ошибок, упомянутых @icza и @PickBoy.
Bytes.Buffer
всего 7
раз быстрее, чем прямая конкатенация строк с помощью оператора +
.
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")
}
}
}
Тайминги:
BenchmarkConcat-4 300000 6869 ns/op
BenchmarkBuffer-4 1000000 1186 ns/op
Ответ 13
goutils.JoinBetween
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()
}
Ответ 14
Я делаю это, используя следующее: -
package main
import (
"fmt"
"strings"
)
func main (){
concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator.
fmt.Println(concatenation) //abc
}
Ответ 15
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))
}
Ответ 16
Для тех, кто пришел из мира Java, где у нас StringBuilder
для эффективной конкатенации строк, похоже, что последняя версия go имеет свой эквивалент и называется Builder
: https://github.com/golang/go/blob/master/src/strings/builder.go
Ответ 17
Взгляните на библиотеку golang strconv, предоставляющую доступ к нескольким функциям AppendXX, что позволяет нам объединять строки со строками и другими типами данных.
Ответ 18
Результат теста со статистикой выделения памяти. проверьте код теста на github.
использовать строки. Builder для оптимизации производительности.
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
Ответ 19
s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))
Ответ 20
strings.Join()
из пакета "strings"
Если у вас есть несоответствие типов (например, если вы пытаетесь соединить int и строку), вы делаете RANDOMTYPE (вещь, которую вы хотите изменить)
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, ""))
}
Выход :
hello all you people in here