Garantiert „flüchtig“ überhaupt etwas im tragbaren C-Code für Mehrkernsysteme?


12

Nach dem Blick auf ein Bündel von anderen Fragen und ihre Antworten , habe ich den Eindruck , dass es auf das, was die „flüchtig“ Schlüsselwort in C genau bedeutet keine weitgehende Übereinstimmung ist.

Selbst der Standard selbst scheint nicht klar genug zu sein, damit sich alle einig sind was er bedeutet .

Unter anderem:

  1. Es scheint je nach Hardware und Compiler unterschiedliche Garantien zu bieten.
  2. Dies wirkt sich auf Compiler-Optimierungen aus, nicht jedoch auf Hardware-Optimierungen. Bei einem erweiterten Prozessor, der seine eigenen Laufzeitoptimierungen durchführt, ist nicht einmal klar, ob der Compiler die von Ihnen gewünschte Optimierung verhindern kann. (Einige Compiler generieren Anweisungen, um einige Hardwareoptimierungen auf einigen Systemen zu verhindern. Dies scheint jedoch in keiner Weise standardisiert zu sein.)

Um das Problem zusammenzufassen, scheint (nach vielem Lesen) "volatile" etwas zu garantieren wie: Der Wert wird nicht nur aus / in ein Register, sondern zumindest in den L1-Cache des Kerns in derselben Reihenfolge gelesen / geschrieben Die Lese- / Schreibvorgänge werden im Code angezeigt. Dies scheint jedoch nutzlos zu sein, da das Lesen / Schreiben von / in ein Register innerhalb desselben Threads bereits ausreicht, während die Koordination mit dem L1-Cache keine weiteren Garantien hinsichtlich der Koordination mit anderen Threads garantiert. Ich kann mir nicht vorstellen, wann es jemals wichtig sein könnte, nur mit dem L1-Cache zu synchronisieren.

VERWENDE 1
Die einzige allgemein vereinbarte Verwendung von flüchtig scheint für alte oder eingebettete Systeme zu sein, bei denen bestimmte Speicherorte Hardware-E / A-Funktionen zugeordnet sind, wie z. B. ein Bit im Speicher, das (direkt in der Hardware) ein Licht steuert oder ein bisschen im Speicher, das Ihnen sagt, ob eine Tastaturtaste gedrückt ist oder nicht (weil sie von der Hardware direkt mit der Taste verbunden ist).

Es scheint, dass "use 1" nicht in portablem Code vorkommt, dessen Ziele Mehrkernsysteme umfassen.

USE 2
Nicht zu verschieden von "use 1" ist der Speicher, der jederzeit von einem Interrupt-Handler gelesen oder geschrieben werden kann (der möglicherweise ein Licht steuert oder Informationen von einem Schlüssel speichert). Aber schon deshalb haben wir das Problem, dass der Interrupt-Handler je nach System auf einem anderen Kern mit eigenem Speicher-Cache ausgeführt wird und "volatile" nicht die Cache-Kohärenz auf allen Systemen garantiert.

So „use 2“ scheint jenseits dessen, was „flüchtig“ liefern zu können.

VERWENDUNG 3
Die einzige andere unbestrittene Verwendung, die ich sehe, besteht darin, eine Fehloptimierung von Zugriffen über verschiedene Variablen zu verhindern, die auf denselben Speicher verweisen, von dem der Compiler nicht erkennt, dass er denselben Speicher hat. Aber das ist wahrscheinlich nur unbestritten, weil die Leute nicht darüber reden - ich habe nur eine Erwähnung davon gesehen. Und ich dachte, der C-Standard hat bereits erkannt, dass "verschiedene" Zeiger (wie verschiedene Argumente auf eine Funktion) auf dasselbe Element oder auf Elemente in der Nähe verweisen können, und bereits angegeben, dass der Compiler Code erzeugen muss, der auch in solchen Fällen funktioniert. Ich konnte dieses Thema jedoch im neuesten Standard (500 Seiten!) Nicht schnell finden.

Also "use 3" gibt es vielleicht gar nicht ?

Daher meine Frage:

Garantiert "flüchtig" überhaupt etwas im tragbaren C-Code für Mehrkernsysteme?


EDIT - Update

Nach dem Durchsuchen des neuesten Standards sieht es so aus, als ob die Antwort zumindest ein sehr begrenztes Ja ist:
1. Der Standard legt wiederholt eine spezielle Behandlung für den spezifischen Typ "volatile sig_atomic_t" fest. Der Standard besagt jedoch auch, dass die Verwendung der Signalfunktion in einem Multithread-Programm zu undefiniertem Verhalten führt. Daher scheint dieser Anwendungsfall auf die Kommunikation zwischen einem Single-Thread-Programm und seinem Signalhandler beschränkt zu sein.
2. Der Standard gibt auch eine klare Bedeutung für "flüchtig" in Bezug auf setjmp / longjmp an. (Beispielcode, wo es darauf ankommt, ist in anderen Fragen und Antworten angegeben .)

Die genauere Frage lautet also:
Garantiert "flüchtig" überhaupt etwas im tragbaren C-Code für Mehrkernsysteme, abgesehen von (1) dem Empfangen von Informationen von einem Single-Thread-Programm von seinem Signalhandler oder (2) dem Zulassen von setjmp Code, um Variablen zu sehen, die zwischen setjmp und longjmp geändert wurden?

Dies ist immer noch eine Ja / Nein-Frage.

Wenn "Ja", wäre es großartig, wenn Sie ein Beispiel für fehlerfreien tragbaren Code zeigen könnten, der fehlerhaft wird, wenn "flüchtig" weggelassen wird. Wenn "nein", kann ein Compiler "volatile" außerhalb dieser beiden sehr spezifischen Fälle für Multi-Core-Ziele ignorieren.


3
Signale existieren in tragbarem C; Was ist mit einer globalen Variablen, die von einem Signalhandler aktualisiert wird? Dies müsste sein volatile, um das Programm darüber zu informieren, dass es sich asynchron ändern kann.
Nate Eldredge

2
@NateEldredge Global ist zwar allein volatil, aber nicht gut genug. Es muss auch atomar sein.
Eugene Sh.

@EugeneSh.: Ja natürlich. Bei der vorliegenden Frage geht es jedoch volatilespeziell um das, was ich für notwendig halte.
Nate Eldredge

" Während die Koordination mit dem L1-Cache keine weiteren Garantien hinsichtlich der Koordination mit anderen Threads gibt " Wo reicht die "Koordination mit dem L1-Cache" nicht aus, um mit anderen Threads zu kommunizieren?
Neugieriger

1
Möglicherweise relevanter C ++ - Vorschlag zur Abwertung von Volatilität . Der Vorschlag befasst sich mit vielen der hier angesprochenen Bedenken, und möglicherweise hat sein Ergebnis Einfluss auf das C-Komitee
MM

Antworten:


1

Um das Problem zusammenzufassen, scheint es (nach vielem Lesen), dass "volatile" so etwas wie Folgendes garantiert: Der Wert wird nicht nur aus / in ein Register, sondern zumindest in den L1-Cache des Kerns in derselben Reihenfolge gelesen / geschrieben Die Lese- / Schreibvorgänge werden im Code angezeigt .

Nein, das tut es absolut nicht . Und das macht volatile für den Zweck des MT-Safe-Codes fast unbrauchbar.

Wenn dies der Fall ist, ist flüchtig für Variablen, die von mehreren Threads gemeinsam genutzt werden, sehr gut geeignet, da die Reihenfolge der Ereignisse im L1-Cache alles ist, was Sie in einer typischen CPU (dh Multi-Core oder Multi-CPU auf dem Motherboard) tun müssen, die zusammenarbeiten kann auf eine Weise, die eine normale Implementierung von C / C ++ - oder Java-Multithreading mit typischen erwarteten Kosten ermöglicht (dh keine großen Kosten für die meisten atomaren oder nicht zufriedenen Mutex-Operationen).

Aber volatil nicht weder theoretisch noch in der Praxis eine garantierte Reihenfolge (oder "Speichersichtbarkeit") im Cache.

(Hinweis: Das Folgende basiert auf einer fundierten Interpretation der Standarddokumente, der Absicht des Standards, der historischen Praxis und einem tiefen Verständnis der Erwartungen von Compiler-Autoren. Dieser Ansatz basiert auf der Geschichte, den tatsächlichen Praktiken sowie den Erwartungen und dem Verständnis realer Personen in die reale Welt, die viel stärker und zuverlässiger ist als das Parsen der Wörter eines Dokuments, von dem nicht bekannt ist, dass es sich um herausragende Spezifikationen handelt und das viele Male überarbeitet wurde.)

In der Praxis garantiert volatile eine ptrace-Fähigkeit, dh die Fähigkeit, Debug-Informationen für das laufende Programm auf jeder Optimierungsstufe zu verwenden , und die Tatsache, dass die Debug-Informationen für diese flüchtigen Objekte sinnvoll sind:

  • Sie können ptrace(einen ptrace-ähnlichen Mechanismus) verwenden, um sinnvolle Haltepunkte an den Sequenzpunkten nach Operationen mit flüchtigen Objekten festzulegen: Sie können wirklich genau an diesen Punkten brechen (beachten Sie, dass dies nur funktioniert, wenn Sie bereit sind, viele Haltepunkte als solche festzulegen Die C / C ++ - Anweisung kann zu vielen verschiedenen Start- und Endpunkten der Assembly kompiliert werden (wie in einer massiv abgewickelten Schleife).
  • Während ein Thread der Ausführung von gestoppt ist, können Sie den Wert aller flüchtigen Objekte lesen, da sie ihre kanonische Darstellung haben (nach dem ABI für ihren jeweiligen Typ). Eine nichtflüchtige lokale Variable könnte eine atypische Darstellung haben, z. eine verschobene Darstellung: Eine Variable, die zum Indizieren eines Arrays verwendet wird, kann zur einfacheren Indizierung mit der Größe einzelner Objekte multipliziert werden. oder es könnte durch einen Zeiger auf ein Array-Element ersetzt werden (solange alle Verwendungen der Variablen ähnlich konvertiert sind) (denken Sie daran, dx in einem Integral in du zu ändern);
  • Sie können diese Objekte auch ändern (sofern die Speicherzuordnungen dies zulassen, da sich flüchtige Objekte mit statischer Lebensdauer, die const-qualifiziert sind, möglicherweise in einem Speicherbereich befinden, der schreibgeschützt zugeordnet ist).

Volatile Garantie in der Praxis etwas mehr als die strikte ptrace-Interpretation: Sie garantiert auch, dass flüchtige automatische Variablen eine Adresse auf dem Stapel haben, da sie keinem Register zugeordnet sind, eine Registerzuordnung, die ptrace-Manipulationen empfindlicher machen würde (Compiler kann Debug-Informationen ausgeben, um zu erklären, wie Variablen Registern zugewiesen werden, aber das Lesen und Ändern des Registerstatus ist etwas aufwändiger als der Zugriff auf Speicheradressen.

Beachten Sie, dass die vollständige Programm-Debug-Fähigkeit, bei der alle Variablen zumindest an Sequenzpunkten flüchtig sind, durch den "Null-Optimierungs" -Modus des Compilers bereitgestellt wird, der immer noch triviale Optimierungen wie arithmetische Vereinfachungen durchführt (normalerweise gibt es keine garantierte Nein-Option) Optimierung in allen Modi). Flüchtig ist jedoch stärker als nicht optimiert: x-xKann für eine nichtflüchtige Ganzzahl, xjedoch nicht für ein flüchtiges Objekt vereinfacht werden.

So flüchtige Mittel, die garantiert so kompiliert werden, wie sie sind , wie die Übersetzung eines Systemaufrufs von der Quelle in die Binärdatei / Assembly durch den Compiler keine Neuinterpretation, Änderung oder Optimierung durch einen Compiler. Beachten Sie, dass Bibliotheksaufrufe Systemaufrufe sein können oder nicht. Viele offizielle Systemfunktionen sind tatsächlich Bibliotheksfunktionen, die eine dünne Interpositionsschicht bieten und sich am Ende im Allgemeinen auf den Kernel verschieben. (Insbesondere getpidmuss nicht zum Kernel gegangen werden und könnte einen Speicherort lesen, der vom Betriebssystem bereitgestellt wird, der die Informationen enthält.)

Flüchtige Wechselwirkungen sind Wechselwirkungen mit der Außenwelt der realen Maschine , die der "abstrakten Maschine" folgen müssen. Sie sind keine internen Interaktionen von Programmteilen mit anderen Programmteilen. Der Compiler kann nur über das nachdenken, was er weiß, das sind die internen Programmteile.

Die Codegenerierung für einen flüchtigen Zugriff sollte der natürlichsten Interaktion mit diesem Speicherort folgen: Es sollte nicht überraschend sein. Das bedeutet, dass einige flüchtige Zugriffe atomar sein sollen : Wenn die natürliche Art, die Darstellung von a longin der Architektur zu lesen oder zu schreiben, atomar ist, wird erwartet, dass ein Lese- oder Schreibvorgang von a volatile longatomar ist, da der Compiler nicht generieren sollte alberner ineffizienter Code, um beispielsweise byteweise auf flüchtige Objekte zuzugreifen .

Sie sollten dies feststellen können, indem Sie die Architektur kennen. Sie müssen nichts über den Compiler wissen, da flüchtig bedeutet, dass der Compiler transparent sein sollte .

Volatile erzwingt jedoch nur die Emission der erwarteten Assembly für die für bestimmte Fälle am wenigsten optimierten, um eine Speicheroperation auszuführen: Volatile Semantik bedeutet allgemeine Fallsemantik.

Der allgemeine Fall ist, was der Compiler tut, wenn er keine Informationen über ein Konstrukt hat: f.ex. Das Aufrufen einer virtuellen Funktion für einen Wert über den dynamischen Versand ist ein allgemeiner Fall, bei dem der Overrider direkt aufgerufen wird, nachdem zur Kompilierungszeit der Typ des durch den Ausdruck angegebenen Objekts bestimmt wurde. Der Compiler hat immer eine allgemeine Fallbehandlung aller Konstrukte und folgt dem ABI.

Volatile unternimmt nichts Besonderes, um Threads zu synchronisieren oder "Speichersichtbarkeit" bereitzustellen: Volatile bietet nur Garantien auf abstrakter Ebene , die von einem Thread aus ausgeführt oder gestoppt werden, dh innerhalb eines CPU-Kerns :

  • volatile sagt nichts darüber aus, welche Speicheroperationen den Hauptspeicher erreichen (Sie können bestimmte Speicher-Caching-Typen mit Montageanweisungen oder Systemaufrufen festlegen, um diese Garantien zu erhalten);
  • volatile bietet keine Garantie dafür, wann Speicheroperationen für eine Cache-Ebene (nicht einmal für L1) festgeschrieben werden .

Nur der zweite Punkt bedeutet, dass flüchtig bei den meisten Kommunikationsproblemen zwischen Threads nicht nützlich ist. Der erste Punkt ist bei Programmierproblemen, bei denen keine Kommunikation mit Hardwarekomponenten außerhalb der CPU (s), aber immer noch auf dem Speicherbus erfolgt, im Wesentlichen irrelevant.

Die Eigenschaft von flüchtig, ein garantiertes Verhalten aus der Sicht des Kerns bereitzustellen, der den Thread ausführt, bedeutet, dass an diesen Thread gelieferte asynchrone Signale, die unter dem Gesichtspunkt der Ausführungsreihenfolge dieses Threads ausgeführt werden, Operationen in der Quellcodereihenfolge sehen .

Wenn Sie nicht vorhaben, Signale an Ihre Threads zu senden (ein äußerst nützlicher Ansatz zur Konsolidierung von Informationen über aktuell ausgeführte Threads ohne zuvor vereinbarten Stopppunkt), ist volatile nichts für Sie.


6

Ich bin kein Experte, aber cppreference.com hat einige meiner Meinung nach ziemlich gute Informationenvolatile . Hier ist der Kern davon:

Jeder Zugriff (sowohl Lesen als auch Schreiben), der über einen l-Wert-Ausdruck vom Typ "flüchtig qualifiziert" erfolgt, wird zum Zweck der Optimierung als beobachtbarer Nebeneffekt angesehen und streng nach den Regeln der abstrakten Maschine bewertet (dh alle Schreibvorgänge werden um abgeschlossen) einige Zeit vor dem nächsten Sequenzpunkt). Dies bedeutet, dass innerhalb eines einzelnen Ausführungsthreads ein flüchtiger Zugriff nicht optimiert oder in Bezug auf einen anderen sichtbaren Nebeneffekt neu angeordnet werden kann, der durch einen Sequenzpunkt vom flüchtigen Zugriff getrennt ist.

Es gibt auch einige Verwendungszwecke:

Verwendung von flüchtigen

1) Statische flüchtige Objekte modellieren speicherabgebildete E / A-Ports und statische konstante flüchtige Objekte modellieren speicherabgebildete Eingangsports, wie z. B. eine Echtzeituhr

2) statische flüchtige Objekte vom Typ sig_atomic_t werden für die Kommunikation mit Signalhandlern verwendet.

3) Flüchtige Variablen, die lokal für eine Funktion sind, die einen Aufruf des Makros setjmp enthält, sind die einzigen lokalen Variablen, deren Werte nach der Rückkehr von longjmp garantiert erhalten bleiben.

4) Zusätzlich können flüchtige Variablen verwendet werden, um bestimmte Formen der Optimierung zu deaktivieren, z. B. um die Eliminierung von toten Speichern oder die konstante Faltung für Mikrobenchmark zu deaktivieren.

Und natürlich wird erwähnt, dass volatiledies für die Thread-Synchronisation nicht nützlich ist:

Beachten Sie, dass flüchtige Variablen nicht für die Kommunikation zwischen Threads geeignet sind. Sie bieten keine Atomizität, Synchronisation oder Speicherreihenfolge. Ein Lesevorgang von einer flüchtigen Variablen, die von einem anderen Thread ohne Synchronisation oder gleichzeitige Änderung von zwei nicht synchronisierten Threads geändert wird, ist aufgrund eines Datenrennens ein undefiniertes Verhalten.


2
Insbesondere sind (2) und (3) für tragbaren Code relevant.
Nate Eldredge

2
@TED ​​Trotz des Domainnamens führt der Link zu Informationen über C, nicht über C ++
David Brown

@NateEldredge Sie können selten longjmpin C ++ - Code verwenden.
Neugieriger

@DavidBrown C und C ++ haben dieselbe Definition einer beobachtbaren SE und im Wesentlichen dieselben Thread-Grundelemente.
Neugieriger

4

Erstens gab es historisch gesehen verschiedene Probleme mit unterschiedlichen Interpretationen der Bedeutung von volatileZugang und ähnlichem. Sehen Sie diese Studie: Flüchtige Bestandteile werden Miscompiled, und was zu tun ist über sie .

Abgesehen von den verschiedenen in dieser Studie erwähnten Problemen ist das Verhalten von volatiletragbar, abgesehen von einem Aspekt: ​​Wenn sie als Speicherbarrieren fungieren . Eine Speicherbarriere ist ein Mechanismus, der die gleichzeitige Ausführung Ihres Codes ohne Sequenz verhindert. Die Verwendung volatileals Speicherbarriere ist sicherlich nicht tragbar.

Ob die C-Sprache das Gedächtnisverhalten garantiert oder nicht, volatileist anscheinend fraglich, obwohl ich persönlich denke, dass die Sprache klar ist. Zuerst haben wir die formale Definition von Nebenwirkungen, C17 5.1.2.3:

Der Zugriff auf ein volatileObjekt, das Ändern eines Objekts, das Ändern einer Datei oder das Aufrufen einer Funktion, die eine dieser Operationen ausführt , sind alles Nebenwirkungen , die sich im Status der Ausführungsumgebung ändern.

Der Standard definiert den Begriff Sequenzierung als eine Möglichkeit, die Reihenfolge der Bewertung (Ausführung) zu bestimmen. Die Definition ist formal und umständlich:

Vorher sequenziert ist eine asymmetrische, transitive, paarweise Beziehung zwischen Auswertungen, die von einem einzelnen Thread ausgeführt werden, was eine Teilreihenfolge zwischen diesen Auswertungen induziert. Wenn bei zwei Bewertungen A und B A vor B sequenziert wird, muss die Ausführung von A der Ausführung von B vorausgehen. (Wenn umgekehrt A vor B sequenziert wird, wird B nach A sequenziert .) Wenn A nicht sequenziert wird vor oder nach B, dann sind A und B nicht sequenziert . Die Bewertungen A und B werden unbestimmt sequenziert, wenn A entweder vor oder nach B sequenziert wird, es ist jedoch nicht spezifiziert, welche.13) Das Vorhandensein eines Sequenzpunkts zwischen der Auswertung der Ausdrücke A und B impliziert, dass jede mit A verbundene Wertberechnung und Nebenwirkung vor jeder mit B verbundenen Wertberechnung und Nebenwirkung sequenziert wird. (Eine Zusammenfassung der Sequenzpunkte ist in Anhang C angegeben.)

Die TL; DR der obigen ist im Grunde genommen, dass, wenn wir einen Ausdruck haben, Ader Nebenwirkungen enthält, er vor einem anderen Ausdruck ausgeführt werden muss B, falls er danach Bsequenziert wird A.

Optimierungen des C-Codes werden durch diesen Teil ermöglicht:

In der abstrakten Maschine werden alle Ausdrücke gemäß der Semantik ausgewertet. Eine tatsächliche Implementierung muss keinen Teil eines Ausdrucks auswerten, wenn daraus geschlossen werden kann, dass sein Wert nicht verwendet wird und keine erforderlichen Nebenwirkungen auftreten (einschließlich solcher, die durch den Aufruf einer Funktion oder den Zugriff auf ein flüchtiges Objekt verursacht werden).

Dies bedeutet, dass das Programm Ausdrücke in der Reihenfolge auswerten (ausführen) kann, die der Standard an anderer Stelle vorschreibt (Reihenfolge der Auswertung usw.). Es muss jedoch keinen Wert auswerten (ausführen), wenn daraus geschlossen werden kann, dass er nicht verwendet wird. Beispielsweise muss die Operation 0 * xnicht ausgewertet werdenx den Ausdruck und einfach durch ersetzen 0.

Es sei denn, der Zugriff auf eine Variable ist ein Nebeneffekt. Was bedeutet , dass für den Fall xist volatile, es muss (execute) zu bewerten , 0 * xauch wenn das Ergebnis ist immer 0. Die Optimierung wird ist nicht erlaubt.

Darüber hinaus spricht der Standard von beobachtbarem Verhalten:

Die geringsten Anforderungen an eine konforme Implementierung sind:

  • Zugriffe auf flüchtige Objekte werden streng nach den Regeln der abstrakten Maschine ausgewertet.
    / - / Dies ist das beobachtbare Verhalten des Programms.

In Anbetracht all dessen kann eine konforme Implementierung (Compiler + zugrunde liegendes System) den Zugriff auf volatileObjekte möglicherweise nicht in einer nicht sequenzierten Reihenfolge ausführen, falls die Semantik der geschriebenen C-Quelle etwas anderes sagt.

Dies bedeutet, dass in diesem Beispiel

volatile int x;
volatile int y;
z = x;
z = y;

Beide Zuweisungsausdrücke müssen ausgewertet werden und z = x; müssen vorher ausgewertet werden z = y;. Eine Multiprozessor-Implementierung, die diese beiden Operationen an zwei verschiedene Unsequenz-Kerne auslagert, ist nicht konform!

Das Dilemma besteht darin, dass Compiler nicht viel gegen Dinge wie Pre-Fetch-Caching und Anweisungs-Pipelining usw. tun können, insbesondere nicht, wenn sie auf einem Betriebssystem ausgeführt werden. Und so übergeben Compiler dieses Problem an die Programmierer und sagen ihnen, dass Speicherbarrieren nun in der Verantwortung des Programmierers liegen. Während der C-Standard eindeutig festlegt, dass das Problem vom Compiler gelöst werden muss.

Der Compiler kümmert sich jedoch nicht unbedingt darum, das Problem zu lösen, und volatileist daher nicht portierbar, um als Speicherbarriere zu fungieren. Es ist zu einem Problem der Qualität der Implementierung geworden.


@curiousguy spielt keine Rolle.
Lundin

@curiousguy spielt keine Rolle, solange es sich um eine Art Ganzzahl mit oder ohne Qualifikationsmerkmale handelt.
Lundin

Wenn es sich um eine einfache nichtflüchtige Ganzzahl handelt, warum sollten redundante Schreibvorgänge zwirklich ausgeführt werden? (wie z = x; z = y;) Der Wert wird in der nächsten Anweisung gelöscht.
Neugieriger

@curiousguy Da die Lesevorgänge zu den flüchtigen Variablen unabhängig von der angegebenen Reihenfolge ausgeführt werden müssen.
Lundin

Wird dann zwirklich zweimal vergeben? Woher wissen Sie, dass "Lesevorgänge ausgeführt werden"?
Neugieriger
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.