Golang имеет множество преимуществ, таких как быстрая разработка/быстрая компиляция и многое другое.

Но для приложений, критически важных для производительности, этот язык может вам не подойти.

почему?

Чтобы добавить простоты для разработчика, язык управляет памятью за вас.

что это значит?

Вместо того, чтобы управлять памятью вручную, выделяя и освобождая память в куче, когда это необходимо, компилятор Golang решает во время компиляции, какие объекты будут выделены в куче, а во время выполнения имеет программу, работающую в фоновом режиме, которая сканирует кучу с интервалами и освобождает память. что не используется.

Программа, которая очищает кучу, называется сборщиком мусора и может оказать существенное влияние на производительность.

решение 1:

Для приложений, критически важных для производительности, используйте языки программирования с ручным управлением памятью.

решение 2:

Определите критические узкие места в коде и перепишите их на c и используйте cgo для запуска вашего приложения, но оставьте все остальные части написанными на golang.

solution3(sync.Pool):

Повторное использование памяти в куче.
Многие программы создают разные экземпляры одного и того же объекта много раз, поэтому вместо выделения новой памяти для каждого нового объекта у нас будет сохранен пул этих объектов, который мы можем использовать повторно.

Таким образом, мы не будем выделять новую память каждый раз, когда создаем этот объект, и у сборщика мусора будет меньше работы. вот где sync.Pool входит.

Как мне это использовать?

Это довольно просто.

Сначала мы объявляем пул с помощью функции New, которая инициализирует пул.
Функция New вызывается, когда нам нужно выделить новый элемент в пуле.

var pool = sync.Pool{New: func() any {
   return make([]int, 0, len)
}}

В этом примере мы создадим срез с динамической длиной.

В этом примере я получаю длину динамически, используя переменную среды (это важно для теста, так как фрагмент будет сохранен в куче, которая будет напоминать фрагмент реального динамического размера)

var len = func() int {
   length, err := strconv.Atoi(os.Getenv("len"))

   if err != nil {
      log.Fatal(err)
   }

   return length
}()

мы запустим 2 теста, один с использованием пула и один без него.

Наша первая функция — заполнить срез.

func fillSlice(arr []int) {

   for i := 0; i < len; i++ {
      arr = append(arr,1)
   }
}

Теперь давайте сравним это, когда мы создаем 1000 фрагментов в отдельных горутинах (при создании в одной горутине будут аналогичные результаты).

func BenchmarkSlice(b *testing.B) {
   b.ReportAllocs()

   for n := 0; n < b.N; n++ {
      wg := &sync.WaitGroup{}

      for i := 0; i < 1000; i++ {
         wg.Add(1)
         go func(wg *sync.WaitGroup) {
            defer wg.Done()
            arr := make([]int, 0, len)
            fillSlice(arr)
         }(wg)
      }
      wg.Wait()
   }
}

BenchmarkSlice-8         294    3459053 ns/op 12313920 B/op     2020 allocs/op

Теперь при запуске с sync.Pool:

func BenchmarkSlicePool(b *testing.B) {
   b.ReportAllocs()
   for n := 0; n < b.N; n++ {
      wg := &sync.WaitGroup{}

      for i := 0; i < 1000; i++ {
         wg.Add(1)
         go func(wg *sync.WaitGroup) {
            defer wg.Done()
            arr := pool.Get().([]int)
            fillSlice(arr)
      
            arr = arr[:0]
            pool.Put(arr)
         }(wg)
      }

      wg.Wait()
   }
}

BenchmarkSlicePool-8        3555     344817 ns/op    97699 B/op     2004 allocs/op

Мы видим, что у нас есть прирост производительности более чем в 10 раз!!!

294 против 3555 пробежек.

3459053 vs 344817 ns(3.4 vs 0.34 ns).

давайте пройдемся по течению:

arr := pool.Get().([]int)

эта строка получает новый фрагмент из пула.

pool.Put(arr)

эта строка помещает arr обратно в пул, чтобы другие подпрограммы могли использовать это.

arr = arr[:0]

это важная часть использования sync.Pool.

Базовый слайс должен быть очищен от старых значений, новажно, чтобы в процессе мы не дали сборщику мусора очистить память, и поэтому мы будем очищать ее таким образом arr = arr[:0] и не arr = nil или другими способами.

Заключение:

Как мы видели, пакет синхронизации может помочь оптимизировать код, критически важный для производительности, с помощью sync.Pool.

Изменения могут быть не всегда такими радикальными, но я улучшил производительность на реальных гипермасштабируемых серверах и рабочих процессах на 30%, используя этот пакет при создании повторно используемых объектов.