rand&co. (geteilt aus: Frage zu lvalue / rvalue Referenzen)



  • Timon Paßlick schrieb:

    Was machen die 2 Funktionen?

    Die erste soll einen gleichverteilten int im angegebenen Bereich liefern, die zweite einen gleichverteilten double.

    Vielleicht hätte ich die besser generate_uniform_int bzw. generate_uniform_double genannt.

    Man könnte dann z.B. double generate_gaus(double mean, double sigma) hinzufügen usw. Hebt man halt die Trennung von Generator und Distributionen ein bisschen auf, dafür bekommt man Syntax-Vervollständigung von der IDE, wenn man den "my_rand_object." tippt.

    Dann braucht man nämlich nur ein einfaches "Zufallsobjekt" und kann mit diesem beliebige Ranges an Zufallszahlen berechnen. Es erschien mit wenig sinnvoll, die distribution mit in das Objekt aufzunehmen*. Aber wie gesagt, wir sind dann dicht am Original dran. Lediglich das Initialisieren hätte man dann schön versteckt.

    * ich habe sogar mal einen Benchmark gesehen, wo es besser war, die distibution innerhalb einer Schleife zu erzeugen als außerhalb - offenbar hat der Optimizer das dann besser optimieren können. Daher keine Angst vorm Erzeugen der distribution-Objekte.



  • Ich finde das immer interessant, wie komisch das ist mit den Compilern. Die können immer andere Dinge gut optimieren, als man denkt. Mir wurde das z.B. so erklärt:
    Muss nicht kopiert werden -> Referenz setzen.
    Konstruktoren selten aufrufen.
    Ich bin richtig verwirrt. Muss man Assembler können und die Compiler haargenau kennen, um schnellen Code zu schreiben?
    Apropos: Warum überlegt der Compiler eigentlich nur, ob er Funktionen in einfachen Code umschreiben kann, wenn inline davorsteht? Und sollte ich vor jede Funktion inline schreiben?
    Edit: Ich weiß, das ist wieder ein anderes Thema, aber der Thread ist sowieso schon abgeschwiffen. 🙂



  • Timon Paßlick schrieb:

    Warum überlegt der Compiler eigentlich nur, ob er Funktionen in einfachen Code umschreiben kann, wenn inline davorsteht?

    Deine Annahme stimmt nicht.

    Siehe z.B. http://en.cppreference.com/w/cpp/language/inline
    Lies insbesondere den Text ab "The original intent"...

    Ähnlich wie das "register"-Keyword. Das nun in C++17 erstmal ganz ungenutzt ist.



  • Kann ich aus deiner Antwort schließen, dass inline irrelevant ist und ich es schreiben und lassen kann, wie ich möchte?



  • Nein. inline ist für Inlining irrelevant. Wenn du eine Funktion im Header definierst, so musst du nach wie vor inline schreiben, um die ODR nicht zu verletzen.
    Des weiteren gibts seit C++17 gibt auch noch inline Variablen.



  • Timon Paßlick schrieb:

    Ich finde das immer interessant, wie komisch das ist mit den Compilern. Die können immer andere Dinge gut optimieren, als man denkt. Mir wurde das z.B. so erklärt:
    Muss nicht kopiert werden -> Referenz setzen.
    Konstruktoren selten aufrufen.
    Ich bin richtig verwirrt. Muss man Assembler können und die Compiler haargenau kennen, um schnellen Code zu schreiben?

    Muss nicht, aber es hilft natürlich.
    Was wichtig ist, ist zu verstehen wie Computer "intern" arbeiten, wie lange bestimmte Dinge so ca. dauern, und dass man halbwegs einschätzen kann was ein C++ Compiler für Code generieren wird. Dabei hilft es natürlich ungemein wenn man Assembler kann - zumindest lesen.
    Das heisst jetzt nicht unbedingt dass man jede Instruktion kennen muss die eine bestimmte CPU so kann. Aber das Grobe, also wie Maschinencode so grundsätzlich funktioniert, das wäre schon gut.
    Und das ist auch nicht SO schwer zu lernen 😉

    Timon Paßlick schrieb:

    Apropos: Warum überlegt der Compiler eigentlich nur, ob er Funktionen in einfachen Code umschreiben kann, wenn inline davorsteht?

    Ohne spezielle, non-standard Settings, können Compiler nur Funktionen inlinen, deren Definition sie auch in der selben Translation-Unit "gesehen" haben.
    Wenn du also eine Funktion Foo in Foo.cpp definierst (und in Foo.h nur deklarierst), und Foo() dann in Bar.cpp aufrufst, dann kann der Compiler kein Inlining machen. Weil er die Definition von Foo() beim Übersetzen von Bar.cpp ja nicht sehen kann.

    Wenn du die entsprechenden Compiler-Optionen mitgibst, dann versucht der Compiler gleich gar keinen Code mehr zu erzeugen, sondern überlässt diese Aufageb dem Linker. Nennt sich "Whole Program Optimization" bzw. "Link Time Code Generation". Dabei kann dann auch der oben beschriebene Aufruf von Foo() in Bar.cpp geinlined werden.

    Und natürlich gibt es Compiler-Settings mit denen man dem Compiler explizit verbieten kann Funktionen zu inlinen die nicht mit "inline" definiert wurden. (MSVC zumindest hat so eine Option.)

    Timon Paßlick schrieb:

    Und sollte ich vor jede Funktion inline schreiben?

    Ich würde inline eher nur für Funktionen verwenden wo du es wegen der ODR verwenden musst.
    Wenn es super-wichtig ist dass die Funktion ganz sicher garantiert immer inline erweitert wird, dann kannst du BOOST_FORCEINLINE verwenden. Da bekommst du dann normalerweise ne Warning wenn die Funktion nicht inline erweitert wird (weil der Compiler es nicht kann).
    Damit kann man sich aber auch selbst in den Fuss schiessen, weil Inlining nicht immer gut ist. Kommt halt drauf an. Und wenn man nicht vor hat sich lange laaaaaaange damit zu beschäftigen, dann sollte man die Wahl besser dem Compiler überlassen.

    (Was dagegen noch öfter Sinn machen kann ist BOOST_NOINLINE - womit man dem Compiler verbietet eine bestimmte Funktion inline zu erweitern.)



  • Timon Paßlick schrieb:

    Ich bin richtig verwirrt. Muss man Assembler können und die Compiler haargenau kennen, um schnellen Code zu schreiben?

    Kommt darauf an, was du mit "schnell" meinst. Wenn du gut skalierende und solche Algorithmen und Datenstrukturen wählst, die zu deinem Problem passen, dann wirst du aller Wahrscheinlichkeit schon schneller sein als die meisten Konkurrenzprodukte (sofern es denn welche gibt). Und damit für die meisten Ansprüche "schnell genug".

    Darüber hinaus: ja, du musst nicht nur deine Compiler sehr gut kennen, sondern auch deine Zielarchitekturen. Dass du Assembler lesen können musst, ist klar, aber auch keine große Hürde.
    Wenn man die Hardware optimal ausnutzt, lassen sich zwar meist noch massive Performancesteigerungen erzielen. Selbst erfahrene Entwickler unterschätzen reihenweise das schiere Potential moderner Architekturen wie Haswell, Skylake oder auch Zen. Die Frage ist, ob sich das für dich lohnt - denn um die Hardware im Zusammenspiel mit dem Compiler optimal auszureizen, brauchst du Jahre an Erfahrung und Fachwissen. Zudem kommst du auch schon sehr weit, indem du Code von fremden Leuten mit Ahnung verwendest, beispielsweise Intels IPP.

    Aber: zumindest die Grundlagen sollte jeder Entwickler kennen, der kein Anfänger mehr ist. Alles andere halte ich fast schon für unverantwortlich.
    Du solltest wissen, welche grundlegende Arten von Optimierungen man von Compilern erwarten kann und du solltest grundlegendes Verständnis von CPU-Caches und der Pipeline haben.
    Mit diesem Wissen kannst du schon die gröbsten Fehler vermeiden.



  • Ich will mich ja auf x86(_64) spezialisieren. Dann hab ichs mit unterschiedlichen Prozessoren zu tun. Die muss ich aber nicht alle kennen, oder?



  • Timon Paßlick schrieb:

    Ich will mich ja auf x86(_64) spezialisieren. Dann hab ichs mit unterschiedlichen Prozessoren zu tun. Die muss ich aber nicht alle kennen, oder?

    Nein, nur die Architektur an sich ist wichtig (also z.B. Skylake) und nicht die konkreten Modelle. Unterschiede zwischen etwa Haswell und Skylake gibt es durchaus, aber im Schnitt sind die nicht dramatisch. Und erfreulicherweise soll sich die AMD Zen-Architektur an Haswell orientieren, sodass für Haswell kompilierte Programme gut auf Zen laufen sollen. Konnte ich mangels Zen-CPU noch nicht überprüfen, aber im Idealfall bedeutet das, dass man in Zukunft nicht mal mehr zwischen AMD und Intel unterscheiden muss.

    Also orientierst du dich immer an der neuesten Architektur, die du auch selbst testen kann, also meist der CPU deines Entwicklungsrechners.



  • hustbaer schrieb:

    Was wichtig ist, ist zu verstehen wie Computer "intern" arbeiten, wie lange bestimmte Dinge so ca. dauern, ...

    Da es scheinbar noch keiner gepostet hat:

    http://norvig.com/21-days.html#answers

    Neben der Verwendung effizienter Algorithmen ist diese Tabelle meiner Meinung nach eines der ersten Dinge,
    die man berücksichtigen sollte, bevor man allzu tief in die Details spezifischer Architekturen einsteigt.

    Auch cool: Visualisierung der Latenzen mit Slider, wo man schön sehen kann, wie sich die Werte über die Jahre in etwa verändert haben:

    https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html



  • Ist x86 eine Architektur oder bring ich was durcheinander?
    Und wenn ja, ist x86_64 eine andere Architektur?
    Und wenn nochmal ja, wie viel ist die anders?

    Die Tabelle ist übrigens cool. Endlich mal Zahlen. Auch wenn ich nicht bei jeder Zeile versteh, was das ist, es hilft mir.


  • Mod

    x86 ist ein gängiger Sammelbegriff für die 386-artigen 32-Bit Architekturen. Davon gibt es sehr viele Varianten, die aber alle halbwegs kompatibel sind, d.h. neuere Versionen davon können auch Code ausführen, der für ältere Varianten gedacht war.

    x86_64 ist die 64-Bit Variante davon. Ein herausragendes Feature dieser Architekturen ist, dass sie auch ganz gut mit Code für x86 zurecht kommen, was einer der Gründe ist, weshalb diese Architektur sich für Desktopcomputer durchgesetzt hat.



  • ...und da wir im C++-Forum sind, schau dir einfach mal die Optionen an, die es hier so gibt:
    https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Submodel-Options.html

    Du siehst, x86 ist hier ein Sammelbegriff für eine recht große Menge an Unteroptionen.



  • Wow... echt viel. Beeindruckend.



  • So, ich hab mir die Kritik zu Herzen genommen. Ruhig an dieser Version nochmal ein bisschen meckern, dann kann ich sie weiter verbessern. 🙂

    trandom.h (Timon Random):

    #pragma once
    
    int uniform_int_rand(const int range);
    int uniform_int_rand(const int low, const int high);
    

    trandom.cpp:

    #include <memory>
    #include <random>
    using namespace std;
    
    using engine_t = mt19937; //Für x64 mt19937_64 nehmen
    
    unique_ptr<engine_t> engine_ptr(nullptr);
    
    int uniform_int_rand(const int range)
    {
    	if (!engine_ptr)
    		engine_ptr = make_unique<engine_t>();
    	uniform_int_distribution<> distribution{ 0, range };
    	return distribution(*engine_ptr);
    }
    
    int uniform_int_rand(const int low, const int high)
    {
    	if (!engine_ptr)
    		engine_ptr = make_unique<engine_t>();
    	uniform_int_distribution<> distribution{ low, high };
    	return distribution(*engine_ptr);
    }
    


  • Entferne die globale Variable! Die hattest du vorher doch auch nicht!



  • Aber dann brauche ich wieder OOP.
    Die Globale nimmt doch nur ein paar Byte auf dem Stack in Anspruch.
    Edit: Ist ja am Anfang nur ein leerer unique_ptr.



  • Timon Paßlick schrieb:

    Aber dann brauche ich wieder OOP.
    Die Globale nimmt doch nur ein paar Byte auf dem Stack in Anspruch.
    Edit: Ist ja am Anfang nur ein leerer unique_ptr.

    Darum geht es nicht. Es geht darum dass globale Variablen bestimmte Probleme machen. Weil sie dem Benutzer die Möglichkeit nehmen für unterschiedliche Dinge unterschiedliche Instanzen zu verwenden.

    Überleg dir z.B. mal was passiert wenn jemand deine uniform_int_rand Funktionen gleichzeitig aus verschiedenen Threads aufruft.



  • Aus der Engine wird doch nur gelesen, oder?
    Das ist doch ne Liste von Zahlen, bei der man mit nem Seed bestimmt, wo man anfängt und dann geht es so im Kreis durch?



  • Timon Paßlick schrieb:

    Aus der Engine wird doch nur gelesen, oder?

    Dann nehmen die Funktionen, die aus der Engine lesen, doch bestimmt als Parameter eine const engine& ?

    Schau doch mal auf http://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution/operator() vorbei.

    Hm, da steht nur Generator& ohne const !

    Tja. Die Engine hat doch internen State! Ein mt19937 wird nach Initialisierung mit derselben Zahl immer dieselbe Reihenfolge weiterer Zahlen ausgeben. Das geht nur, weil jedes Lesen aus der Engine deren internen State ändert.


Anmelden zum Antworten