Frage

Ich arbeite an einer benutzerdefinierten mark-release Stil arbeitsspeicherzuweisung für die D-Programmiersprache, die arbeitet, durch die Zuteilung von thread-lokalen Regionen.Es scheint, dass die lokalen thread-Speicher-Engpass verursacht eine große (~50%) Verlangsamung bei der Zuweisung von Speicher aus diesen Regionen im Vergleich zu einer ansonsten identischen Singlethread-version der code auch nach der Gestaltung mein code, um nur eine TLS-lookup pro allocation/deallocation.Dies basiert auf der Zuweisung/Freigabe von Speicher eine große Anzahl von malen in einer Schleife, und ich versuche herauszufinden, wenn es ein Artefakt meines benchmarking-Methode.Mein Verständnis ist, dass die thread-lokalen Speicher sollte im Grunde nur Zugriff auf etwas, was durch eine zusätzliche Dereferenzierung, ähnlich wie der Zugriff auf eine variable über einen Zeiger.Ist das falsch?Wie viel Aufwand bedeutet thread-local storage in der Regel haben?

Hinweis:Obwohl ich schon erwähnt, D, ich bin auch interessiert im Allgemeinen Antworten, dass sind nicht spezifisch für D, da D die Umsetzung des lokalen thread-Speicher wird wahrscheinlich steigen, wenn es ist langsamer als die besten Umsetzungen.

War es hilfreich?

Lösung

Die Geschwindigkeit ist abhängig von der TLS-Implementierung.

Ja, Sie sind richtig, dass TLS so schnell wie ein Zeiger Lookup sein kann. Es kann sogar sein, schneller auf Systeme mit einer Speicherverwaltungseinheit.

Für die Zeiger-Lookup Sie aus dem Scheduler obwohl die Hilfe benötigen. Der Planer muss - bei einem Taskwechsel - Aktualisiere den Zeiger auf die TLS-Daten.

Eine weitere schnelle Möglichkeit, TLS zu implementieren, ist über das Memory Management Unit. Hier wird die TLS ist wie jede andere Daten mit der Ausnahme behandelt, daß TLS Variablen in einem speziellen Segment zugeordnet sind. Der Scheduler wird - auf Task-Switch - Karte den richtigen Teil des Speichers in den Adressraum der Aufgabe.

Wenn der Planer eine dieser Methoden nicht unterstützt, der Compiler / Bibliothek hat Folgendes zu tun:

  • erhalten aktuelle ThreadId
  • Nehmen Sie eine Semaphore
  • Nachschlagen die Zeiger auf den TLS-Block durch die ThreadId (kann eine Karte verwenden oder so)
  • Lassen Sie die Semaphore
  • Zurück diesen Zeiger.

Natürlich alles tun für jeden Zugriff TLS-Daten eine Weile dauert, und kann bis zu drei OS Anrufe benötigen. Die ThreadId bekommen, nehmen und die Semaphore loslassen

Die Semaphore btw benötigt kein Thread liest aus dem TLS-Zeigerliste, um sicherzustellen, während ein anderer Thread in der Mitte ist ein neues Thema von Laichen. (Und als solche einen neuen TLS Block zuweisen und modifizieren, um die Datenstruktur).

Leider ist es nicht ungewöhnlich, dass die langsame TLS-Implementierung in der Praxis zu sehen.

Andere Tipps

Thread einheimischen in D sind richtig schnell.Hier sind meine tests.

64 bit-Ubuntu, core i5, dmd v2.052 Compiler-Optionen: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;
    }
}

So verlieren wir nur 1,2 Sekunden einer der CPU-Kerne pro 1000*1000*1000 thread-lokale Zugriffe.Thread einheimischen Zugriff über %fs register - es gibt also nur ein paar von Prozessor Befehlen, die beteiligt sind:

Demontage mit objdump -d:

- 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>

Vielleicht compiler könnte noch mehr clevere und cache-thread-lokalen vor-Schleife, um ein register und senden es zu lokalen thread am Ende (es ist interessant zu vergleichen mit gdc compiler), aber auch jetzt noch Fragen sind sehr gut, IMHO.

muss man bei der Interpretation von Benchmark-Ergebnisse sehr vorsichtig sein. Zum Beispiel ergab ein kürzlich Thread in der D-Newsgroup von einer Benchmark, dass die Codegenerierung der DMD eine große Verlangsamung in einer Schleife verursacht wurde, die arithmetische tat, aber in Wirklichkeit verbrachte die Zeit von der Laufzeithilfsfunktion beherrscht wurde, die lange Teilung tat. Der Code-Generierung des Compilers hatte nichts mit der Verlangsamung zu tun.

Um zu sehen, welche Art von Code für tls erzeugt, kompiliert und obj2asm diesen Code:

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

TLS ist sehr unterschiedlich auf Windows implementiert als auf Linux, und wird sehr unterschiedlich sein wieder auf OSX. Aber in allen Fällen wird es viele weitere Anweisungen als eine einfache Last eines statischen Speicherstelle sein. TLS ist immer langsam in Bezug auf den einfachen Zugriff sein würde. Zugriff auf TLS Globals in einer engen Schleife geht langsam, zu sein. Versuchen Sie Caching der TLS-Wert in einem temporären statt.

Ich schrieb einige Thread-Pool Allocation Code Jahren, und im Cache der TLS zum Pool behandeln, die gut funktioniert.

Wenn Sie nicht Compiler TLS-Unterstützung verwenden können, können Sie TLS selbst verwalten. Ich baute eine Wrapper-Vorlage für C ++, so ist es einfach, eine zugrunde liegende Implementierung zu ersetzen. In diesem Beispiel habe ich es für Win32 implementiert. Hinweis: Da Sie können eine unbegrenzte Anzahl von TLS Indizes pro Prozess (zumindest unter Win32) erhalten, Sie sollten Blöcke Haufen groß genug, um darauf alle Thread spezifischen Daten zu halten. Auf diese Weise haben Sie eine minimale Anzahl von TLS-Indizes und der damit verbundenen Abfragen. Im „besten Fall“, dann würden Sie nur 1 TLS Zeiger, der auf einen privaten Heapblock pro Thread haben.

Auf den Punkt gebracht:. Sie nicht auf einzelne Objekte verweisen, statt spezifische, Heap-Speicher / Behältnissen mit einem Objektzeiger auf Einfädelpunkt eine bessere Leistung zu erzielen

Vergessen Sie nicht, Speicher zu befreien, wenn es nicht wieder verwendet wird. Ich tue dies durch einen Faden in eine Klasse Verpackung (wie Java tut) und Griff TLS und Destruktor. Des Weiteren Speicher, tue ich häufig verwendete Daten wie Thread-Handles und IDs als Klassenmitglieder.

Nutzung:

  

für Typ *:   tl_ptr

     

für const Typ *:   tl_ptr

     

für Typ * const:   const tl_ptr

     

const Typ * 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);
    }
};

Ich habe für Embedded-Systeme Multi-Tasker entwickelt und konzeptionell die wichtigste Voraussetzung für die Thread-Local Storage die Kontextschalter Verfahren mit Speichern / Wiederherstellen einen Zeigers auf Thread-lokale Speicher zusammen mit den CPU-Registern und was sonst es sparend / Wiederherstellung. Für Embedded-Systeme, die den gleichen Satz von Code werden immer ausgeführt, sobald sie habe in Betrieb genommen, ist es am einfachsten einen Zeiger einfach speichern / wiederherstellen, die für jeden Thread zu einem Fest Format Block verweist. Schön, sauber, einfach und effizient.

Ein solcher Ansatz funktioniert gut, wenn man nicht für jeden Thread-lokale Variable in jedem Thread zugewiesen, die Raum hat nichts dagegen - auch diejenigen, die es nie wirklich nutzen - und wenn alles, was in dem Thread-lokalen Speicher sein wird Block kann als eine einzelne struct definiert werden. In diesem Szenario greift auf Thread-lokale Variablen können fast so schnell wie der Zugang zu anderen Variablen sein, der einzige Unterschied dereferenzieren ein zusätzlicher Zeiger zu sein. Leider sind viele PC-Anwendungen erfordern etwas komplizierter.

Bei einigen Frameworks für den PC, wird ein Thread nur Platz für gewinde statischen Variablen zugewiesen, wenn ein Modul, das diese Variablen verwendet hat auf diesem Thread ausgeführt wurde. Während dies manchmal von Vorteil sein kann, bedeutet dies, dass verschiedene Threads oft ihre lokalen Speicher unterschiedlich angelegt haben werden. Folglich kann es erforderlich sein, die Fäden eine Art von durchsuchbaren Index zu haben, wo ihre Variablen befinden, und zu leiten alle Zugriffe auf diese Variablen durch diesen Index.

Ich würde erwarten, dass, wenn der Rahmen eine geringe Menge an festes Format Speicher zuordnet, ist es hilfreich sein kann, einen Cache der letzten 1-3 Thread-lokalen Variablen zugegriffen zu halten, da in vielen Szenarien sogar ein Einzel Itemcache bieten könnte eine ziemlich hohe Trefferquote.

Wir haben ähnliche Performance-Probleme von TLS (unter Windows) gesehen. Wir verlassen uns auf sie für bestimmte kritische Operationen innerhalb unseres Produktes „Kernel‘. Nach einiger Anstrengung, die ich beschlossen, zu versuchen und auf diese zu verbessern.

Ich freue mich, zu sagen, dass wir jetzt eine kleine API haben, bietet> 50% ige Reduktion der CPU-Zeit für eine äquivalente Operation, wenn der Callin Thread nicht „kennt“ seinen Thread-ID und> 65% Rabatt an, wenn Aufruf Faden bereits seine gewinde ID erhalten (möglicherweise aus einem anderen frühen Verarbeitungsschritt).

Die neue Funktion (get_thread_private_ptr ()) gibt immer einen Zeiger auf eine Struktur wir intern verwenden, um alle Arten zu halten, so brauchen wir nur eine pro Thread.

Alles in allem denke ich, die Win32-TLS-Unterstützung schlecht wirklich in Handarbeit gemacht.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top