Почему такая операция со строками медленная в Go

Вопрос или проблема

Я провел несколько тестов, и это занимает около 60 мс. Я также провел аналогичное тестирование с nodejs, и это занимает только 6 мс.

Код:

func main() {
    var r string
    s := ""
    for range 200 {
        s += "adfddslfjaidsfjidsf"
    }
    start := time.Now()
    defer func() {
        fmt.Println(time.Since(start))
    }()

    for range 100000 {
        r = s + "foo" // добавляем "foo", чтобы избежать использования ссылки
    }
    fmt.Println(len(r))
}

Вывод:

3803
63.370134мс

Ответ или решение

Вопрос о производительности операций со строками в Go, как в вашем примере, заключается в том, как Go обрабатывает строки и их конкатенацию. В вашем коде вы наблюдаете заметное замедление при работе со строками по сравнению с Node.js, и это связано с особенностями работы со строками в Go.

Причины медленной работы с конкатенацией строк в Go:

  1. Неизменяемость строк: В языке Go строки являются неизменяемыми (immutable), что означает, что всякий раз, когда вы изменяете строку (например, при конкатенации), создается новая строка в памяти. В вашем коде это происходит множество раз. Каждая операция конкатенации создает новую строку, которая копирует содержимое существующих строк и добавляет новую часть. Это увеличивает время выполнения и потребление памяти.

  2. Медленная аллокация памяти: При каждой конкатенации строк требуется выделение новой памяти, что добавляет накладные расходы в формате распределения памяти. Go использует сборщик мусора, и частые операции по выделению и освобождению памяти могут приводить к фрагментации и снижению производительности.

  3. Неоптимизированная конкатенация в цикле: В вашем случае вы производите множество операций конкатенации в цикле, а это создает комбинацию «плохих» операций. Вместо того чтобы накапливать строки по одному, было бы разумнее использовать strings.Builder или []rune, что позволит более эффективно накапливать строки и минимизировать количество копирований.

Рекомендации по улучшению производительности:

  1. Использование strings.Builder: Вместо конкатенации строк с помощью оператора +=, вы можете использовать strings.Builder, который оптимизирован для такого рода операций. Ниже приведен пример переписанного кода на основе вашего:

    package main
    
    import (
       "fmt"
       "strings"
       "time"
    )
    
    func main() {
       var r string
       s := ""
       var builder strings.Builder
    
       for range 200 {
           builder.WriteString("adfddslfjaidsfjidsf")
       }
       s = builder.String()
    
       start := time.Now()
       defer func() {
           fmt.Println(time.Since(start))
       }()
    
       builder.Reset() // очистка буфера
       for range 100000 {
           builder.WriteString(s + "foo") // использование буфера для конкатенации
       }
       r = builder.String() // преобразование в строку
       fmt.Println(len(r))
    }
  2. Предварительное выделение памяти: Если вы знаете, сколько места вам потребуется, можно заранее выделить память, что также поможет избежать многократных аллокаций.

Заключение:

Использование strings.Builder или аналогичных подходов значительно снижает накладные расходы, связанные с регулярной конкатенацией строк. Это должно помочь уменьшить время выполнения вашего кода и приблизить его производительность к более оптимизированной реализации, как в Node.js. Как разработчик, важно учитывать внутренние механизмы работы с памятью и эффективные паттерны программирования, чтобы добиться максимальной производительности приложений на Go.

Оцените материал
Добавить комментарий

Капча загружается...