996 Stimmen

Wie man Strings in Go effizient verketten kann

Im Go ist ein string ein primitiver Typ, was bedeutet, dass er schreibgeschützt ist und jede Manipulation einen neuen String erstellt.

Also, wenn ich Strings viele Male hintereinander verketten möchte, ohne die Länge des resultierenden Strings zu kennen, wie mache ich das am besten?

Der naive Ansatz wäre:

var s string
for i := 0; i < 1000; i++ {
    s += getShortStringFromSomewhere()
}
return s

aber das scheint nicht sehr effizient zu sein.

12 Stimmen

Noch eine Bank

1 Stimmen

Hinweis: Diese Frage und die meisten Antworten scheinen vor dem Hinzufügen von append() in die Sprache geschrieben worden zu sein, was eine gute Lösung dafür ist. Es wird genauso schnell wie copy() ausgeführt, aber das Slice wird zuerst erweitert, auch wenn dies bedeutet, dass ein neues zugrundeliegendes Array allokiert wird, wenn die Kapazität nicht ausreicht. bytes.Buffer macht immer noch Sinn, wenn Sie seine zusätzlichen Komfortmethoden möchten oder wenn das Paket, das Sie verwenden, dies erwartet.

10 Stimmen

Es ist nicht nur "sehr ineffizient"; es hat ein spezifisches Problem, mit dem jeder neue Nicht-CS-Mitarbeiter, den wir jemals eingestellt haben, in den ersten Wochen der Arbeit konfrontiert wird. Es ist quadratisch - O(n*n). Denken Sie an die Zahlenfolge: 1 + 2 + 3 + 4 + .... Es ist n*(n+1)/2, die Fläche eines Dreiecks mit der Basis n. Sie reservieren die Größe 1, dann die Größe 2, dann die Größe 3 usw., wenn Sie unveränderliche Strings in einer Schleife anhängen. Diese quadratische Ressourcenverwendung äußert sich auf mehrere Weisen als nur diese.

32voto

harold ramos Punkte 597
package main

import (
  "fmt"
)

func main() {
    var str1 = "Zeichenfolge1"
    var str2 = "Zeichenfolge2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}

2 Stimmen

Willkommen bei Stack Overflow! Nehmen Sie sich einen Moment Zeit, um sich das Bearbeitungshilfe im Hilfe-Center durchzulesen. Die Formatierung auf Stack Overflow unterscheidet sich von anderen Websites.

2 Stimmen

Während dieser Code-Schnipsel die Frage lösen kann, hilft eine Erklärung wirklich, die Qualität Ihres Beitrags zu verbessern. Denken Sie daran, dass Sie die Frage für zukünftige Leser beantworten und diese möglicherweise nicht die Gründe für Ihren Code-Vorschlag kennen. Versuchen Sie außerdem, Ihren Code nicht mit erläuternden Kommentaren zu überfüllen, da dies die Lesbarkeit des Codes und der Erklärungen verringert!

6 Stimmen

Dies beantwortet die Frage überhaupt nicht. fmt.Sprintf ist die schlechteste Methode in Bezug auf Effizienz beim Verknüpfen einfacher Zeichenfolgen. Laut diesem Bench erweist sich fmt.Sprintf sogar als langsamer als der Additionsoperator (+), den der OP erwähnte, als sehr ineffizient.

26voto

rog Punkte 5752

Dies ist die schnellste Lösung, die keine Kenntnis oder Berechnung der Gesamtpuffergröße erfordert:

var data []byte
for i := 0; i < 1000; i++ {
    data = append(data, getShortStringFromSomewhere()...)
}
return string(data)

Nach meinem Benchmark ist sie 20% langsamer als die Kopierlösung (8,1 ns pro Append statt 6,72 ns), aber immer noch 55% schneller als die Verwendung von bytes.Buffer.

25voto

Sie könnten ein großes Bytes-Slice erstellen und die Bytes der Kurzzeichenfolgen darin mit String-Slices kopieren. Es gibt eine Funktion in "Effective Go":

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // Neuzuweisung
        // Das Doppelte von dem zuweisen, was für zukünftiges Wachstum benötigt wird.
        newSlice := make([]byte, (l+len(data))*2);
        // Daten kopieren (könnte bytes.Copy() verwenden).
        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;
}

Dann, wenn die Operationen abgeschlossen sind, verwenden Sie string() auf dem großen Bytes-Slice, um es wieder in eine Zeichenfolge umzuwandeln.

0 Stimmen

Es ist interessant, dass es so viele Möglichkeiten gibt, dies in Go zu tun.

13 Stimmen

In effektivem Go heißt es auch, dass die Idee so nützlich ist, dass sie in einem Builtin eingefangen wurde. Also könnten Sie Ihre Funktion durch append(slice, byte...) ersetzen, scheint es.

24voto

PickBoy Punkte 1194

Notiz hinzugefügt im Jahr 2018

Ab Go 1.10 gibt es einen strings.Builder-Typ, bitte schauen Sie sich diese Antwort für weitere Details an.

Vor-201x-Antwort

Der Benchmark-Code von @cd1 und anderen Antworten ist falsch. b.N sollte nicht im Benchmark-Code gesetzt werden. Es wird vom go test-Tool dynamisch gesetzt, um zu bestimmen, ob die Ausführungszeit des Tests stabil ist.

Ein Benchmark-Code sollte den gleichen Test b.N-mal ausführen und der Test innerhalb der Schleife sollte für jede Iteration gleich sein. Also habe ich es durch das Hinzufügen einer inneren Schleife behoben. Ich habe auch Benchmarks für einige andere Lösungen hinzugefügt:

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)
    }
}

...

Die Umgebung ist OS X 10.11.6, 2,2 GHz Intel Core i7

Testergebnisse:

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

Schlussfolgerung:

  1. CopyPreAllocate ist der schnellste Weg; AppendPreAllocate liegt ziemlich nah an Platz 1, aber es ist einfacher, den Code zu schreiben.
  2. Concat hat sowohl hinsichtlich Geschwindigkeit als auch Speicherverbrauch eine wirklich schlechte Leistung. Verwenden Sie es nicht.
  3. Buffer#Write und Buffer#WriteString sind im Grunde in der Geschwindigkeit gleich, im Gegensatz zu dem, was @Dani-Br im Kommentar gesagt hat. Angesichts dessen, dass string in der Tat []byte in Go ist, macht es Sinn.
  4. bytes.Buffer verwendet im Wesentlichen die gleiche Lösung wie Copy mit zusätzlicher Buchführung und anderen Dingen.
  5. Copy und Append verwenden eine Bootstrap-Größe von 64, die gleiche wie bei bytes.Buffer
  6. Append verwendet mehr Speicher und Allokationen, ich denke, es hängt mit dem Wachstumsalgorithmus zusammen, den es verwendet. Es wächst nicht so schnell wie bytes.Buffer

Vorschlag:

  1. Für einfache Aufgaben wie das, was OP möchte, würde ich Append oder AppendPreAllocate verwenden. Es ist schnell genug und einfach zu verwenden.
  2. Wenn der Puffer gleichzeitig gelesen und geschrieben werden muss, verwenden Sie natürlich bytes.Buffer. Dafür ist es ausgelegt.

14voto

Peter Buchmann Punkte 514

Mein ursprünglicher Vorschlag war

s12 := fmt.Sprint(s1,s2)

Aber die oben genannte Antwort, die bytes.Buffer - WriteString() verwendet, ist der effizienteste Weg.

Mein anfänglicher Vorschlag verwendet Reflexion und einen Typenschalter. Siehe (p *pp) doPrint und (p *pp) printArg
Es gibt keine universelle Stringer() Schnittstelle für grundlegende Typen, wie ich naiverweise gedacht hatte.

Zumindest verwendet Sprint() intern einen bytes.Buffer. Daher

`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`

ist akzeptabel in Bezug auf Speicherzuweisungen.

\=> Sprint() Konkatenation kann für schnelle Debug-Ausgabe verwendet werden.
\=> Andernfalls verwenden Sie bytes.Buffer ... WriteString

9 Stimmen

Es ist nicht integriert und es ist nicht effizient.

0 Stimmen

Das Importieren eines Pakets (wie fmt) bedeutet, dass es nicht eingebaut ist. Es befindet sich in der Standardbibliothek.

0 Stimmen

Es ist langsam, nur weil es Reflexion auf seine Argumente verwendet. Es ist effizient. Ansonsten ist es nicht weniger effizient als das Verbinden mit strings.Join.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X