Hat mein Compiler mein nicht verwendetes statisches thread_local-Klassenmitglied ignoriert?

10

Ich möchte eine Thread-Registrierung in meiner Klasse durchführen, daher entscheide ich mich, eine Überprüfung für die thread_localFunktion hinzuzufügen :

#include <iostream>
#include <thread>

class Foo {
 public:
  Foo() {
    std::cout << "Foo()" << std::endl;
  }
  ~Foo() {
    std::cout << "~Foo()" << std::endl;
  }
};

class Bar {
 public:
  Bar() {
    std::cout << "Bar()" << std::endl;
    //foo;
  }
  ~Bar() {
    std::cout << "~Bar()" << std::endl;
  }
 private:
  static thread_local Foo foo;
};

thread_local Foo Bar::foo;

void worker() {
  {
    std::cout << "enter block" << std::endl;
    Bar bar1;
    Bar bar2;
    std::cout << "exit block" << std::endl;
  }
}

int main() {
  std::thread t1(worker);
  std::thread t2(worker);
  t1.join();
  t2.join();
  std::cout << "thread died" << std::endl;
}

Der Code ist einfach. Meine BarKlasse hat ein statisches thread_localMitglied foo. Wenn eine Statik thread_local Foo fooerstellt wird, bedeutet dies, dass ein Thread erstellt wird.

Aber wenn ich den Code ausführe, nichts in den Foo()Ausdrucken, und wenn ich den Kommentar im BarKonstruktor entferne , der verwendet foo, funktioniert der Code einwandfrei.

Ich habe dies auf GCC (7.4.0) und Clang (6.0.0) versucht und die Ergebnisse sind die gleichen. Ich vermute, dass der Compiler festgestellt hat, dass er foonicht verwendet wird und keine Instanz generiert. Damit

  1. Hat der Compiler das static thread_localMitglied ignoriert ? Wie kann ich dafür debuggen?
  2. Wenn ja, warum hat ein normales staticMitglied dieses Problem nicht?
Ravenisadesk
quelle

Antworten:

9

Es gibt kein Problem mit Ihrer Beobachtung. [basic.stc.static] / 2 verbietet das Entfernen von Variablen mit statischer Speicherdauer:

Wenn eine Variable mit statischer Speicherdauer eine Initialisierung oder einen Destruktor mit Nebenwirkungen aufweist, wird sie nicht entfernt, selbst wenn sie nicht verwendet zu werden scheint, mit der Ausnahme, dass ein Klassenobjekt oder seine Kopie / Verschiebung wie in [class.copy] angegeben entfernt werden kann. .

Diese Einschränkung gilt nicht für andere Lagerdauern. Tatsächlich sagt [basic.stc.thread] / 2 :

Eine Variable mit Thread-Speicherdauer muss vor ihrer ersten Verwendung initialisiert und, falls konstruiert , beim Thread-Ausgang zerstört werden.

Dies legt nahe, dass eine Variable mit Thread-Speicherdauer nur erstellt werden muss, wenn odr verwendet wird.


Aber warum ist diese Diskrepanz?

Für die statische Speicherdauer gibt es nur eine Instanz einer Variablen pro Programm. Die Nebenwirkungen der Konstruktion können erheblich sein (ähnlich wie bei einem programmweiten Konstruktor), daher sind die Nebenwirkungen erforderlich.

Für die lokale Speicherdauer des Threads gibt es jedoch ein Problem: Ein Algorithmus kann viele Threads starten. Für die meisten dieser Threads ist die Variable völlig irrelevant. Es wäre lustig, wenn eine externe Physik-Simulationsbibliothek, die aufruft, std::reduce(std::execution::par_unseq, first, last)am Ende viele fooInstanzen erstellt, oder?

Natürlich kann es eine legitime Verwendung für Nebenwirkungen der Konstruktion von Variablen der lokalen Speicherdauer des Threads geben, die nicht odr-verwendet werden (z. B. ein Thread-Tracker). Der Vorteil, dies zu gewährleisten, reicht jedoch nicht aus, um den oben genannten Nachteil auszugleichen, sodass diese Variablen eliminiert werden dürfen, solange sie nicht odr-verwendet werden. (Ihr Compiler kann sich jedoch dafür entscheiden, dies nicht zu tun. Außerdem können Sie einen eigenen Wrapper erstellen std::thread, der sich darum kümmert.)

LF
quelle
1
Okay ... wenn der Standard dies sagt, dann sei es so ... Aber ist es nicht seltsam, dass das Komitee die Nebenwirkungen nicht als statische Speicherdauer betrachtet?
Ravenisadesk
@reavenisadesk Siehe aktualisierte Antwort.
LF
1

Ich habe diese Informationen in " ELF-Handhabung für threadlokalen Speicher " gefunden, was die Antwort von @LF beweisen kann

Darüber hinaus sollte die Laufzeitunterstützung das Erstellen des threadlokalen Speichers vermeiden, wenn dies nicht erforderlich ist. Beispielsweise kann ein geladenes Modul nur von einem der vielen Threads verwendet werden, aus denen der Prozess besteht. Es wäre eine Verschwendung von Speicher und Zeit, den Speicher für alle Threads zuzuweisen. Eine faule Methode ist erwünscht. Dies ist keine große zusätzliche Belastung, da für die Verarbeitung dynamisch geladener Objekte bereits Speicher erkannt werden muss, der noch nicht zugeordnet ist. Dies ist die einzige Alternative zum Stoppen aller Threads und zum Zuweisen von Speicher für alle Threads, bevor sie erneut ausgeführt werden.

Ravenisadesk
quelle