Оптимизация производительности Голанга

Оптимизация памяти.

#1. Объединение небольших объектов.

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

Механизм памяти Golang также представляет собой пул памяти, каждый диапазон имеет размер 4 КБ и поддерживает кеш, который имеет массив списков.

Массив хранит связанный список, как и метод zip в HashMap, размер памяти, представленный каждой сеткой массива, разный, а 64-битная машина основана на 8 байтах.

Например, нижний индекс 0 — это узел связанного списка размером 8 байт, а нижний индекс 1 — узел связанного списка размером 16 байт. Память каждого индекса отличается, и используется самая последняя память, выделенная по запросу.

В другом примере память структуры на самом деле составляет 31 байт, и при ее выделении будет выделено 32 байта.

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

Поэтому рекомендуется объединять небольшие объекты в одну структуру.

for k, v := range m {
    // copy for capturing by the goroutine
    x := struct {k , v string} {k, v} 
    go func() {
        // using x.k & x.v
    }()
}

#2. Разумное использование buff кеша.

Когда протокольное кодирование требует частой работы баффа, вы можете использовать bytes.Buffer в качестве буферного объекта, он будет выделять достаточно памяти за один раз, избегать динамического применения памяти, когда памяти недостаточно, уменьшать количество выделений памяти, и бафф можно дублировать Использование (рекомендуется повторное использование)

Например, при создании срезов и карт заранее оцените размер и укажите емкость.

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

t := make([]int, 0, 100)
m := make(map[string]int, 100)

Если вы не уверены, будет ли инициализирован slice, используйте var, который не будет выделять память, а make([]int,0) выделит место в памяти.

var t []int

Совет. Емкость среза удваивается до того, как емкость станет меньше или равна 1024. После того, как емкость превысит 1024, каждое увеличение составляет 1/4.

Механизм расширения карты более сложен. Каждое расширение кратно 2. В структуре есть сегмент oldBuckets для реализации постепенного расширения.

#3. Длинный стек вызовов позволяет избежать обращения к большему количеству временных объектов.

Размер стека по умолчанию для goroutine составляет 4 КБ.

В Golang1.7 он изменен на 2 КБ, в котором используется механизм непрерывного стека. Когда места в стеке недостаточно, горутина будет продолжать расширяться, и каждое расширение будет таким же, как расширение слайсов.

Это включает в себя применение нового пространства стека и копирование старого пространства стека. Если GC обнаружит, что текущее пространство составляет всего 1/4 от предыдущего, оно снова уменьшится, а частое обращение к памяти и копирование вызовут дополнительные накладные расходы.

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

#4. Избегайте частого создания временных переменных.

Время GC STW было оптимизировано до наихудшей 1 мс, но все еще существуют смешанные барьеры записи, которые снижают производительность. Если временных переменных слишком много, потеря производительности GC будет высокой.

Рекомендации: уменьшить область действия переменных, использовать локальные переменные, свести к минимуму видимость и объединить несколько переменных в один массив структур (уменьшить время сканирования).

#5. Большие структуры передаются по указателю.

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

Оптимизация параллелизма.

#1. Используйте goroutine пул.

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

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

#2. Сократите количество системных вызовов.

Реализация goroutine заключается в моделировании асинхронных операций посредством синхронизации. Например, следующие операции не будут блокировать планирование потока runtime .

  • Сетевой ввод-вывод.
  • Канал.
  • time.Sleep.
  • На основе базового асинхронного SysCall .

Следующая блокировка создает новое расписание потока.

  • местный ИО.
  • SysCall основан на низкоуровневой синхронизации.
  • CGO вызывает IO или другую блокировку.

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

#3. Разумно уменьшить детализацию блокировок.

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

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

Нижний слой channel — это связанный список + блокировка.

Не используйте channel для передачи данных, таких как изображения, производительность любой очереди очень низкая, вы можете попытаться оптимизировать большие объекты с помощью указателей.

#4. Разумное использование protobuf.

protobuf более эффективен в хранении и анализе, чем json. Рекомендуется использовать protobuf вместо json для сохранения или передачи данных.

#5. Совокупные данные на основе бизнес-сценариев.

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

ctxTimeout, cf := context.WithTimeout(context.Background(), time.Second)
defer cf()
g, ctx := errgroup.WithContext(ctxTimeout)
var urls = []string{
    "http://www.golang.org/",
    "http://www.google.com/",
    "http://www.foo.com/",
}
for _, url := range urls {
    // Launch a goroutine to fetch the URL.
    url := url // https://golang.org/doc/faq#closures_and_goroutines
    g.Go(func() error {
        // Fetch the URL.
        resp, err := http.Get(url)
        if err == nil {
            resp.Body.Close()
        }
        return err
    })
}
// Wait for all HTTP fetches to complete.
if err := g.Wait(); err == nil {
    fmt.Println("Successfully fetched all URLs.")
}
select {
case <-ctx.Done():
    fmt.Println("Context canceled")
default:
    fmt.Println("Context not canceled")
}

Простые ошибки.

#1. Распространенные ошибки, связанные с channels.

  • Закрытие закрытого канала приведет к panic.
  • Отправка данных в закрытый канал будет panic.
  • Чтение данных из закрытого канала является начальным значением по умолчанию.

Принцип закрытия канала.

  • Не закрывайте channel от приемника.
  • Не закрывайте channels с несколькими отправителями.
  • Когда есть только один отправитель и в дальнейшем данные не будут отправлены, channel можно закрыть.

Также обратите внимание, что кэшированные каналы не обязательно упорядочены.

Как изящно закрыть каналы?

https://go101.org/article/channel-closing.html

#2. Распространенные ошибки, связанные с defer.

Дополнительное внимание следует уделить переменным в defer.

  • Параметры передаются при вызове.
i := 1
defer println("defer", i)
i++
// defer 1
  • Непараметрические замыкания.
i := 1
defer func() {
    println("defer", i)
}()
i++
// defer 2
  • Именованные возвраты — это то же замыкание, которое изменит возвращаемое значение именованного возврата.
func main(){
   fmt.Printf("main: %v\n", getNum())
   // defer 2
   // main: 2
}

func getNum() (i int) {
   defer func() {
      i++
      println("defer", i)
   }()
   i++
   return
}

В частности, будьте осторожны, чтобы не вызвать defer в for loop.

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

Предложение: инкапсулировать логику кода цикла for, требующего отсрочки, в функцию.

#3. Распространенные сбои в http.

Стандартный запрос Golang HTTP не имеет тайм-аута, что является большой проблемой.

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

Кроме того, ответ фреймворка HTTP-запроса должен быть закрыт методом Close, иначе возможны утечки памяти.

#4. Распространенные ошибки, связанные с interface.

Когда именно interface равно nil?

Примечание: interface{} и тип интерфейса отличаются от struct. Нижний уровень интерфейса состоит из двух элементов: один type, а другой value.

Только когда type и value оба равны nil, interface{} равно nil.

var u interface{} = (*interface{})(nil)
if u == nil {
    t.Log("u is nil")
} else {
    t.Log("u is not nil")
}
// u is not nil

Пример интерфейса.

var u Car = (Car)(nil)
if u == nil {
    t.Log("u is nil")
} else {
    t.Log("u is not nil")
}
// u is nil

Пользовательская структура.

var u *user = (*user)(nil)
if u == nil {
    t.Log("u is nil")
} else {
    t.Log("u is not nil")
}
// u is nil

#5. Общие сбои около map.

Map одновременное чтение и запись будут panic, необходимо заблокировать или использовать sync.Map .

map также не может напрямую обновлять поле value.

type User struct{
   name string
}
func TestMap(t *testing.T) {
   m := make(map[string]User)
   m["1"] = User{name:"1"}
   m["1"].name = "2"
   // Compilation failed, you cannot directly modify a field value of map
}

Выводить нужно отдельно.

func TestMap(t *testing.T) {
   m := make(map[string]User)
   m["1"] = User{name: "1"}
   u1 := m["1"]
   u1.name = "2"
}

#6. Распространенные ошибки, связанные с slice.

Массивы — это типы значений, срезы — это ссылочные типы (указатели).

func TestArray(t *testing.T) {
   a := [1]int{}
   setArray(a)
   println(a[0])
   // 0
}
func setArray(a [1]int) {
   a[0] = 1
}
func TestSlice(t *testing.T) {
   a := []int{
      1,
   }
   setSlice(a)
   println(a[0])
   // 1
}
func setSlice(a []int) {
   a[0] = 1
}

Метод range создаст копию каждого элемента, и будет копия значения. Если в массиве хранится большая структура, можно использовать обход индекса или оптимизацию указателя.

Поскольку value является копией, исходное значение изменить нельзя.

Метод append изменяет адрес.

Суть типа slice — это структура.

type slice struct {
   array unsafe.Pointer
   len   int
   cap   int
}

Копирование значения функции сделает модификацию недействительной.

func TestAppend1(t *testing.T) {
   var a []int
   add(a)
   println(len(a))
   // 0
}

func add(a []int) {
   a = append(a, 1)
}

#7. Распространенные ошибки, связанные с closure.

for i := 0; i < 3; i++ {
    go func() {
        println(i)
    }()
}
time.Sleep(time.Second)
// 2
// 2
// 2

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

Решение 1. Локальные переменные.

for i := 0; i < 3; i++ {
    ii := i
    go func() {
        println(ii)
    }()
}
time.Sleep(time.Second)
// 2
// 0
// 1

Решение 2. Передача параметров.

for i := 0; i < 3; i++ {
    go func(ii int) {
        println(ii)
    }(i)
}
time.Sleep(time.Second)
// 2
// 0
// 1

#8. Распространенные ошибки, связанные с select.

default в for будет выполняться в select, и ЦП не будет занят все время, что приведет к бездействию ЦП.

Образец кода.

func TestForSelect(t *testing.T) {
   for {
      select {
        case <-time.After(time.Second * 1):
            println("hello")
        default:
            if math.Pow10(100) == math.Pow(10, 100) {
                println("equal")
            }
        }
    }
}

Выполните команду top.

top - 15:00:50 up 1 day, 15:55,  0 users,  load average: 1.36, 0.85, 0.35
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND   
28632 root      20   0 2168296   1.4g   2244 S 252.8  11.7   1:04.15 __debug_bin

Спасибо за прочтение.

Если вам нравятся такие истории и вы хотите поддержать меня, пожалуйста, хлопните мне в ладоши.

Ваша поддержка очень важна для меня — спасибо.