GoSuda

GreenTea GC: Un momento di pace con il tè verde sottratto dal runtime Go

By snowmerak
views ...

Perché un nuovo GC?

Perché il GC esistente?

L'attuale GC di Go può essere descritto con le seguenti espressioni:

  • concurrent tri-color mark and sweep
    • Utilizza tre colori (bianco, grigio, nero) per tracciare lo stato degli oggetti.
      • Bianco: oggetti non ancora visitati
      • Grigio: oggetti visitati ma con figli non tutti visitati
      • Nero: oggetti con tutti i figli visitati
      • Al termine della scansione, tutti gli oggetti bianchi vengono raccolti.
    • Utilizza una Write Barrier per proteggere la memoria appena creata durante il GC.
      • Il tempo impiegato per attivare/disattivare la Write Barrier costituisce una parte significativa del cosiddetto Stop-The-World (di seguito STW).
    • Questo processo viene eseguito in modo concorrente.
  • No Compaction
    • Il GC di Go non esegue la compattazione.
    • Può verificarsi frammentazione della memoria.
      • Tuttavia, gli oggetti inferiori a 32 KB minimizzano la frammentazione utilizzando la cache per-P dell'allocatore di memoria del linguaggio Go.
  • Non-Generational
    • Il GC di Go non gestisce gli oggetti per generazione.
    • Tutti gli oggetti appartengono alla stessa generazione.
  • Escape Analysis
    • Go determina se un oggetto viene allocato sull'heap o sullo stack tramite l'Escape Analysis.
    • In generale, se si utilizzano puntatori dangling o interfacce, si può considerare che l'oggetto venga allocato sull'heap.

L'aspetto cruciale

Il GC di Go esplora tutti gli oggetti dalla radice ed esegue la marcatura a tre colori. Questo processo può essere descritto in una riga come un "algoritmo di esplorazione concorrente di grafi con ordinamento topologico". Tuttavia, è altamente probabile che ogni oggetto esista in aree di memoria diverse. In termini semplici:

  1. Si considerino due aree di memoria distanti circa 32 MB.
  2. In queste due aree di memoria sono allocati 5 oggetti in ciascuna.
    1. Gli oggetti A, B, C si trovano nell'area 1.
    2. Gli oggetti D, E si trovano nell'area 2.
  3. L'ordine di riferimento degli oggetti è A -> D -> B -> E -> C.
  4. Quando il GC inizia, visita A, poi D, poi B, poi E, e infine C.
  5. In questo processo, poiché A e D esistono in aree di memoria diverse, è necessario spostarsi tra le aree di memoria per passare da A a D.
  6. Questo processo comporta un costo di "salto di memoria", ad esempio lo spostamento tra aree di memoria e il riempimento di nuove cache line. Una parte significativa del tempo CPU utilizzato dall'applicazione può essere allocata a queste operazioni di GC.
  7. Questo costo si verifica continuamente durante l'esecuzione del GC.

Qual è il problema allora?

Il comportamento di questo GC è deleterio nei seguenti casi:

  • Elevato numero di core e ampia memoria
    • Il GC di Go viene eseguito in modo concorrente per impostazione predefinita.
    • Tuttavia, se l'area di memoria è ampia e gli oggetti sono distribuiti, più core esploreranno contemporaneamente gli oggetti in diversi spazi di memoria.
    • In questo processo, se il bus tra la CPU e la memoria non è sufficientemente ampio, la larghezza di banda della memoria può diventare un collo di bottiglia.
    • Inoltre, poiché vi è una distanza fisica, ogni esplorazione comporterà un ritardo relativamente grande.
  • Numerosi oggetti di piccole dimensioni
    • Se si gestiscono alberi o grafi con elevata profondità o molti figli in Go, il GC deve esplorare tutti questi oggetti.
    • In questo processo, se gli oggetti sono distribuiti in aree di memoria diverse, si verificherà un ritardo a causa del primo motivo.
    • Inoltre, se ci sono molti oggetti di piccole dimensioni, il GC deve esplorare tutti questi oggetti, quindi durante l'esecuzione del GC, i core della CPU allocheranno una notevole quantità di risorse al GC e svolgeranno il lavoro.

Per risolvere questi problemi, il team di Golang ha presentato il GreenTea GC.

Non puoi più permetterti di bere altro tè verde

Cosa ti priva del tè verde

L'area vista come la soluzione più rapida per il GC esistente è stata la località della memoria. Cioè, minimizzare i salti di memoria posizionando gli oggetti vicini tra loro. Tuttavia, non è possibile imporre i modelli dei programmatori che scrivono il codice, e l'allocazione degli oggetti è imprevedibile a seconda del flusso di lavoro.

Per questo motivo, il metodo scelto dal team di Golang è lo Memory Span.

Cos'è uno Memory Span?

Uno Memory Span è un'area di memoria relativamente grande allocata per l'allocazione di oggetti di piccole dimensioni. E il nuovo GC, chiamato GreenTea GC, esegue la garbage collection su questi Memory Span. Il funzionamento dettagliato è quasi identico al Tri-Color Mark and Sweep esistente.

Funzionamento del GreenTea GC

Innanzitutto, il GreenTea GC alloca uno Memory Span. La dimensione, come descritto in precedenza, è di 8 KiB, una dimensione considerevole. E al suo interno possono essere allocati oggetti con una dimensione massima di 512 byte. Questa dimensione è tale che è difficile che la dimensione dei nodi di un albero o di un grafo, o la dimensione delle struct che "scappano" nell'heap, sia maggiore di questa. Gli oggetti inferiori a 512 byte si accumulano in questo Memory Span ogni volta che "scappano" nell'heap, e quando questo Memory Span si riempie, viene allocato un nuovo Memory Span.

Ora, quando si verifica un GC, il GreenTea GC accoda questi Memory Span per esaminarli sequenzialmente. In questo processo, il GreenTea GC utilizza una pianificazione quasi identica al modello GMP esistente. Anche aree come il work stealing sono implementate. Comunque, un worker che estrae uno Memory Span dalla coda esamina gli oggetti interni allo Memory Span che gli è stato allocato. In questo processo, il Tri-Color Mark and Sweep viene utilizzato in modo identico.

Quali sono i vantaggi?

La differenza principale rispetto al GC esistente è una sola: l'unità su cui viene eseguito il GC è passata dall'oggetto allo Memory Span. Di conseguenza, il GC presenta i seguenti vantaggi:

  • Località della memoria: Poiché gli oggetti sono raggruppati nello Memory Span, i salti di memoria sono minimizzati durante l'esplorazione degli oggetti. Ciò significa che la cache della CPU può essere utilizzata al massimo.
  • Miglioramento delle prestazioni del GC: Eseguendo il GC a livello di Memory Span, il GC funziona in modo più efficiente in ambienti multi-core.
  • Ottimizzazione dell'allocazione della memoria: Poiché lo Memory Span è allocato a dimensione fissa, l'overhead di allocazione della memoria viene ridotto quando si allocano oggetti di piccole dimensioni e la possibilità di frammentazione diminuisce. Ciò aumenta l'efficienza della deallocazione della memoria.

In breve, ora possiamo allocare oggetti inferiori a 512 byte più frequentemente e con maggiore leggerezza.

Tuttavia, ci sono anche scenari più vantaggiosi:

  • Strutture a albero/grafo: quando il fan-out è elevato e le modifiche sono rare.
    • B+ tree, Trie, AST (Abstract Syntax Tree)
    • Strutture dati cache-friendly
  • Elaborazione batch: workflow di allocazione massiva di piccoli dati
    • Molti piccoli oggetti generati dal parsing JSON
    • Oggetti DAO dai set di risultati del database

In questi casi, il GreenTea GC può offrire prestazioni migliori rispetto al GC esistente. Tuttavia, non si applica a tutti i casi, e in particolare quando gli oggetti sono distribuiti in diverse aree di memoria, è ancora difficile aspettarsi un miglioramento drammatico delle prestazioni.

Conclusione

Sembra che il team di Golang intenda continuare a migliorare il GC a lungo termine. Questo GreenTea GC può essere considerato un cambiamento minore in un'area ristretta e non sostituisce il GC esistente. Tuttavia, il GreenTea GC sembra essere un caso interessante che permette di intravedere i problemi che il team di Golang sta affrontando o prevede. Nonostante la complessità del problema, il tentativo di risolverlo con l'aggiunta di un concetto relativamente semplice è stato impressionante. Personalmente, lo considero un caso interessante.

Il GreenTea GC è una funzionalità sperimentale introdotta a partire da Go 1.25. Può essere abilitato utilizzando la variabile d'ambiente GOEXPERIMENT=greenteagc. Poiché questa funzionalità è ancora sperimentale, sono necessari test approfonditi prima di utilizzarla in un ambiente di produzione.

Riferimenti