Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Оптимізація у Golang

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

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

Нагадаю, минулого разу розповів про Examples, сьогодні хочу повідати своє потаєнне — чому я перейшов на Golang.

I як ти потрапив у нашу клінику?

Було це десь два роки тому. Великий ентерпрайз, пишемо складну Bussiness Critical систему. Я, як R&D інженер та за сумісництвом системний аналітик (колишній) наполягаю на важливості NFR (нефункціональних вимог) та методів їх перевірки — Load Testing.

Ок — запрапонував, сам і копай. А я узяв лопату клаву та пішов. Нарив мабуть найкращу тулзу для Load Testing K6 (зараз не буду розводити холіварів чому не традиційний вже Jmeter — на цей випадок можу написати сттатю але це ніяк не 25 хвилин) Так я і познайомився з Golang — K6 на Golang а стандартного функціоналу ніколи не вистачає.

Так і кодив потихеньку і тестив. Перетворився я на якогось продвинутого QA. І тут сталося несподіване.

Тестимо так тестимо. Кажу — тут погано, і тут погано, а тут взагалі жах (основний стек — .net). І які висновки — звичайно підкидуємо дров подів або ресурсів до подів. Бо оптимізувати це ж відволікати важливих сеньйорів від їхніх там якихось суперечок про високі матерії від банди чотирьох та спускати до фундаментальних речей. Ні — це дорого. А процессори/пам’ять дешево.

А я якраз начитався багато про нескінченні можливості оптимізації Go коду. Усі ці алокейшени і т/д і вирішив — досить засмучувати свого внутрішнього інженера і послав усіх за кораблем до того як це стало мейнстрімом.

Ось і я вам покажу звідки готувався напад трохи про бренчмарк тести і роскажу як можна оптимізувати.

Досить води. Давай про тести

Бренчмарк тести як і звичайні тести пишуться у звичайних для GO файлах із суфіксом *_test.go. Назва тестової функції починається з ключового слова

Benchmark
Передається у Benchmark функцію об’єкт типу testing.B.
Тест повинен запускатись у циклі щоб заміряти середній показник ітерацій тому запускаємо виклик функції у циклі:


func BenchmarkRepeat(b *testing.B) {
    fori := 0; i < b.N; i++ {
     Repeat("a")
    }
}
Саме цікаве тут b.N. N означає кількість ітерацій. Це число зазвичай розраховує сам Golang опираючись на заданий час тесту. Ми можемо лише контролювати цей час задаючи ліміт часу. Типу так

go test -bench=. -benchtime=10s
Але це все можете подивитись у доках і про все це написано багато. Давайте краще запустимо і подивимось що у нас на виході
І так ми побачили:
1. Запустили 18 060 724 разів.
2. Час виконання 55.64 ns/op (для операції)
3. 5 байт сжерли пам’яті
4. 1 алокація на операцію
Останнє найцікавіше. Багато хто вчився на курсах мабуть чули як важливо зменшувати кількість алокацій. Але не усім розповідали що це таке.

Ну давай про твої хіпи та стеки

Хто проходив курс універу по Computer Science мабуть чули що існують там якісь хіппі кучі та стеки. Не буду малювати 10000001 малюнок який демонструє як це працює.

Просто нагадаю

1. Cтек — це пам’ять якою дуже легко керувати. Вона працює по принципу LIFO — перший прийшов, перший уйшов. Якщо змінна вийшла зі стеку — її не існує. І нічого почищати не треба

2. Куча — просто куча. Ми виділяємо адресу пам’яті і щось туди кладемо щоб потім до цього звенутися напряму або за допомогою поінтера.

Стек — швидкий. Куча — тормозить.

Чому тормозить? Тому що ми не можемо знати напевне коли змінна чи об’єкт більше не потрібні і просто звільнити цю комірку пам’яті.

А на поміч нам приходить — Garbage Collector. І як у Python (та мабуть у ще багатьох мовах) коли він працює НІХТО БІЛЬШЕ НЕ ПРАЦЮЄ! Бо увесь його алгоритм зводиться к підрахуванню посилань на цю комірку і коли їх немає просто звільняти місце.

Ось чому важливо позбавлятись цих клятих алокацій що звичайно не можливо, але робити їх менше можливо

Lets Оптимізувати

І так уявімо собі існування такої функції:


func Repeat(s string) string {
    result := ""


    for i := 0; i < 5; i++ {
        result += s
    }
    return result
}
Функція просто бере і повторює переданий їй символ 5 разів. Ну що ж подивимось на бренчмарку

4 алокації. 164.0 ns/op.

У попередньому прикладі була 1 алокація та 55ns/op

Якось дуже попортив функцію (я старався)

Давайте подивимось що можна тут зробити. Ну по перше у кожній ітерації циклу ми заново ініціалізуємо I

Спробуємо так

func Repeat(s string) string {
    var result string
    var i int
    for i = 0; i < 5; i++ {
        result += s
    }
    return result
}
Я об’явив i до початку циклу (ще переписав об’яву result але повірте це нічого не змінить)
І результати:

Нічого. Але не засмучуємось з цього приводу. Я не маю великого досвіду тому оперую трохи на осліп. То ж давайте вигадаємо щось без циклу.

Заглянемо до стандартної ліби. Є така чудова функція як strings.Join(). Скористаємося цим:


func Repeat(s string) (result string) {
    r := make([]string, 5+2)
    return strings.Join(r, s)
}
Що ми тут зробили? Обьявили слайс довжиною скільки нам потрібно символів + 2 (це на початок та кінець. І обь’єднали цей слайс з сепаратором s (наш символ)
Результат вже краще:
Насправді функція Repeat теж є у стандартній лібі strings. Давайте протестуємо саму цю функцію зі стандартної ліби:
Алокація одна а працює швидше. Добре що все зараз OpenSource (навіть Microsoft де зрозуміли) і ми можемо зазернути у код і подивитись у чому ж твій секрет бро.
Коду там багато лише скажу що там використовується спеціальний тип Builder який дозволяє будувати строки з мінімальним використанням пам’яті. Але це тема окремої статті

Висновки

Ну по перше я зрозумів що ніяк не можу вкластися у виділену годину на статтю. Треба як найменше півтори години. Тому більше лірики писати не буду (мабуть вона вам теж не до вподоби)

По друге — досить бути дитиною, яка складає Lego по інструкції (кодером, який інтерпретує Bussiness Requirements у коді використовуючі визубрені патерни). Так ми завжди йдемо на технічні борги заради того щоб встигнути до дедлайну. Але борги треба гасити і окрім функціональних вимог ще є нефункціональні які у 90% випадків стосуються саме швидкодії нашого додатку.

І не треба все що стосується швидкодії та інших нецікавих штук перекладати на плечі DevOps бо вони терплять, терплять а потім майдан проти вас підіймуть :-)

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

ви тут взагалі-то чергу описали :D

перший прийшов, перший уйшов.

Підписатись на коментарі