Бенчмарки, фазинг і детектор гонитв¶
Три інструменти, що постачаються з go test і знаходять те, чого
звичайний тест не знайде: наскільки швидкий код, вхідні дані, про які
ви й не думали, і баги конкурентності, що трапляються лише інколи.
Бенчмарки¶
Бенчмарк — це func BenchmarkXxx(b *testing.B) у файлі _test.go.
b.Loop() виконує тіло достатню кількість разів, щоб отримати стабільне
вимірювання — скільки саме, вирішує фреймворк.
-run XXX не збігається з жодним тестом, тож запускаються лише
бенчмарки. -bench приймає regex; . означає "усі".
BenchmarkConcatPlus-12 37771 31376 ns/op 265138 B/op 499 allocs/op
BenchmarkConcatBuilder-12 506193 2366 ns/op 3320 B/op 9 allocs/op
Читання рядка: -12 — це GOMAXPROCS, далі кількість ітерацій, далі
наносекунди на операцію, далі — з -benchmem — байти й кількість
виділень пам'яті (allocations) на операцію.
Це те саме застереження про += у циклі зі
статті про оператори, тільки
виміряне: версія зі strings.Builder приблизно в тринадцять разів
швидша й робить дев'ять виділень пам'яті замість 499. allocs/op часто
є найбільш дієвим числом, бо саме кількість виділень визначає
навантаження на збирач сміття.
Тримайте налаштування поза вимірюванням¶
func BenchmarkWithSetup(b *testing.B) {
data := strings.Repeat("x", 1000)
b.ResetTimer()
for b.Loop() {
_ = strings.ToUpper(data)
}
}
b.ResetTimer() відкидає все виміряне досі. Також є
b.StopTimer()/b.StartTimer() для налаштування на кожній ітерації,
хоча це достатньо повільно, тож зазвичай краще перебудувати код.
Не давайте компілятору видалити вашу роботу¶
Якщо результат не використовується, оптимізатор може повністю прибрати виклик, і ви виміряєте порожній цикл. Присвоюйте пакетній змінній:
b.Loop() стійкіший до цього за старішу форму
for i := 0; i < b.N; i++, але sink лишається надійною звичкою.
Бенчмарк, що показує неправдоподібно мало наносекунд на операцію,
зазвичай оптимізовано геть.
Порівнюйте чесно¶
Один прогін — це шум. Числа бенчмарків рухаються разом із троттлінгом
процесора, іншими процесами й станом кешу. Робіть кілька прогонів
(-count=10) і порівнюйте через benchstat, який повідомляє, чи є
різниця статистично значущою. Вимірюйте зміну на тій самій машині в
тій самій сесії; ніколи не порівнюйте числа з двох різних машин.
Фазинг¶
Ціль для фазингу — це func FuzzXxx(f *testing.F). Ви надаєте
насіннєві вхідні дані (seed inputs) і властивість; час виконання
генерує мутації, шукаючи ту, що її ламає:
func FuzzParseKV(f *testing.F) {
f.Add("a=b")
f.Add("noequals")
f.Add("=")
f.Fuzz(func(t *testing.T, s string) {
k, v, ok := ParseKV(s)
if ok && k+"="+v != s {
t.Fatalf("round trip failed for %q: %q %q", s, k, v)
}
})
}
Без -fuzz ціль все одно виконується як звичайний тест над насіннєвим
корпусом, тож цілі для фазингу корисні в CI, навіть коли ви не фазите.
Фазити можна лише одну ціль за раз.
Що він знаходить і що записує¶
Наведіть його на функцію з неперевіреним припущенням — і він знайде вхідні дані за мілісекунди:
fuzz: minimizing 27-byte failing input file
--- FAIL: FuzzFirstByte (0.03s)
testing.go:2076: panic: runtime error: index out of range [0] with length 0
Failing input written to testdata/fuzz/FuzzFirstByte/5838cdfae7b16cde
Варто помітити дві речі. Він спершу мінімізував вхідні дані — 27
байтів, на яких він спіткнувся, звелися до найменшого, що досі
провалюється. І він записав випадок у testdata/:
Закомітьте цей файл. Він стає частиною насіннєвого корпусу, тож баг
перетворюється на постійний регресійний тест, який виконується при
кожному go test, без потреби в жодному фазингу.
Що робить властивість гарною¶
Твердження не може звучати як "вивід дорівнює X", бо ви не знаєте вхідних даних. Корисні форми:
- Не панікує — часто цього достатньо самого по собі для парсера.
- Проходить туди й назад —
decode(encode(x)) == x. - Збігається з повільнішою, очевидно правильною реалізацією.
- Дотримуються інваріанти — вивід відсортований, довжина збережена.
Фазинг підходить для будь-чого, що приймає ненадійні байти: парсерів, декодерів, валідаторів, усього, що обробляє тіло запиту.
Детектор гонитв¶
-race інструментує доступ до пам'яті й повідомляє про
несинхронізований конкурентний доступ:
WARNING: DATA RACE
Read at 0x00c0000122c8 by goroutine 19:
r_test.go:20 +0x68
Previous write at 0x00c0000122c8 by goroutine 8:
r_test.go:20 +0x78
Показуються обидва стеки викликів, що зазвичай робить баг очевидним.
Тут це counter++ зі ста горутин — інкремент є читанням і записом, а
не атомарною операцією, тож губить оновлення.
Три речі, які варто зрозуміти про нього:
Він повідомляє лише про гонитви, які дійсно сталися. Це не
статичний аналіз. Гонитва на шляху, яким ваш тест ніколи не проходить,
невидима, тому запуск усього набору тестів під -race важливіший за
запуск одного тесту.
Він коштує. Приблизно в п'ять-десять разів повільніше і значно більше пам'яті. Запускайте його в CI на кожній збірці; локально — коли торкаєтесь конкурентності.
У нього немає хибних спрацювань. Якщо -race про щось повідомляє
— це справжній баг. Не додавайте м'ютекс лише щоб його заглушити —
спочатку зрозумійте, що саме є спільним.
-race також ловить несинхронізований доступ до мапи, який інакше
проявляється як убивство процесу середовищем виконання з повідомленням
"concurrent map writes" у непередбачуваний момент.
З досвіду Python: бенчмарки — це
timeit, вбудований у раннер тестів, із доданими підрахунками виділень пам'яті. Фазинг — це Hypothesis, але заснований на мутаціях, а не на стратегіях, і він автоматично зберігає провали на диск як регресійні тести. У детектора гонитв немає аналога в Python — через GIL більшість цих багів там статися не можуть, а в Go можуть, ще й як.
Швидка довідка¶
| Задача | Форма |
|---|---|
| бенчмарк | func BenchmarkXxx(b *testing.B), for b.Loop() |
| запустити їх | go test -run XXX -bench . -benchmem ./... |
| виключити налаштування | b.ResetTimer() |
| уникнути видалення оптимізатором | присвоєння пакетному sink |
| порівняти прогони | -count=10 плюс benchstat |
| ціль для фазингу | func FuzzXxx(f *testing.F), f.Add, f.Fuzz |
| фазити її | go test -run XXX -fuzz FuzzXxx -fuzztime 30s |
| знайдений провал | записаний у testdata/fuzz/... — закомітьте його |
| гарні властивості | без паніки, туди й назад, інваріанти |
| знайти гонитви даних | go test -race ./... у CI |