Рациональный анализ Golang strings.builder

Go

задний план

Во многих сценариях мы будем выполнять операции конкатенации строк.

В начале вы можете использовать следующие операции:

package main

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var str string
    for _, s := range ss {
        str += s
    }

    print(str)
}

Как и многие языки, поддерживающие строковый тип, строковый тип в golang доступен только для чтения и неизменяем. Таким образом, этот способ объединения строк приводит к созданию, уничтожению большого количества строк и выделению памяти. Если вы объединяете много строк, это, очевидно, неправильная позиция.

До Golang 1.10 вы могли использоватьbytes.BufferЧтобы оптимизировать:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b bytes.Buffer
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

использовать здесьvar b bytes.BufferСохраните окончательную сплайсированную строку, чтобы в определенной степени избежать вышеперечисленного.strПроблема повторного обращения за новым пространством памяти для хранения промежуточных строк каждый раз, когда выполняется операция склейки.

Но здесь все же есть небольшая проблема:b.String()будет один раз[]byte -> stringПреобразование типов. Эта операция выполнит выделение памяти и копирование содержимого.

Конкатенация строк с использованием strings.Builder

Если вы уже используете golang 1.10, у вас есть лучший вариант:strings.Builder:

package main

import (
    "fmt"
    "strings"
)

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b strings.Builder
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

Официальная воля Голангаstrings.BuilderПредставлено как функция, должно быть две кисти. Не верите в счет? Вот простой бенчмарк:

package ts

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
)

func BenchmarkBuffer(b *testing.B) {
    var buf bytes.Buffer
    for i := 0; i < b.N; i++ {
        fmt.Fprint(&buf, "")
        _ = buf.String()
    }
}

func BenchmarkBuilder(b *testing.B) {
    var builder strings.Builder
    for i := 0; i < b.N; i++ {
        fmt.Fprint(&builder, "")
        _ = builder.String()
    }
}
╰─➤  go test -bench=. -benchmem                                                                                                                         2 ↵
goos: darwin
goarch: amd64
pkg: test/ts
BenchmarkBuffer-4         300000        101086 ns/op      604155 B/op          1 allocs/op
BenchmarkBuilder-4      20000000            90.4 ns/op        21 B/op          0 allocs/op
PASS
ok      test/ts 32.308s

Прирост производительности впечатляет. Вы должны знать, что такие языки, как C# и Java с собственным GC, были представлены очень рано.string builder, Golang был представлен только в 1.10, сроки не слишком ранние, но огромное улучшение не разочаровало. Давайте посмотрим, как это делает стандартная библиотека.

Разбор принципа strings.Builder

strings.BuilderРеализация в файлеstrings/builder.goВсего всего 120 строк, что очень изыскано. Выдержки ключевых кодов следующие:

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte // 1
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, p...) // 2
    return len(p), nil
}

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))  // 3
}

func (b *Builder) copyCheck() {
    if b.addr == nil {
        // 4
        // This hack works around a failing of Go's escape analysis
        // that was causing b to escape and be heap allocated.
        // See issue 23382.
        // TODO: once issue 7921 is fixed, this should be reverted to
        // just "b.addr = b".
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}

  1. иbyte.BufferИдея аналогична, так как в процессе строительства струна будет постоянно разрушаться и перестраиваться, постарайтесь избежать этой проблемы и используйтеbuf []byteдля хранения содержимого строки.
  2. Для операций записи просто записывайте байты в buf.
  3. чтобы решитьbytes.Buffer.String()существующий[]byte -> stringИ проблемы преобразования типа копирования памяти Вотunsafe.PointerРабота преобразования указателя хранения, реализует прямое преобразованиеbuf []byteПреобразуйте в строковый тип, избегая проблемы полного выделения памяти.
  4. Если мы реализуем strings.Builder сами, в большинстве случаев мы делаем первые 3 шага. Но стандартная библиотека идет еще дальше. Мы знаем, что стек Golang в большинстве случаев не требует от разработчиков внимания, и если работа, которую можно выполнить в стеке, уйдет в кучу, производительность сильно снизится. следовательно,copyCheckДобавлена ​​строка сравнения, чтобы избежать ухода buf в кучу. В этой части вы можете прочитать больше о Дейве Чейни.Скрытые #прагмы Go.

останавливаться на достигнутом?

Как правило, методы, используемые в стандартной библиотеке Golang, будут постепенно продвигаться и стать лучшими практиками в определенных сценариях.

используется здесь*(*string)(unsafe.Pointer(&b.buf))На самом деле, его можно использовать и в других сценариях. Например: как сравнитьstringи[]byteЭто равно без выделения памяти?😓Кажется, что предзнаменование слишком очевидное, его должен написать каждый, только дайте код напрямую:

func unsafeEqual(a string, b []byte) bool {
    bbp := *(*string)(unsafe.Pointer(&b))
    return a == bbp
}

Расширенное чтение

The State of Go 1.10