Quando vengono eseguiti calcoli limitati della larghezza di banda della memoria in ambienti di memoria condivisa (ad esempio threading tramite OpenMP, Pthreads o TBB), esiste un dilemma su come garantire che la memoria sia distribuita correttamente nella memoria fisica , in modo tale che ciascun thread acceda principalmente alla memoria su un bus di memoria "locale". Sebbene le interfacce non siano portatili, la maggior parte dei sistemi operativi ha modi per impostare l'affinità dei thread (ad esempio pthread_setaffinity_np()
su molti sistemi POSIX, sched_setaffinity()
su Linux, SetThreadAffinityMask()
su Windows). Esistono anche librerie come hwloc per determinare la gerarchia di memoria, ma sfortunatamente la maggior parte dei sistemi operativi non fornisce ancora modi per impostare criteri di memoria NUMA. Linux è un'eccezione notevole, con libnumaconsentendo all'applicazione di manipolare i criteri di memoria e la migrazione delle pagine con granularità delle pagine (in linea di massima dal 2004, quindi ampiamente disponibili). Altri sistemi operativi prevedono che gli utenti osservino una politica implicita di "primo tocco".
Lavorare con una politica di "primo tocco" significa che il chiamante dovrebbe creare e distribuire thread con qualsiasi affinità che intendono utilizzare in seguito quando scrivono per la prima volta nella memoria appena allocata. (Pochissimi sistemi sono configurati in modo tale da malloc()
trovare effettivamente le pagine, promette solo di trovarle quando sono effettivamente guaste, forse da thread diversi.) Ciò implica che l'allocazione che utilizza calloc()
o che inizializza immediatamente la memoria dopo l'utilizzo di allocazione memset()
è dannosa poiché tenderà a guastarsi tutta la memoria sul bus di memoria del core che esegue il thread di allocazione, portando alla larghezza di banda di memoria nel caso peggiore quando si accede alla memoria da più thread. Lo stesso vale per l' new
operatore C ++ che insiste sull'inizializzazione di molte nuove allocazioni (ad esstd::complex
). Alcune osservazioni su questo ambiente:
- L'allocazione può essere resa "thread collettiva", ma ora l'allocazione viene mescolata nel modello di threading, il che è indesiderabile per le librerie che potrebbero dover interagire con i client utilizzando diversi modelli di threading (forse ognuno con i propri pool di thread).
- RAII è considerata una parte importante del C ++ idiomatico, ma sembra essere attivamente dannosa per le prestazioni della memoria in un ambiente NUMA. Il posizionamento
new
può essere utilizzato con memoria allocata tramitemalloc()
o da routinelibnuma
, ma ciò modifica il processo di allocazione (che credo sia necessario). - EDIT: La mia precedente dichiarazione sull'operatore
new
non era corretta, può supportare più argomenti, vedi la risposta di Chetan. Credo che ci sia ancora la preoccupazione di far sì che le librerie o i contenitori STL utilizzino l'affinità specificata. Più campi possono essere impacchettati e può essere scomodo garantire, ad esempio, unastd::vector
riallocazione con il gestore del contesto corretto attivo. - Ogni thread può allocare e criticare la propria memoria privata, ma l'indicizzazione nelle regioni vicine è più complicata. (Considera un prodotto vettoriale a matrice sparsa con una partizione di riga della matrice e dei vettori; l'indicizzazione della parte non posseduta di richiede una struttura di dati più complicata quando non è contiguo nella memoria virtuale.)
Qualche soluzione all'allocazione / inizializzazione NUMA è considerata idiomatica? Ho lasciato fuori altri aspetti critici?
(Non voglio dire per la mia C esempi ++ implicare l'accento su quel linguaggio, ma il C ++ linguaggio di codifica per alcune decisioni sulla gestione della memoria che un linguaggio come C non, quindi non vi tende ad essere più resistenza quando suggerendo che i programmatori C ++ fanno quelli le cose diversamente.)