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.

1077voto

marketer Punkte 37017

Neue Methode:

Ab Go 1.10 gibt es einen strings.Builder Typ, bitte werfen Sie einen Blick auf diese Antwort für weitere Details.

Alte Methode:

Verwenden Sie das bytes Paket. Es hat einen Buffer Typ, der io.Writer implementiert.

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString("a")
    }

    fmt.Println(buffer.String())
}

Dies geschieht in O(n) Zeit.

32 Stimmen

Anstelle von println(string(buffer.Bytes())); könnten Sie einfach println(buffer.String()) verwenden.

31 Stimmen

Anstelle von buffer := bytes.NewBufferString("") kannst du var buffer bytes.Buffer verwenden. Du benötigst auch keine dieser Semikolons :).

80 Stimmen

Unglaublich schnell. Hat einige naive "+"-Zeichenfolgenverknüpfungen in meinem Programm von 3 Minuten auf 1,3 Sekunden reduziert.

615voto

Inanc Gumus Punkte 20769

Ab Go 1.10+ gibt es strings.Builder, hier.

Ein Builder wird verwendet, um effizient einen String mit Write-Methoden zu erstellen. Es minimiert das Kopieren von Speicher. Der Nullwert ist einsatzbereit.


Beispiel

Es ist fast dasselbe wie mit bytes.Buffer.

package main

import (
    "strings"
    "fmt"
)

func main() {
    // NULLWERT:
    //
    // Es ist von Anfang an einsatzbereit.
    // Du musst es nicht initialisieren.
    var sb strings.Builder

    for i := 0; i < 1000; i++ {
        sb.WriteString("a")
    }

    fmt.Println(sb.String())
}

Klicke hier, um dies im Playground zu sehen.


Unterstützte Schnittstellen

Die Methoden von strings.Builder werden unter Berücksichtigung der vorhandenen Schnittstellen implementiert, sodass Sie einfach vom neuen Builder-Typ in Ihrem Code umschalten können.

Methodensignatur

Schnittstelle

Beschreibung

Grow(int)

bytes.Buffer

Erhöht die Kapazität des Puffers um den angegebenen Betrag. Siehe bytes.Buffer#Grow für weitere Informationen.

Len() int

bytes.Buffer

Gibt die Anzahl der Bytes im Puffer zurück. Siehe bytes.Buffer#Len für weitere Informationen.

Reset()

bytes.Buffer

Setzt den Puffer zurück, um leer zu sein. Siehe bytes.Buffer#Reset für weitere Informationen.

String() string

fmt.Stringer

Gibt den Inhalt des Puffers als String zurück. Siehe fmt.Stringer für weitere Informationen.

Write([]byte) (int, error)

io.Writer

Schreibt die gegebenen Bytes in den Puffer. Siehe io.Writer für weitere Informationen.

WriteByte(byte) error

io.ByteWriter

Schreibt das gegebene Byte in den Puffer. Siehe io.ByteWriter für weitere Informationen.

WriteRune(rune) (int, error)

bufio.Writer oder bytes.Buffer

Schreibt das gegebene Zeichen in den Puffer. Siehe bufio.Writer#WriteRune oder bytes.Buffer#WriteRune für weitere Informationen.

WriteString(string) (int, error)

io.stringWriter

Schreibt den gegebenen String in den Puffer. Siehe io.stringWriter für weitere Informationen.


Unterschiede zu bytes.Buffer

  • Es kann nur wachsen oder zurücksetzen.
  • Es hat einen integrierten copyCheck-Mechanismus, der verhindert, dass er versehentlich kopiert wird. In bytes.Buffer kann man z.B. auf die zugrunde liegenden Bytes wie folgt zugreifen: (*Buffer).Bytes(). strings.Builder verhindert dieses Problem. Manchmal ist dies jedoch kein Problem, sondern stattdessen erwünscht. Zum Beispiel: Für das Peek-Verhalten, wenn die Bytes an einen io.Reader übergeben werden usw.
  • bytes.Buffer.Reset() spult vor und verwendet erneut den zugrunde liegenden Puffer, während strings.Builder.Reset() es nicht tut, es trennt den Puffer.

Hinweis

  • Kopieren Sie keinen strings.Builder-Wert, da er die zugrunde liegenden Daten zwischenspeichert.
  • Wenn Sie einen strings.Builder-Wert teilen möchten, verwenden Sie einen Zeiger darauf.

Schau dir den Quellcode für weitere Details an, hier.

5 Stimmen

Was meinen Sie mit 'entkommen'? Bedeuten Sie Entkommen in der Zeichenfolge oder nur dass die zugrunde liegenden Bytes freigelegt werden können?

1 Stimmen

@makhdumi Ja, 2., Bloßstellung der zugrunde liegenden Bytes.

0 Stimmen

Wichtig ist strings.Builder implementiert seine Methoden mit einem Zeigerempfänger, was mich einen Moment lang verwirrt hat. Deshalb würde ich wahrscheinlich einen mit new erstellen.

293voto

cd1 Punkte 14827

Wenn Sie die Gesamtlänge des Strings kennen, den Sie vorab zuweisen werden, kann der effizienteste Weg zum Konkatenieren von Strings die Verwendung der integrierten Funktion copy sein. Wenn Sie die Gesamtlänge nicht im Voraus wissen, verwenden Sie copy nicht und lesen Sie stattdessen die anderen Antworten.

In meinen Tests ist dieser Ansatz ~3x schneller als die Verwendung von bytes.Buffer und viel, viel schneller (~12.000x) als die Verwendung des Operators +. Außerdem benötigt es weniger Speicher.

Ich habe einen Testfall erstellt, um dies zu beweisen, und hier sind die Ergebnisse:

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

Im Folgenden finden Sie den Code für die Tests:

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("unerwartetes Ergebnis; erhalten=%s, soll=%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("unerwartetes Ergebnis; erhalten=%s, soll=%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("unerwartetes Ergebnis; erhalten=%s, soll=%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("unerwartetes Ergebnis; erhalten=%s, soll=%s", strBuilder.String(), s)
    }
}

6 Stimmen

Der bytes.Buffer sollte im Grunde dasselbe wie die Kopie machen (mit etwas zusätzlicher Buchführung, nehme ich an), und die Geschwindigkeit ist nicht so unterschiedlich. Also würde ich das benutzen :). Der Unterschied besteht darin, dass der Puffer mit 0 Bytes beginnt, sodass er neu allokiert werden muss (das lässt ihn etwas langsamer erscheinen, denke ich). Einfacher zu benutzen, allerdings.

1 Stimmen

Der Testfall gibt runtime.main: undefined: main.main zurück.

1 Stimmen

Du kannst Testdateien nicht so ausführen wie normale Dateien; du musst sie mit go test ausführen.

155voto

mbarkhau Punkte 7822

Wenn Sie einen Zeichenfolgenslice haben, den Sie effizient in eine Zeichenfolge umwandeln möchten, können Sie diesen Ansatz verwenden. Andernfalls schauen Sie sich die anderen Antworten an.

Es gibt eine Bibliotheksfunktion im Paket strings namens Join:

Ein Blick auf den Code von Join zeigt einen ähnlichen Ansatz wie die von Kinopiko geschriebene Append-Funktion: https://golang.org/src/strings/strings.go#L420

Verwendung:

import (
    "fmt";
    "strings";
)

func main() {
    s := []string{"dies", "ist", "ein", "zusammengesetzter", "String\n"};
    fmt.Printf(strings.Join(s, " "));
}

$ ./test.bin
dies ist ein zusammengesetzter String

25 Stimmen

Funktioniert nicht, wenn Sie über etwas iterieren müssen, das kein []string ist.

48voto

JasonMc Punkte 689

Ich habe gerade die beste Antwort oben in meinem eigenen Code (ein rekursiver Baumspaziergang) benchmarkt und der einfache Konkatenationsoperator ist tatsächlich schneller als der 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()
}

Dies dauerte 0,81 Sekunden, während der folgende Code:

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
} 

nur 0,61 Sekunden dauerte. Dies liegt wahrscheinlich an den Overhead-Kosten für das Erstellen des neuen BufferString.

Update: Ich habe auch die join Funktion benchmarkt und sie lief in 0,54 Sekunden.

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

5 Stimmen

Ich glaube, der OP machte sich mehr Gedanken über den Speicherkomplexität als über die Laufzeitkomplexität, da naive Zeichenkettenverkettungen jedes Mal zu neuen Speicherzuweisungen führen.

16 Stimmen

Die langsame Geschwindigkeit könnte gut damit zusammenhängen, dass fmt.Fprint anstelle von buffer.WriteString("\t"); und buffer.WriteString(subs[i]); verwendet wird.

0 Stimmen

Ich freue mich zu wissen, dass meine bevorzugte Methode von (strings.Join) als die schnellste läuft, während von dieser sagt, dass (bytes.Buffer) der Gewinner ist!

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