Domanda

Sto lavorando su un mark-rilascio allocatore di memoria stile personalizzato per il linguaggio di programmazione D che funziona assegnando dalle regioni filo-locale. Sembra che il collo di bottiglia memoria locale filetto porta un enorme rallentamento (~ 50%) in allocazione di memoria da queste regioni rispetto ad una versione filettata singola altrimenti identica del codice, anche dopo la progettazione codice esistente per avere un solo TLS ricerca per allocazione / deallocazione. Questo si basa sulla allocazione / liberazione della memoria di un gran numero di volte in un ciclo, e sto cercando di capire se si tratta di un manufatto del mio metodo di benchmarking. La mia comprensione è che memoria locale filo deve fondamentalmente solo comportare l'accesso a qualcosa attraverso un ulteriore livello di indirezione, simile a l'accesso a una variabile tramite un puntatore. È questo non è corretto? Quanto in alto fa archiviazione thread-locali hanno in genere?

. Nota: Anche se ho citato D, sono anche interessato a risposte generali che non sono specifici di D, in quanto D's implementazione di archiviazione thread-local probabilmente migliorare se è più lento rispetto ai migliori implementazioni

È stato utile?

Soluzione

La velocità dipende dall'implementazione TLS.

Sì, lei ha ragione che TLS può essere veloce come una ricerca puntatore. Può anche essere più veloce su sistemi con un'unità di gestione della memoria.

Per la ricerca puntatore avete bisogno di aiuto dal programmatore però. Lo scheduler deve - su uno switch compito - aggiorna il puntatore ai dati TLS.

Un altro modo veloce per implementare TLS è tramite la Memory Management Unit. Qui il TLS viene trattato come tutti gli altri dati con l'eccezione che le variabili TLS sono allocate in un segmento speciale. Lo scheduler sarà - il task switch - mappare la corretta pezzo di memoria nello spazio degli indirizzi del compito.

Se l'utilità di pianificazione non supporta uno di questi metodi, il compilatore / biblioteca ha a che fare il seguente:

  • ottenere corrente ThreadId
  • Prendere un semaforo
  • Lookup il puntatore al blocco TLS dal ThreadId (può usare una mappa o giù di lì)
  • Release il semaforo
  • Rientro tale puntatore.

Ovviamente facendo tutto questo per ogni accesso ai dati TLS prende un po 'e può richiedere fino a tre chiamate OS:. Ottenere il ThreadId, prendere e rilasciare il semaforo

Il semaforo è btw necessario per assicurarsi che nessuno thread legge dalla lista puntatore TLS mentre un altro thread è nel bel mezzo di deposizione delle uova un nuovo thread. (E come tale allocare un nuovo blocco TLS e modificare la datastructure).

Purtroppo non è raro vedere l'attuazione lenta TLS nella pratica.

Altri suggerimenti

i locali della discussione in D sono veramente veloce. Qui ci sono i miei test.

64 bit Ubuntu, nucleo i5, v2.052 dmd opzioni del compilatore: DMD -O -release -inline -m64

// this loop takes 0m0.630s
void main(){
    int a; // register allocated
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

// this loop takes 0m1.875s
int a; // thread local in D, not static
void main(){
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

Così si perde solo 1,2 secondi di uno dei core della CPU per 1000 * 1000 * 1000 del filo accessi locali. i locali della discussione sono accessibili tramite il registro% fs - quindi non c'è solo un paio di comandi processori coinvolti:

Smontaggio con -d objdump:

- this is local variable in %ecx register (loop counter in %eax):
   8:   31 c9                   xor    %ecx,%ecx
   a:   b8 00 ca 9a 3b          mov    $0x3b9aca00,%eax
   f:   83 c1 09                add    $0x9,%ecx
  12:   ff c8                   dec    %eax
  14:   85 c0                   test   %eax,%eax
  16:   75 f7                   jne    f <_Dmain+0xf>

- this is thread local, %fs register is used for indirection, %edx is loop counter:
   6:   ba 00 ca 9a 3b          mov    $0x3b9aca00,%edx
   b:   64 48 8b 04 25 00 00    mov    %fs:0x0,%rax
  12:   00 00 
  14:   48 8b 0d 00 00 00 00    mov    0x0(%rip),%rcx        # 1b <_Dmain+0x1b>
  1b:   83 04 08 09             addl   $0x9,(%rax,%rcx,1)
  1f:   ff ca                   dec    %edx
  21:   85 d2                   test   %edx,%edx
  23:   75 e6                   jne    b <_Dmain+0xb>

Forse compilatore potrebbe essere ancora più intelligente e filo cache locale prima di ciclo per un registro e restituirlo infilare locale alla fine (interessa da confrontare con compilatore GDC), ma anche ora le cose sono molto buone IMHO.

Uno ha bisogno di essere molto attenti nella interpretazione dei risultati dei benchmark. Ad esempio, un recente thread nel newsgroup D concluso da un punto di riferimento che la generazione del codice di DMD stava causando un forte rallentamento in un ciclo che faceva aritmetica, ma in realtà il tempo trascorso è stato dominato dalla funzione runtime di supporto che ha divisione lunga. la generazione di codice del compilatore non aveva niente a che fare con il rallentamento.

Per vedere che tipo di codice viene generato per TLS, compilare e obj2asm questo codice:

__thread int x;
int foo() { return x; }

TLS è implementato in modo molto diverso su Windows che su Linux, e sarà ancora una volta molto diversa su OSX. Ma, in tutti i casi, sarà molto più istruzioni di una semplice carico di una locazione di memoria statica. TLS sta andando sempre essere lento rispetto a un accesso semplice. Accesso globali TLS in un loop stretto sta per essere lento, troppo. Prova cache il valore TLS in una temporanea, invece.

ho scritto qualche anno pool di thread codice di allocazione fa, e memorizzata nella cache il TLS manico alla piscina, che ha funzionato bene.

Se non è possibile utilizzare il compilatore supporto TLS, è possibile gestire TLS te stesso. Ho costruito un modello involucro per C ++, quindi è facile da sostituire un'implementazione sottostante. In questo esempio, ho implementato per Win32. Nota: Dal momento che non è possibile ottenere un numero illimitato di indici TLS per ogni processo (almeno sotto Win32), è necessario indicare al heap blocchi abbastanza grande per contenere tutti i dati specifici di thread. In questo modo si dispone di un numero minimo di indici TLS e query correlate. Nel "caso migliore", si avrebbe solo 1 TLS puntatore punta a un isolato mucchio privato per thread.

In poche parole:. Non puntare a oggetti singoli, invece punto infilare specifici memoria mucchio / contenitori, in possesso di puntatori all'oggetto per ottenere prestazioni migliori

Non dimenticare di liberare la memoria se non viene utilizzato nuovamente. Faccio questo avvolgendo un filo in una classe (come Java fa) e gestire TLS dal costruttore e distruttore. Inoltre, posso conservare spesso usato i dati come le maniglie di thread e l'ID di come i membri della classe.

Utilizzo:

  

per il tipo di *:   tl_ptr

     

per il tipo const *:   tl_ptr

     

per il tipo const *:   const tl_ptr

     

tipo const * const:   const tl_ptr

template<typename T>
class tl_ptr {
protected:
    DWORD index;
public:
    tl_ptr(void) : index(TlsAlloc()){
        assert(index != TLS_OUT_OF_INDEXES);
        set(NULL);
    }
    void set(T* ptr){
        TlsSetValue(index,(LPVOID) ptr);
    }
    T* get(void)const {
        return (T*) TlsGetValue(index);
    }
    tl_ptr& operator=(T* ptr){
        set(ptr);
        return *this;
    }
    tl_ptr& operator=(const tl_ptr& other){
        set(other.get());
        return *this;
    }
    T& operator*(void)const{
        return *get();
    }
    T* operator->(void)const{
        return get();
    }
    ~tl_ptr(){
        TlsFree(index);
    }
};

Ho progettato multi-tasker per sistemi embedded, e concettualmente il requisito fondamentale per lo stoccaggio thread-local sta avendo il metodo cambio di contesto salvataggio / ripristino di un puntatore al thread-local di storage insieme ai registri della CPU e qualsiasi altra cosa è il risparmio / ripristino. Per sistemi embedded che sarà sempre in esecuzione lo stesso set di codice una volta che hanno cominciato in su, è più semplice per risparmiare semplicemente / ripristino un puntatore, che punta ad un blocco formato fisso per ogni thread. Bello, pulito, semplice ed efficiente.

Un simile approccio funziona bene se uno non dispiacerebbe avere spazio per tutte le variabili thread-locali assegnati nell'ambito di ogni filo - anche quelle che in realtà mai usarlo - e se tutto quello che sta andando ad essere entro l'archiviazione thread-local blocco può essere definito come una singola struct. In tale scenario, gli accessi alle variabili di thread-local può essere quasi veloce come l'accesso ad altre variabili, l'unica differenza è un puntatore dereference supplementare. Purtroppo, molte applicazioni per PC richiedono qualcosa di più complicato.

Su alcuni quadri per PC, un filo avrà solo spazio allocato per le variabili thread-statico se un modulo che utilizza tali variabili è stato eseguito su quel thread. Questa soluzione può essere vantaggioso, significa che diversi fili spesso hanno la loro memorizzazione locale strutturate in modo diverso. Di conseguenza, può essere necessario per i fili abbiano una sorta di indice di ricerca di cui si trovano loro variabili, e di indirizzare tutti gli accessi alle variabili attraverso tale indice.

mi aspetterei che se il quadro assegna una piccola quantità di storage a formato fisso, può essere utile per mantenere una cache degli ultimi 1-3 variabili thread-local accessibili, dal momento che in molti scenari, anche una cache singola voce potrebbe offrire un piuttosto alto tasso di successo.

Abbiamo visto i problemi di prestazioni simili da TLS (su Windows). Facciamo affidamento su di esso per alcune operazioni critiche all'interno del nostro prodotto "kernel'. Dopo un po' ho deciso sforzo per cercare di migliorare su questo.

Sono contento di dire che ora abbiamo una piccola API che offre una riduzione> 50% del tempo di CPU per un'operazione equivalente quando il thread Callin non "sa" il suo filo-id e la riduzione> 65% se chiamate filetto ha già ottenuto il filo-id (forse per qualche altra fase di lavorazione precedente).

La nuova funzione (get_thread_private_ptr ()) restituisce sempre un puntatore ad una struct che usiamo internamente per contenere tutti i tipi, quindi abbiamo solo bisogno uno per thread.

Tutto sommato penso che il supporto Win32 TLS è mal realizzato davvero.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top