GoSuda

Il tempo di una tazza di tè verde che il runtime di Go non ci sottrarrà più: GreenTea GC

By snowmerak views ...

Perché esiste un altro GC?

Perché il GC attuale

Il GC attuale di Go può essere descritto con le seguenti caratteristiche.

  • Concurrent tri-color mark and sweep
    • Utilizza complessivamente tre colori (bianco, grigio, nero) per tracciare lo stato degli oggetti.
      • Bianco: oggetto non ancora visitato
      • Grigio: oggetto visitato, ma i cui oggetti figli non sono stati tutti visitati
      • Nero: oggetto i cui oggetti figli sono stati tutti visitati
      • Al termine della visita, tutti gli oggetti bianchi vengono raccolti.
    • Utilizza una Write Barrier per proteggere la memoria allocata di recente durante il GC.
      • Il tempo necessario per attivare e disattivare la Write Barrier costituisce gran parte 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, per gli oggetti di dimensione inferiore a 32KB, l'allocatore di memoria di Go sfrutta la cache per-P per ridurre al minimo la frammentazione.
  • Non-Generational
    • Il GC di Go non gestisce gli oggetti per generazioni.
    • Tutti gli oggetti appartengono alla stessa generazione.
  • Escape Analysis
    • Go determina tramite la Escape Analysis se un oggetto viene allocato sull'heap o sullo stack.
    • In linea di massima, si può considerare che un oggetto venga allocato sull'heap se viene referenziato da un puntatore pendente o se si utilizza un'interfaccia.

Ciò che conta

Il GC di Go esegue il mark a tre colori attraversando tutti gli oggetti a partire dalla radice. Questo processo può essere riassunto come "algoritmo di attraversamento concorrente di un grafo con ordinamento topologico". Tuttavia, è molto probabile che ciascun oggetto si trovi in un'area di memoria diversa. In parole semplici,

  1. Supponiamo che vi siano due aree di memoria distanti circa 32MB l'una dall'altra.
  2. In ciascuna di queste due aree di memoria sono allocati cinque oggetti.
    1. Gli oggetti A, B e C si trovano nell'area 1
    2. Gli oggetti D e E si trovano nell'area 2
  3. L'ordine dei riferimenti tra gli oggetti è A -> D -> B -> E -> C.
  4. Quando il GC viene avviato, visita A, poi D, poi B, poi E e infine C.
  5. In questo caso, poiché A e D si trovano in aree di memoria diverse, visitare A e poi D comporta uno spostamento tra aree di memoria.
  6. Questo processo genera il cosiddetto costo di salto di memoria, come lo spostamento tra aree di memoria e il riempimento di nuove cache line. Una parte considerevole del tempo di CPU dell'applicazione può essere assorbita da queste operazioni del GC.
  7. Questo costo continua a manifestarsi per tutta la durata del GC.

Quindi qual è il problema?

Il comportamento del GC descritto sopra diventa un serio problema nei seguenti casi.

  • Quando il numero di core è elevato e la memoria è di grandi dimensioni
    • 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 esplorano oggetti in spazi di memoria diversi contemporaneamente.
    • In questo processo, se il bus tra CPU e memoria non è sufficientemente ampio, la larghezza di banda della memoria può diventare un collo di bottiglia.
    • Inoltre, poiché la distanza fisica è reale, ogni singola esplorazione può introdurre un ritardo relativamente elevato.
  • Quando sono presenti molti oggetti di piccole dimensioni
    • Se si gestiscono in Go alberi o grafi con profondità elevata o con molti figli, il GC deve esplorare tutti questi oggetti.
    • In questo processo, se gli oggetti sono distribuiti su aree di memoria diverse, il ritardo si verifica per la prima ragione descritta.
    • Inoltre, poiché in presenza di molti oggetti di piccole dimensioni il GC deve esplorarli tutti, i core della CPU dedicheranno una capacità considerevole al GC durante la sua esecuzione.

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

Non c'è più tempo per un altro tè verde

Cosa sottrae il tè verde

La località della memoria sembra essere l'area in cui il team ha individuato la soluzione più rapida applicabile al GC esistente. In altre parole, si tratta di far sì che gli oggetti siano posizionati vicini tra loro, così da minimizzare i salti di memoria. Tuttavia, non è possibile imporre ai programmatori che scrivono il codice un determinato schema, e l'allocazione degli oggetti non è prevedibile in base al flusso di lavoro.

Forse per questo, il metodo scelto dal team di Golang è la Memory Span.

Cos'è una Memory Span?

Una Memory Span è un'area di memoria relativamente grande, ottenuta e utilizzata per allocare oggetti di piccole dimensioni. Il nuovo GC, denominato GreenTea GC, esegue la raccolta dei rifiuti su queste Memory Span. I dettagli operativi sono quasi identici al tradizionale Tri-Color Mark and Sweep.

Funzionamento del GreenTea GC

Innanzitutto, il GreenTea GC alloca una Memory Span. La dimensione è di 8KiB, come descritto in precedenza, una dimensione sufficientemente ampia. All'interno di essa possono essere allocati oggetti di dimensione massima pari a 512 byte. È difficile che la dimensione dei nodi di alberi o grafi come quelli dell'esempio, o delle strutture che generalmente vengono spostate sull'heap, superi questo valore. Gli oggetti di dimensione inferiore o uguale a 512 byte vengono accumulati in questa Memory Span ogni volta che vengono spostati sull'heap e, quando la Memory Span si riempie, ne viene allocata una nuova.

Quando si verifica un GC, il GreenTea GC accoda queste Memory Span e le ispeziona in sequenza. In questo processo, il GreenTea GC utilizza uno scheduling quasi simile al modello GMP esistente. È inoltre implementata un'area per il work stealing. In ogni caso, il worker che estrae una Memory Span dalla coda ispeziona gli oggetti interni alla Memory Span a lui assegnata. In questo processo viene utilizzato allo stesso modo il Tri-Color Mark and Sweep.

Quali sono i vantaggi?

Considerando nel complesso, la differenza rispetto al GC precedente è una sola. L'unità in cui il GC esegue la raccolta in una sola volta passa dall'oggetto alla Memory Span. Per questo motivo, il GC presenta i seguenti vantaggi.

  • Località della memoria: poiché gli oggetti sono raggruppati in una Memory Span, durante l'esplorazione degli oggetti i salti di memoria sono ridotti al minimo. In altre parole, la cache della CPU può essere sfruttata al massimo.
  • Miglioramento delle prestazioni del GC: eseguendo il GC per unità di Memory Span, il GC funziona in modo più efficiente in ambienti multi-core.
  • Ottimizzazione dell'allocazione della memoria: poiché le Memory Span vengono allocate a dimensione fissa, l'overhead dell'allocazione di oggetti di piccole dimensioni si riduce e diminuisce la probabilità di frammentazione. Ciò aumenta l'efficienza della deallocazione della memoria.

In breve, d'ora in poi possiamo allocare oggetti di dimensione inferiore o uguale a 512 byte più spesso e con maggiore leggerezza.

Tuttavia, anche in questo caso esistono alcuni scenari favorevoli.

  • Strutture ad 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: flussi di lavoro con allocazione massiva di dati di piccole dimensioni
    • Numerosi piccoli oggetti generati dal parsing di JSON
    • Oggetti DAO derivati dai result set del database

In questi casi, il GreenTea GC può offrire prestazioni migliori rispetto al GC tradizionale. Tuttavia, non si applica a tutti i casi e, in particolare quando gli oggetti sono distribuiti su aree di memoria diverse, è ancora difficile aspettarsi miglioramenti prestazionali drastici.

Quindi

Il team di Golang sembra intenzionato a continuare a migliorare il GC anche nel lungo periodo. Questo GreenTea GC può essere considerato una modifica minore in un ambito ristretto e non sostituisce il GC esistente. Tuttavia, il GreenTea GC appare come un caso interessante che permette di intravedere i problemi affrontati o previsti dal team di Golang. È un problema sorprendentemente complesso, ma ho trovato apprezzabile anche il tentativo di risolverlo con l'aggiunta di un concetto relativamente semplice. Personalmente lo considero un caso interessante.

Il GreenTea GC è una funzionalità sperimentale introdotta a partire da Go 1.25. Può essere attivato utilizzando la variabile d'ambiente GOEXPERIMENT=greenteagc. Trattandosi di una funzionalità ancora sperimentale, è necessario un test approfondito prima di utilizzarla in un ambiente di produzione.

Riferimenti