string - titel - title tag länge 2018




Wie können Strings in Go effizient verkettet werden? (12)

In Go ist string ein primitiver Typ, es ist readonly, jede Manipulation daran erzeugt eine neue Zeichenkette.

Also, wenn ich Strings viele Male verketten möchte, ohne die Länge der resultierenden Saite zu kennen, was ist der beste Weg, dies zu tun?

Der naive Weg wäre:

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

aber das scheint nicht sehr effizient zu sein.


Aktualisierung 2018-04-03

Ab Go 1.10 wird here als Ersatz für bytes.Buffer . Überprüfen Sie die 1.10 Versionshinweise

Ein Builder vom Typ new ist ein Ersatz für bytes.Buffer für den Anwendungsfall, Text in einem String-Ergebnis zu sammeln. Die API des Builders ist eine eingeschränkte Teilmenge von bytes.Buffer, mit der sicher vermieden werden kann, dass während der String-Methode eine doppelte Kopie der Daten erstellt wird.

================================================= ==========

Der Benchmark-Code von @ cd1 und anderen Antworten ist falsch. bN soll nicht in Benchmark-Funktion gesetzt werden. Es wird vom go-Test-Tool dynamisch festgelegt, um festzustellen, ob die Ausführungszeit des Tests stabil ist.

Eine Benchmark-Funktion sollte denselben bN Test bN und der Test innerhalb der Schleife sollte für jede Iteration gleich sein. Also repariere ich es, indem ich eine innere Schleife hinzufüge. Ich füge auch Benchmarks für einige andere Lösungen hinzu:

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

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

Fazit:

  1. CopyPreAllocate ist der schnellste Weg; AppendPreAllocate ist ziemlich nah an No.1, aber es ist einfacher, den Code zu schreiben.
  2. Concat hat wirklich schlechte Leistung sowohl für Geschwindigkeit und Speicherverbrauch. Benutze es nicht.
  3. Buffer#Write und Buffer#WriteString sind im Grunde die gleiche Geschwindigkeit, im Gegensatz zu dem, was @ Dani-Br im Kommentar gesagt hat. Wenn man bedenkt, dass in Go tatsächlich ein []byte , ergibt das einen Sinn.
  4. Bytes.Buffer verwenden grundsätzlich die gleiche Lösung wie Copy mit zusätzlicher Buchführung und anderem.
  5. Copy und Append verwenden Sie eine Bootstrap-Größe von 64, das gleiche wie Byte.Buffer
  6. Append benutze mehr Speicher und weise zu, ich denke, es hängt mit dem verwendeten Grow-Algorithmus zusammen. Es wächst Speicher nicht so schnell wie Bytes.Buffer

Vorschlag:

  1. Für einfache Aufgaben wie das, was OP will, würde ich Append oder AppendPreAllocate . Es ist schnell genug und einfach zu bedienen.
  2. Wenn Sie den Puffer gleichzeitig lesen und schreiben müssen, verwenden bytes.Buffer natürlich bytes.Buffer . Dafür ist es gedacht.

Der beste Weg ist die Verwendung des bytes Pakets. 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 macht es in O (n) Zeit.


Dies ist die aktuelle Version des Benchmarks von @ cd1 ( Go 1.8 , linux x86_64 ) mit den Bugfixes von @icza und @PickBoy.

Bytes.Buffer ist nur 7 mal schneller als direkte Stringverkettung über + Operator.

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

Timings:

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op

Dies ist die schnellste Lösung, bei der Sie zunächst nicht die Gesamtpuffergröße kennen oder berechnen müssen:

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

Bei meinem benchmark ist es 20% langsamer als die Kopierlösung (8.1ns pro Anhang statt 6.72ns), aber immer noch 55% schneller als die Verwendung von bytes.Buffer.



Ich habe nur die obere Antwort in meinem eigenen Code (eine rekursive Baumspaziergang) Benchmark und der einfache concat-Operator 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 s, 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
} 

nahm nur 0,61s. Dies ist wahrscheinlich auf den Mehraufwand beim Erstellen der neuen BufferStrings zurückzuführen.

Update: Ich habe auch die Join- Funktion getestet und es lief in 0.54s

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

Im Strings-Paket Join gibt es eine Bibliotheksfunktion: http://golang.org/pkg/strings/#Join

Ein Blick auf den Code von Join zeigt einen ähnlichen Ansatz zu Append-Funktion Kinopiko schrieb: https://golang.org/src/strings/strings.go#L462

Verwendung:

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

In Go 1.10 gibt es einen strings.Builder , here .

Verwendung

Es ist fast das gleiche mit 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 Methoden und Schnittstellen, die es unterstützt

Methoden werden unter Berücksichtigung der vorhandenen Schnittstellen implementiert, so dass Sie einfach in Ihrem Code zum neuen Builder wechseln können.

Nullwertnutzung

sb := strings.Builder{}

Unterschiede von Bytes.Buffer

In bytes.Buffer Bytes können wie bytes.Buffer entkommen: (*Buffer).Bytes() . strings.Builder verhindert, dass das interne Byte-Segment zu entkommen. Außerdem ist es unveränderlich und kann nur wachsen oder zurückgesetzt werden.

Es hat auch einen Kopiercheck-Mechanismus, der verhindert, dass es auf seine Grow / Write-Methoden kopiert wird: func (b *Builder) copyCheck() { ... }

Überprüfen Sie den Quellcode here .


Sie könnten ein großes Stück von Bytes erstellen und die Bytes der kurzen Zeichenfolgen mithilfe von Zeichenfolgen in das Byte kopieren. Es gibt eine Funktion in "Effective 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;
}

Wenn die Operationen beendet sind, verwenden Sie string ( ) für die große Anzahl von Bytes, um sie erneut in eine Zeichenfolge umzuwandeln.


Werfen Sie einen Blick auf die strconv-Bibliothek von strconv , die Zugriff auf mehrere AppendXX-Funktionen bietet, die es uns ermöglichen, Strings mit Strings und anderen Datentypen zu verketten.

[Off Topic] Schau dir this Blog an, für einige Funktionen von Golang


package main

import (
  "fmt"
)

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

s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))




string-concatenation