[gelöst] C++: OpenMP Parallel



  • Hallo ihr Lieben,

    ich habe mal wieder eine Frage, ob ich das langsam richtig verstehe mit diesen Threads. Ich habe das folgende Minimalbeispiel

    #include <iostream>
    
    #include <omp.h>
    
    int main()
    {
    unsigned int a = 0;
    std::cout << omp_get_num_threads() << std::endl;
    #pragma omp parallel
    {
    std::cout << omp_get_num_threads() << std::endl;
    	#pragma omp for reduction(+:a)
    		for(unsigned int i = 0; i < 10000; ++i)
    		{
    			++a;
    		}
    }
    std::cout << a << std::endl;
    }
    

    Und als Ausgabe

    1
    888
    
    8888
    
    8
    10000
    

    Also den ersten Teil verste ich noch, die 1 bedeutet, dass ein Thread läuft. Allerdings ist dies noch vor der #pragma omp parallel Anweisung. Das bedeutet also, dass main schon einen Thread für sich darstellt, auch wenn man das nicht sagt, so lange man nicht über Parallelisierung redet?

    Was ich viel interessanter finde ist die Verteilung der 8. Ich tippe darauf, dass ich eine 8 kriege, weil mein PC acht Kerne besitzt, aber:

    i) Warum kriege ich acht Mal die 8? Verstehe ich das richtig, dass ab #pragma omp parallel alles pro Thread innerhalb der Umgebung geschieht? Und da acht Threads erzeugt werden, gibt es acht mal die Ausgabe.

    ii) Falls dies stimmen würde bin ich aber irritiert, weil ich dachte, dass die for -Schleife auf verschiedene Threads aufgeteilt werden soll, aber scheinbar werden diese Threads schon erzeugt, bevor die for Schleife startet? 😕

    iii) Und sehe ich das richtig, dass die Ausgabe so seltsam angeordnet ist, weil alle acht Threads um cout konkurrieren? Mal mit der Ausgabe der 8 und mal mit endl ? Also zuerst schaffen drei Threads die 8 auszugeben, dann schaffen zwei Threads davon das endl durchzukriegen, dann wieder vier Threads die 8, usw.

    Gruß,
    -- Klaus.


  • Mod

    Klaus82 schrieb:

    Also den ersten Teil verste ich noch, die 1 bedeutet, dass ein Thread läuft. Allerdings ist dies noch vor der #pragma omp parallel Anweisung. Das bedeutet also, dass main schon einen Thread für sich darstellt,

    Ja, natürlich ist der Hauptprozess auch ein Thread. Wobei es aber sehr ungenau ist, diesen als "main" zu bezeichnen. main ist bloß eine funktion, die in diesem Thread ausgeführt wird.

    auch wenn man das nicht sagt, so lange man nicht über Parallelisierung redet?

    Man redet selten über Threads, wenn man nichts paralleles macht.

    i) Warum kriege ich acht Mal die 8? Verstehe ich das richtig, dass ab #pragma omp parallel alles pro Thread innerhalb der Umgebung geschieht? Und da acht Threads erzeugt werden, gibt es acht mal die Ausgabe.

    Ja, genau. Gerade dafür ist omp parallel doch da.

    ii) Falls dies stimmen würde bin ich aber irritiert, weil ich dachte, dass die for -Schleife auf verschiedene Threads aufgeteilt werden soll, aber scheinbar werden diese Threads schon erzeugt, bevor die for Schleife startet? 😕

    Gerade dafür ist omp parallel doch da. OMP-Dokumentation lesen!

    iii) Und sehe ich das richtig, dass die Ausgabe so seltsam angeordnet ist, weil alle acht Threads um cout konkurrieren? Mal mit der Ausgabe der 8 und mal mit endl ? Also zuerst schaffen drei Threads die 8 auszugeben, dann schaffen zwei Threads davon das endl durchzukriegen, dann wieder vier Threads die 8, usw.

    Korrekt. Daher macht man Ausgaben gewöhnlicherweise zentral in einem Thread. Es gibt gewisse Implementierungen (z.B. glibc, also die gängigste C(!)-Bibliothek unter Linux), bei denen Ausgaben automatisch synchronisiert werden, aber für C++ wäre mir so etwas nicht bekannt.



  • Hallo,

    SeppJ schrieb:

    Ja, genau. Gerade dafür ist omp parallel doch da.

    Aaaah, ich glaube langsam verstehe ich den Unterschied. Also folgender Code

    #pragma omp parallel reduction (+:a)
    {
    		for(unsigned int i = 0; i < 10000; ++i)
    		{
    			++a;
    		}
    }
    

    sagt aus, dass mit der #pragma Anweisung (in meinem Falle) 8 Threads erzeugt werden und jeder die eingeschlossene Aufgabe ausführt. Deshalb habe ich am Ende ein Ergenis von 8000, weil acht Mal 1000 Mal aufaddiert wird.

    Und jetzt gibt es den Spezialfall der for Schleife

    #pragma omp parallel for reduction (+:a)
    		for(unsigned int i = 0; i < 10000; ++i)
    		{
    			++a;
    		}
    

    Wiederum werden 8 Threads erzeugt, aber die Option for gibt nun an, dass eine for Schleife folgt und die ganzen Iterationen auf die 8 Threads verteilt werden sollen, deshalb kriege ich am Ende auch als Ergebnis 1000 heraus.

    Is' so richtig Chef? 🙂

    Gruß,
    -- Klaus.


  • Mod

    Unter Vorbehalt: Ja. Ich bin nicht so ganz der große Profi mit OpenMP. Bloß mal gelernt, aber dann nie gebraucht, daher etwas eingerostet. Aber so hätte ich es auch erklärt.



  • Jetzt habe ich dazu noch eine naive Frage, die auf Arrays abzielt. Bei der Parallelisierung ist ein Problem der Zugriff verschiedener Threads auf eine gemeinsame Variable.

    Wie ist das denn bei Arrays, zählt da tatsächlich jeder einzelne Eintrag als 'einzelne Variable' ? Denn wenn Thread Eins auf array[0] zugreift, dann scheint das den Zugriff von Thread Drei auf array[2] nicht zu behindern.

    Ist das eben u.A. genau der Vorteil des Zugriffs per Index, den man z.B. gegenüber einer verketteten Liste hat?

    Gruß,
    -- Klaus.



  • Du kannst eigentlich auf jedem Container arbeiten, solange du nur den Inhalt beareitest und nicht den Container selbst (push, insert) etc.


  • Mod

    Klaus82 schrieb:

    Jetzt habe ich dazu noch eine naive Frage, die auf Arrays abzielt. Bei der Parallelisierung ist ein Problem der Zugriff verschiedener Threads auf eine gemeinsame Variable.

    Wie ist das denn bei Arrays, zählt da tatsächlich jeder einzelne Eintrag als 'einzelne Variable' ? Denn wenn Thread Eins auf array[0] zugreift, dann scheint das den Zugriff von Thread Drei auf array[2] nicht zu behindern.

    Ja. Prinzipiell müsstest du beachten, dass ein komplexer Containerdatentyp das auch anders machen könnte, aber bei den ganzen Standardcontainern ist garantiert, dass gleichzeitige Zugriffe auf verschiedene Elemente sich nicht in die Quere kommen. Du kannst sogar gleichzeitig lesend zugreifen. Was du natürlich nicht machen darfst, ist die Struktur des Containers an sich verändern (z.B. einen vector reallokieren). Es ist alles so, wie man naiv erwarten würde.
    Garantiert ist dies erst ab C++11, aber vor C++11 müsstest du schon nach einer absichtlich bösartigen Implementierung suchen, bei der dieses Verhalten nicht so wäre.

    Ist das eben u.A. genau der Vorteil des Zugriffs per Index, den man z.B. gegenüber einer verketteten Liste hat?

    Auch bei einer Liste kannst du gleichzeitig auf verschiedene Elemente zugreifen, solange du die Liste selber nicht veränderst. Selbst wenn du sie veränderst, darfst du gleichzeitig auf die nicht von der Änderung betroffenen Teile zugreifen.
    Abgesehen davon hat eine Liste praktisch nur (große!) Nachteile, sofern dein Programm nicht massiv den einen Vorteil der Listendatenstruktur (günstiges Splicing) benutzt.



  • SeppJ schrieb:

    Abgesehen davon hat eine Liste praktisch nur (große!) Nachteile, sofern dein Programm nicht massiv den einen Vorteil der Listendatenstruktur (günstiges Splicing) benutzt.

    Stabile Verweise und Iteratoren sind auch ein nicht zu unterschätzender Vorteil.



  • Klaus82 schrieb:

    Wie ist das denn bei Arrays, zählt da tatsächlich jeder einzelne Eintrag als 'einzelne Variable' ? Denn wenn Thread Eins auf array[0] zugreift, dann scheint das den Zugriff von Thread Drei auf array[2] nicht zu behindern.

    Aus Programmsicht sind die einzelne Eintrag unabhängig und daher ist das kein Problem. In der Praxis ist es aber so, dass Prozessoren in der Granularität von Cachelines auf den Speicher zugreifen, so dass es durchaus wahrscheinlich ist, dass bei parallelem Zugriff auf benachbarte Elemente doch wieder implizit synchronisiert wird (Stichwort Cache-Kohärenz), womit die Parallelität weg ist. Das parallelisieren lohnt sich daher nur bei großen Arrays und günstigen Zugriffsmustern.

    Zusatz: Das gilt nicht für rein lesenden Zugriff.



  • So,

    ich hoffe, dass ich mal wieder was verstanden habe:

    Ich möchte in einer omp pragma for Umgebung jedem Thread eine eigene Kopie eines Objekts zur Verfügung stellen. Zunächst geht das mit der private Bezeichnung. Allerdings steht in der OMP Beschreibung:

    The order in which any default constructors for different private variables of class type are called is unspecified.

    Aha. Da wird also nur der Default Konstruktor aufgerufen. Also entweder ist mein Objekt durch aufrufen dieses Konstruktors vollständig initialisiert oder mir reicht das eben nicht.

    Falls es nicht vollständig initialisiert ist, muss ich auf firstprive zurückgreifen, denn da steht

    For variables of class type, a copy constructor is invoked to perform the initialization.

    Aha, denn mittels Copy Konstruktor kann ich ein bestehendes Objekt initialisieren und die bisherigen Werte reinkopieren. Den muss ich dann allerdings definieren (und Regel der 3 bzw. 5 mittlerweile 😉 ).

    Sehe ich das so richtig? Muss doch mal langsam was gelernt haben. 🕶

    Gruß,
    -- Klaus.


  • Mod

    Du musst keine eigenen Kopierkonstruktoren schreiben, wenn du kein spezielles Verhalten benötigst. Die zählen auch zu den "special member functions".

    Der Rest ist korrekt.



  • SeppJ schrieb:

    Du musst keine eigenen Kopierkonstruktoren schreiben, wenn du kein spezielles Verhalten benötigst.

    Das ist sicherlich der Knackpunkt, was man unter spezielles Verhalten versteht.

    Wenn ich mich recht entsinne, dann begann für mich der ganze Lernprozess mit den verschiedenen Konstruktoren, weil ich new und delete verwendet hatte, also pointer.

    Bei der Verwendung eines Pointer im Kopierkonstruktor wird allerdings nur ein neuer Pointer erzeugt und der Inhalt hineinkopiert, was in diesem Falle die Addresse ist. Eine shallow copy, denn jetzt habe ich den Fall, dass zwei Pointer auf den gleichen Speicherbereich zeigen was z.B. zu einem dangling pointer führen kann?

    Also möchte ich ein spezielles Verhalten: Ich möchte, dass der Speicherbereich, auf den der Pointer zeigt, neu angelegt wird und der Inhalt des Speicherbereichs (nicht nur die Addresse des Pointers) kopiert wird - eine deep copy. [1]

    Vielleicht etwas salopp formuliert: Bei einem automatisch erstellten Kopier Konstruktor von einer shallow copy ausgehen und dann überlegen ob das reicht?

    Gruß,
    -- Klaus.

    [1] Oder ich umgehe die deep copy, indem ich mitzähle, wie viele Pointer auf einen Speicherbereich zeigen, um dangling Pointer zu verhindern. Das entspricht dann dem smart_pointer?


  • Mod

    Alles richtig, jedoch ein ganz, ganz wichtiger Punkt wurde ausgelassen:

    Wenn du new/delete brauchst, dann brauchst du meistens kein new/delete, sondern die Standardbibliothek. Meistens std::vector. Wenn du schon überlegst, selber Referenzen zu zählen, dann brauchst du ebenfalls die Standardbibliothek und zwar die Smartpointer daraus. Aber höchstwahrscheinlich brauchst du ebenfalls irgendeinen der fertigen Container, anstatt diesen aus Smartpointern nachzubauen.

    Die ganzen Sachen aus der Standardbibliothek implementieren Kopien korrekt, du brauchst dich also wieder um nichts mehr zu kümmern.

    http://en.cppreference.com/w/cpp/container
    http://en.cppreference.com/w/cpp/memory

    Das ist sicherlich der Knackpunkt, was man unter spezielles Verhalten versteht.

    Das ist leicht zu erklären:
    Standardverhalten des Defaultkonstruktors:
    Defaultkonstruktor aller Elemente und Basisklassen aufrufen

    Standardverhalten des Kopierkonstruktors:
    Kopierkonstruktor aller Elemente und Basisklassen aufrufen

    Standardverhalten des Zuweisungsoperators:
    Zuweisungsoperator aller Elemente und Basisklassen aufrufen

    Standardverhalten des Destruktors:
    Destruktor aller Elemente und Basisklassen aufrufen

    Kann man sich merken, oder? 🙂

    Zu beachten ist natürlich, dass diese Subfunktionen nicht unbedingt selber trivial sein müssen. Der Kopierkonstruktor eines vectors macht komplexe Dinge. Aber eine Klasse mit einem vector als Member braucht keinen eigenen Code im Kopierkonstruktor, um den vector zu kopieren, da schließlich der Kopierkonstruktors des vectors genommen wird, der schon alles richtig macht.


  • Mod

    Man kann es sich auch merken, in dem man Basisklassen und Member als Subobjekte zusammenfasst - und dann machen die speziellen Memberfunktionen nichts weiter, als die Subobjekte zu verwalten - sprich kopieren, erzeugen, zuzuweisen oder zu zerstören.



  • Also so ganz habe ich es noch nicht raus.

    Ich kriege bei der Übergabe zu firstprivate z.B. folgende Fehelermeldung:

    cppwrapper.cpp:46:47: error: use of deleted function ‘interpolation::interpolation(const interpolation&)’
    In file included from cppwrapper.cpp:9:0:
    interpolation.h:11:8: note: ‘interpolation::interpolation(const interpolation&)’ is implicitly declared as deleted because ‘interpolation’ declares a move constructor or move assignment operator
    

    Okay, ich habe noch keinen Copy Constructor eingerichtet, aber was heißt dieser explizite Hinweis, dass etwas deleted ist? Dass Open MP nicht mit den Move und Assignement zurechtkommt? 😕

    Edit:
    Argh! Jetzt habe ich einen Copy Constructor eingerichtet und jetzt heißt es

    interpolation.cpp:26:56: error: definition of implicitly-declareed 'interpolation::interpolation(const interpolation&)’
    

    😕 😕

    Gruß,
    -- Klaus.


  • Mod

    Jetzt habe ich einen Copy Constructor eingerichtet

    Du musst dem armen Compiler aber sagen, dass er deinen nehmen soll, und nicht seinen. In dem du ihn in der Klasse deklarierst.

    aber was heißt dieser explizite Hinweis, dass etwas deleted ist?

    Einige spezielle Memberfunktionen werden als deleted definiert (sprich, als unbenutzbar definiert), weil andere definiert wurden.

    Sobald du einen Kopierkonstruktor definiert hast, darf der Compiler auch keinen Move-Konstruktor oder -Zuweisungsoperator definieren.
    Et vice versa.

    Du solltest also in der entsprechenden Klasse den Kopierkonstruktor selbst als defaulted definieren:

    interpolation(interpolation const&) = default; // In der Klasse
    

  • Mod

    Vielleicht solltest du mal erst C++ lernen, bevor du mit Parallelisierung anfängst. Das Stichwort der "special member functions" ist doch schon öfters gefallen. Hast du das nie nachgeschlagen? Wenn du einen move-Konstruktor oder move-Zuweisungsoperator deklariert hast, dann wird kein Kopierkonstruktor automatisch erzeugt. Das ist auch gut so, denn wenn es irgendwie Sinn macht, einen eigenen move zu schreiben, dann ist ein automatisch erzeugter Kopierkonstruktor sicher nicht das, was du willst.
    Genau das besagt die Fehlermeldung.

    Die Frage ist mal wieder, wieso du überhaupt einen move-Konstruktor definiert hast? Nutz doch die Standardbibliothek für dynamische Speicherverwaltung! Ganz besonders dann, wenn du nicht ganz sicher bist, was du tust. Du scheinst noch sehr unsicher auf dem Gebiet zu sein.



  • @Mods: Was gibt es hier zu löscheln?

    OpenMP würde ich nicht mehr zu lernen empfehlen. Siehe zum Beispiel http://stackoverflow.com/questions/13837696/can-i-safely-use-openmp-with-c11 : Der Konsens ist, dass OpenMP als eine fortran-like Erweiterung für C++98 entworfen wurde. Weder C++03 noch C++11 sind unterstützt, es ist nicht einmal geplant, sie irgendwann zu unterstützen. OpenMP bedeutet ausschliesslich OpenMP, es gibt keine Verbindung zu anderen Threading-Modellen. Wenn OpenMP eine Berechtigung hat, dann als Nischenlösung für wissenschaftliche Simulationen ö.Ä.

    Für "echtes" Threading gibt es C++11 oder TBB.


  • Mod

    ommp schrieb:

    @Mods: Was gibt es hier zu löscheln?

    Weil dein vorheriger Beitrag nur sinnloses Gebashe ohne Bezug zum Thema war? Du hättest auch genau so gut "OpenMP ist doof!!!!!1111elf" schreiben können. Im Prinzip war es das, was du geschrieben hast, bloß mit leicht anderen Worten. Dein neuer Beitrag ist wenigstens produktiv, wenn auch nur mit sehr indirektem Bezug zum Thema, den lasse ich gerne stehen.

    Wenn OpenMP eine Berechtigung hat, dann als Nischenlösung für wissenschaftliche Simulationen ö.Ä.

    Ich ging bis jetzt davon aus, dass der TE genau das macht. Das sieht mir doch sehr nach einem schnellen C++-Lernen für ein bestimmtes Ziel aus, ohne Plan zum richtigen, tiefgründigen Verstehen. Zum Beispiel eine Diplom-/Master-/Bachelorarbeit, wo diese Vorgehensweise leider Standard ist, da man eben unter Zeitdruck ist.



  • SeppJ schrieb:

    Ich ging bis jetzt davon aus, dass der TE genau das macht. Das sieht mir doch sehr nach einem schnellen C++-Lernen für ein bestimmtes Ziel aus, ohne Plan zum richtigen, tiefgründigen Verstehen. Zum Beispiel eine Diplom-/Master-/Bachelorarbeit, wo diese Vorgehensweise leider Standard ist, da man eben unter Zeitdruck ist.

    Äh ja, das trifft es ziemlich gut. Momentan habe ich die Anforderung: Binden wir meinen C++ Code doch an einen mittels Open MP parallelisierten Fortran Code an. 🙄

    Aber wenn ich es nicht genauer verstehen wollte, dann würde ich ja nicht so ausdauernd nachfragen, sondern so lange programmieren, bis die Compiler Warnungen verschwinden - oder? 😉

    Gruß,
    -- Klaus.


Anmelden zum Antworten