Лукса на една чаша зелен чай, която Go рънтаймът ще отнеме: GreenTea GC

Защо отново нов GC?
Защо съществуващият GC
Текущият GC на Go може да се опише с следните понятия.
- concurrent tri-color mark and sweep
- Използва общо 3 цвята (бял, сив, черен) за проследяване на състоянието на обектите.
- Бял: обект, който все още не е посетен
- Сив: обект, който е посетен, но не са посетени всички негови дъщерни обекти
- Черен: обект, при който всички дъщерни обекти са посетени
- След приключване на обхождането всички бели обекти се събират.
- Използва Write Barrier, за да защити паметта, която се създава по време на GC.
- Времето, през което Write Barrier се включва и изключва, е значителна част от т.нар. Stop-The-World (по-долу STW).
- Този процес се извършва конкурентно.
- Използва общо 3 цвята (бял, сив, черен) за проследяване на състоянието на обектите.
- No Compaction
- GC на Go не извършва компактиране.
- Може да възникне фрагментация на паметта.
- Но обектите до 32 KB минимизират фрагментацията, като използват per-P кеша на алокатора на паметта (Allocator) на Go.
- Non-Generational
- GC на Go не управлява обектите по поколения.
- Всички обекти принадлежат към едно и също поколение.
- Escape Analysis
- Go определя чрез Escape Analysis дали обектът ще бъде разпределен в хийпа или в стека.
- Грубо казано, ако има висящи указатели или се използват интерфейси, може да се приеме, че обектът се разпределя в хийпа.
Важното е
Важното е, че GC на Go обхожда всички обекти, започвайки от корените, и извършва трицветно маркиране. Този процес може да се запише с един ред като конкурентен обход на ориентиран граф с топологична подредба. Но всеки обект вероятно се намира в различна област на паметта. Конкретно:
- Нека има две области на паметта, които са разположени на около 32 MB една от друга.
- В тези две области са разпределени по 5 обекта.
- Обекти A, B, C са в област 1
- Обекти D, E са в област 2
- Редът на препратките между обектите е
A -> D -> B -> E -> C. - Когато GC започне, той посещава A, после D, после B, после E и после C.
- При това, тъй като A и D се намират в различни области на паметта, при посещението на A и след това на D се налага преминаване между области на паметта.
- В този процес се получават т.нар. разходи за скок в паметта, тъй като се сменя областта на паметта и се зареждат нови cache линии. Значителна част от процесорното време на приложението може да бъде отделена за тази GC работа.
- Този разход продължава да се начислява през цялото време, докато GC работи.
Какъв е проблемът?
Това поведение на GC е фатално в следните случаи:
- Голям брой ядра и голяма памет
- GC на Go по принцип работи конкурентно.
- Но когато областта на паметта е обширна и обектите са разпръснати, множество ядра едновременно обхождат обекти в различни пространства на паметта.
- Ако шината между процесора и паметта не е достатъчно широка, пропускателната способност на паметта може да стане bottleneck.
- Освен това, поради физическото разстояние, всяко обхождане може да създаде сравнително голямо закъснение.
- Голям брой дребни обекти
- Ако използвате в Go дървета или графи с голяма дълбочина или с много дъщерни елементи, GC трябва да обходи всички тези обекти.
- Ако при това обектите са разпределени в различни области на паметта, закъснение възниква поради първата причина.
- Освен това, при голям брой дребни обекти GC трябва да обходи всички тях, което означава, че по време на работа на GC значителен капацитет на CPU ядрата ще бъде отделен за GC и за извършването на тази работа.
За да реши този проблем, екипът на Golang представи GreenTea GC.
Вече нямате време за още зелен чай
Какво отнема зеления чай
Изглежда, че най-бързото решение на съществуващия GC е да се приложи към локалността на паметта. Тоест обектите да се разполагат близо един до друг, за да се минимизират скоковете в паметта. Но не може да се наложи шаблон на програмистите, които пишат кода, а разпределението на обектите е непредсказуемо в зависимост от работния процес.
Може би затова избраният от екипа на Golang подход е Memory Span.
Какво е Memory Span?
Memory Span е място, където се получава сравнително голяма област от паметта и в нея се разпределят малки обекти. Новият GC, наречен GreenTea GC, извършва събиране на боклука върху тези Memory Span. Подробното поведение е почти същото като при съществуващия Tri-Color Mark and Sweep.
Работа на GreenTea GC
Първо GreenTea GC разпределя Memory Span. Размерът е, както беше описано по-рано, 8 KiB, което е доста голям размер. В него могат да се разпределят обекти с размер до 512 bytes. Размерът на възлите на дървото или графа, за който дадохме пример, или на типичните структури, които се преместват в хийпа, едва ли е по-голям от това. Обектите до 512 bytes се натрупват в този Memory Span при всяко преместване в хийпа, а когато Memory Span се запълни, се разпределя нов Memory Span.
Когато настъпи GC, GreenTea GC нарежда тези Memory Span в опашка и ги проверява последователно. При това GreenTea GC използва планиране, почти подобно на съществуващия GMP модел. Реализирани са и области като кражба на работа. Във всеки случай работникът, който е извлякъл Memory Span от опашката, проверява вътрешните обекти на назначения му Memory Span. При това се използва същият Tri-Color Mark and Sweep.
Каква е ползата?
В този процес основната разлика спрямо съществуващия GC е една: единицата, за която GC извършва работа наведнъж, е променена от обект на Memory Span. Поради това GC има следните предимства:
- Локалност на паметта: тъй като обектите са събрани в Memory Span, при обхождане на обектите скоковете в паметта са минимизирани. Тоест CPU cache може да се използва максимално.
- Подобрение на производителността на GC: като GC се извършва на ниво Memory Span, той работи по-ефективно в многоядрена среда.
- Оптимизация на разпределението на паметта: тъй като Memory Span се разпределя с фиксиран размер, при разпределяне на малки обекти режийните разходи за алокация намаляват и вероятността за фрагментация се понижава. Това повишава ефективността на освобождаването на паметта.
Накратко, отсега нататък можем да разпределяме обекти до 512 bytes по-често и с по-лека душа.
Все пак и тук има определени благоприятни сценарии.
- Структури тип дърво/граф: при висока fan-out и когато промените са редки
- B+ дървета, Trie, AST (Abstract Syntax Tree)
- Структури от данни, дружелюбни към cache
- Пакетна обработка: работни процеси с масово разпределяне на малки данни
- Множество малки обекти, създадени при JSON парсване
- DAO обекти от резултатни набори на база данни
В тези случаи GreenTea GC може да покаже по-добра производителност от съществуващия GC. Въпреки това не се прилага във всички случаи и особено когато обектите са разпределени в различни области на паметта, все още не може да се очаква драматично подобрение на производителността.
Така че
Екипът на Golang вероятно има намерение да продължи да подобрява GC в дългосрочен план. Този GreenTea GC може да се разглежда като дребна промяна в малка област и не замества съществуващия GC. Въпреки това GreenTea GC е интересен пример, който дава представа за проблемите, с които екипът на Golang се сблъсква или очаква. Макар проблемът да е доста сложен, опитът да бъде решен чрез добавяне на сравнително проста концепция също беше впечатляващ. Лично аз смятам, че това е интересен пример.
GreenTea GC е експериментална функция, която се въвежда от Go 1.25. Може да се активира чрез използване на променливата на средата GOEXPERIMENT=greenteagc. Тъй като функцията все още е експериментална, преди използване в производствена среда е необходимо достатъчно тестване.