Оптимизация производительности Голанга
Оптимизация памяти.
#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
Спасибо за прочтение.
Если вам нравятся такие истории и вы хотите поддержать меня, пожалуйста, хлопните мне в ладоши.
Ваша поддержка очень важна для меня — спасибо.