Как эффективно объединить строки в Go?
В, а string является примитивным типом, что означает, что он доступен только для чтения, и каждая манипуляция с ним создаст новую строку.
Итак, если я хочу объединить строки много раз, не зная длины результирующей строки, каков наилучший способ сделать это?
наивный способ будет:
s := ""
for i := 0; i < 1000; i++ {
s += getShortStringFromSomewhere()
}
return s
но это не кажется очень эффективным.
20 ответов:
лучший способ-использовать
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) времени.
Примечание добавлено в 2018 году
от Go 1.10 есть строки.Строитель тип, который достигает этого еще более эффективно (для строк). Приведенный там пример лаконичен и легко копируется/адаптируется.
Это аналог StringBuilder класс в Java.
наиболее эффективный способ объединения строк - это использование встроенной функции
copy. В моих тестах этот подход ~3 раза быстрее, чем использованиеbytes.Bufferи гораздо быстрее (~12,000 x), чем с помощью оператора+. Кроме того, он использует меньше памяти.Я создал тестовый случай чтобы доказать это и вот результаты:
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) } }
в пакете strings есть функция библиотеки с именем
Join: http://golang.org/pkg/strings/#Joinа посмотрите на код
Joinпоказывает аналогичный подход к добавлению функции Кинопико писал: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
начиная с 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 методы и интерфейсы, которые он поддерживает:
его методы реализуются с учетом существующих интерфейсов, так что вы можете легко переключиться на новый конструктор в своем коде.
- Grow (int) ->байт.Буфер#Grow
- Len () int ->байт.Буфер#Len
- сброс() ->байт.Буфер#Reset
- String () string -> fmt.Стрингер
- Write ([] byte) (int, error) ->io.Писатель
- ошибка WriteByte(байт) ->io.ByteWriter
- WriteRune (rune) (int, error) ->bufio.Писатель#WriteRune -байт.Буфер#WriteRune
- WriteString (string) (int, error)-> io.stringWriter
использование нулевого значения:
var sb strings.Builder
отличия от Байт.Буфер:
он неизменен, и он может только расти или сбрасываться.
на
bytes.Bufferбазовые байты могут экранироваться следующим образом:(*Buffer).Bytes();strings.Builderпредотвращает эту проблему.он также имеет механизм copyCheck внутри которого предотвращает случайное копирование (
func (b *Builder) copyCheck() { ... }).
проверьте его исходный код здесь.
Я просто сопоставил верхний ответ, опубликованный выше в моем собственном коде (рекурсивная прогулка по дереву), и простой оператор concat на самом деле быстрее, чем 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 С. Это, вероятно, из-за накладных расходов на создание новых BufferStrings.
обновление: Я также сравнил вступить функция и она побежала в 0.54 s
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,"") }
Это самое быстрое решение, которое не требует вы должны знать или рассчитать общий размер буфера в первую очередь:
var data []byte for i := 0; i < 1000; i++ { data = append(data, getShortStringFromSomewhere()...) } return string(data)мой ориентир, это на 20% медленнее, чем решение для копирования (8.1 НС на добавить вместо 6.72 НС), но все же на 55% быстрее, чем при использовании байтов.Буфер.
вы можете создать большой кусок байтов и скопировать байты коротких строк в него с помощью строковых срезов. Существует функция, заданная в "эффективном 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; }затем, когда операции будут завершены, используйте
string ( )на большой кусок байтов, чтобы преобразовать его снова в строку.
обновление 2018-04-03
по состоянию на 1.10,
string.Builderрекомендуется заменить наbytes.Buffer. Проверьте 1.10 примечания к выпускуновый тип Builder является заменой для байтов.Буфер для случая использования накопления текста в строковый результат. API Строителя-это ограниченное подмножество байтов.Буфер, который позволяет ему безопасно избежать создания дубликата данных во время строки метод.
============================================================
тестовый код @cd1 и другие ответы неверны.
b.Nне должен быть установлен в исходную функцию. Он устанавливается инструментом go test динамически, чтобы определить, является ли время выполнения теста стабильным.контрольная функция должна выполнять тот же тест
b.Nраз и тест внутри цикла должны быть одинаковыми для каждой итерации. Поэтому я исправляю это добавление внутреннего контура. Я также добавляю тесты для некоторых других решений: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 GHz 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имеет очень плохую производительность, как для скорости и использования памяти. Не используй его.Buffer#WriteиBuffer#WriteStringв основном то же самое в скорости, вопреки тому, что @Dani-Br сказал в комментарии. Учитываяstringдействительно[]byteв Go, это имеет смысл.- байт.Буфер в основном использует то же решение, что и
CopyС дополнительной бухгалтерии и других вещей.CopyиAppendиспользуйте размер начальной загрузки 64, такой же, как байты.БуферAppendИспользуйте больше памяти и выделений, я думаю, что это связано с алгоритмом роста, который он использует. Это не растет память так быстро как байты.Буферпредложение:
- для простой задачи, такой как то, что OP хочет, я бы использовал
AppendилиAppendPreAllocate. Это достаточно быстро и легко в использовании.- если необходимо одновременно читать и записывать буфер, используйте
bytes.Bufferконечно. Вот для чего он предназначен.
package main import ( "fmt" ) func main() { var str1 = "string1" var str2 = "string2" out := fmt.Sprintf("%s %s ",str1, str2) fmt.Println(out) }
мое первоначальное предложение было
s12 := fmt.Sprint(s1,s2)но выше ответ с помощью байт.Buffer-WriteString () - самый эффективный способ.
мое первоначальное предложение использует отражение и тип выключателя. посмотреть
(p *pp) doPrintи(p *pp) printArg
Нет универсального интерфейса Stringer () для базовых типов, как я наивно думал.хотя бы спринт() внутри использует байт.Буфер. Таким образом
`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`is приемлемо с точки зрения выделения памяти.
=> конкатенация Sprint() может использоваться для быстрого вывода отладки.
= > В противном случае используйте байты.Буфер... WriteString
расширение на ответ по компакт-диск 1 : Вы можете использовать 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
это фактическая версия бенчмарка, предоставленная @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
для тех, кто пришел из мира Java, где мы имеем
StringBuilderдля эффективной конкатенации строк, похоже, последняя версия go имеет свой эквивалент и называетсяBuilder: https://github.com/golang/go/blob/master/src/strings/builder.go
Я делаю это с помощью следующего :-
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)) }
результат бенчмарка со статистикой выделения памяти. проверьте тестовый код на github.
использовать строки.Конструктор для оптимизации производительности.
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() }
взгляните на golang это strconv библиотека, предоставляющая доступ к нескольким функциям AppendXX, что позволяет нам объединять строки со строками и другими типами данных.
strings.Join()из пакета "strings"Если у вас есть несоответствие типа(например, если вы пытаетесь соединить int и строку), Вы делаете RANDOMTYPE (вещь, которую вы хотите изменить)
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) }
Comments