if-Anweisung ausschließlich zur Compilezeit auswerten



  • Arcoth schrieb:

    Und es muss absolut nichts an der Codestruktur geändert werden, ich brauche keine zig Varianten der gleichen Funktion.

    ...außer den zig Varianten, die Du ohnehin definiert hast.

    Die gibt es nicht.

    Arcoth schrieb:

    Wenn du mittels __builtin_expect die Branch weights setzt, dann wird der Prozessor spekulativ den Standardpfad (not taken) ausführen. Wir verlieren damit im Normalfall quasi überhaupt nichts, weil die Branch einfach zusammen mit einer anderen Instruktion ausgeführt wird (superscalar), oder vielleicht einen Takt in der pipeline. Nichts, was in einem komplexen Algo mit mindestens Hunderten Takten eine Auswirkung haben sollte

    Wir reden von Funktionen, die im Schnitt 0.5-10 Takte dauern, vielleicht mal 50, sagte ich ja bereits ("hunderte Millionen Mal pro Sekunde"). Auch wenn die branch prediction immer richtig liegt, kommt es zur erheblichen Einbußen. __builtin_expect verwende ich bereits für Fälle, die sehr oft/selten zutreffen.

    Arcoth schrieb:

    So etwas nennt man Optimierung. Dass der Code dadurch an Schlichtheit verliert, ist "hinnehmbar", wie Du so schön sagst.

    Nein, schlechter und schwer zu wartender Code ist nicht hinnehmbar. Man kann Code schreiben, der wartbar ist und trotzdem in allen Situationen die maximal mögliche Performance aufweist. Darum geht es hier ja. Wenn ich irgendwo eine globale Konstante wie coordsPerMeter von 32 auf 64 ändere kann es nicht sein, dass der Code auf einmal an allen Ecken zusammenbricht. Auch dann nicht, wenn ich sie auf 50 ändere. Ich kann mir zwar ausmalen, dass die Performance etwas sinkt, weil an verschiedenen Stellen nun multipliziert und dividiert wird anstatt dass bitshifting verwendet wird, aber das ist hinnehmbar wenn ich unbedingt 50 haben möchte. Ebenso wäre diese Konsequenz hinnehmbar, wenn ich aus der Konstante eine dynamische Variable mache. Nicht hinnehmbar ist es dagegen, wenn ich aufgrund einer solchen Änderung auf einmal überall Codereparaturen anstellen muss.
    Ebenso unnötig ist es, aufgrund von Performanceoptimierungen ganze Funktionshierarchien zu klonen, nur dass man dann an einigen bedeutenden Stellen etwas verändert - vor allem, wenn es auch anders geht, wie ich bereits dargelegt habe.

    Arcoth schrieb:

    Aber jetzt sprechen wir von Mikrooptimierung, da kann man auch gleich erstmal profilen gehen!

    Darum ging es von Anfang an. Selbst ein halber gesparter Takt in einer low level-Funktion hat großen Einfluß auf die Gesamtperformance. Und ich kann dir ja den 10.000sten Profilingvorgang widmen. Vielleicht ist er auch schon vorüber, ich habe die Jahre über nicht mitgezählt.

    Arcoth schrieb:

    Schnell verglichen womit--dem gleichen Code, der kein __builtin_constant_p einsetzt? Kaum.

    Verglichen mit dem Code, der keine Sonderfälle kennt. Dass mit den Überprüfungen im konstanten Fall immer optimaler Code erzeugt wird, habe ich bereits geschrieben. Ist jetzt aber auch keine weltbewegende Erkenntnis und darum geht es nicht.

    Arcoth schrieb:

    Und abhängig von der Verteilung der Inputs ist er langsamer.

    Nein. Außer du meinst den Fall, wo die Inputs zwar zur Compilezeit unbekannt aber aufgrund der Verteilung trotz Overhead von den Sonderbehandlung profitieren würden. Diese Fälle gibt es zwar (vor allem je teurer die Funktionen werden), die können aber auch weiterhin ohne __builtin_constant_p behandelt werden. Es geht um Fälle, wo das bisher nicht profitabel war, also vor allem bei der untersten Ebene des Codes in low level-Routinen.

    camper schrieb:

    Es existiert kein Mechanismus in Standard C++, der Funktionalität äquivalent zu __builtin_contant_p bietet.
    N3583 diskutiert die Problematik, allerdings scheint diesbzgl. in der Zwischenzeit nichts passiert zu sein.

    Danke, dann muss ich mir über eine standardkonforme Lösung keine Gedanken machen. Da __builtin_constant_p einen graceful fallback ermöglicht, ist das auch überhaupt kein Problem.

    Das Thema wäre damit erfolgreich abgehakt.


  • Mod

    Arcoth schrieb:

    Wenn du mittels __builtin_expect die Branch weights setzt, dann wird der Prozessor spekulativ den Standardpfad (not taken) ausführen. Wir verlieren damit im Normalfall quasi überhaupt nichts, weil die Branch einfach zusammen mit einer anderen Instruktion ausgeführt wird (superscalar), oder vielleicht einen Takt in der pipeline. Nichts, was in einem komplexen Algo mit mindestens Hunderten Takten eine Auswirkung haben sollte

    Wir reden von Funktionen, die im Schnitt 0.5-10 Takte dauern, vielleicht mal 50, sagte ich ja bereits ("hunderte Millionen Mal pro Sekunde").

    Funktionen, die 0.5 Takte dauern? Bist Du jetzt senil geworden? ... die i7 pipeline besteht aus etwa einem Dutzend stages, und deine Funktion braucht nicht einmal instruction fetch? Darüber hinaus interessant, dass du die Naturgesetze so umbiegen kannst, dass deine Funktion vor der clock edge zurückspringt..

    Nein, schlechter und schwer zu wartender Code ist nicht hinnehmbar.

    Niemand hat behauptet, dass Code, der nicht schlicht ist, schlecht ist oder schwer zu warten. Lege mir keine Worte in den Mund.

    Man kann Code schreiben, der wartbar ist und trotzdem in allen Situationen die maximal mögliche Performance aufweist.

    [citation needed] "Maximal mögliche performance" kann nur ein ignoranter Naivling von seiner Software behaupten.

    Nicht hinnehmbar ist es dagegen, wenn ich aufgrund einer solchen Änderung auf einmal überall Codereparaturen anstellen muss.

    Doch, völlig hinnehmbar. Weil Du offenbar wesentliche, bedeutende Optimierungen vorgenommen hast, die von der Konstanz dieses Wertes abhingen. Wenn deine Optimierungen so unbedeutend sind, dass eine Anpassung ihrer Vorkommnisse dir unangemessene Arbeit bereitet, solltest du den Sinn dieser Optimierung überdenken.

    Ebenso unnötig ist es, aufgrund von Performanceoptimierungen ganze Funktionshierarchien zu klonen, nur dass man dann an einigen bedeutenden Stellen etwas verändert - vor allem, wenn es auch anders geht, wie ich bereits dargelegt habe.

    Ja, oder wenn man einfach nicht behauptet, Funktionen optimieren zu müssen, die einen halben Takt dauern.

    Arcoth schrieb:

    Aber jetzt sprechen wir von Mikrooptimierung, da kann man auch gleich erstmal profilen gehen!

    Darum ging es von Anfang an. Selbst ein halber gesparter Takt in einer low level-Funktion hat großen Einfluß auf die Gesamtperformance.

    Tatsächlich, denn nun braucht die Funktion 0 Takte!



  • Arcoth schrieb:

    Genauso wie man nicht zwei Versionen von Funktionen anbieten kann, eine die zur Compile- und eine die zur Laufzeit aufgerufen werden soll. Weil die Semantik einer Funktion identisch bleiben soll.

    Was aber IMO recht praktisch wäre. Was die Gefahr von unterschiedlichem Verhalten angeht: das selbe Problem ergibt sich doch auch bei anderen Dingen wo C++ nicht davor zurückschreckt sie zu machen/erlauben. z.B. const Overloads, T() + U() vs. U() + T() , + vs. += etc.

    Warum dann nich auch constexpr vs. non- constexpr ?



  • Arcoth schrieb:

    Funktionen, die 0.5 Takte dauern? Bist Du jetzt senil geworden? ... die i7 pipeline besteht aus etwa einem Dutzend stages, und deine Funktion braucht nicht einmal instruction fetch? Darüber hinaus interessant, dass du die Naturgesetze so umbiegen kannst, dass deine Funktion vor der clock edge zurückspringt..

    0.5 Latency wäre komisch.
    0.5 Throughput ist mit Inlining überhaupt kein Problem. Ohne ... weiss nicht, will mich nicht zu sehr aus dem Fenster lehnen. Schätze mal nicht.


  • Mod

    Wenn ich es richtig verstehe, geht es hier ja darum, eine constexpr-Ausdruck bei Funktionsaufrufen als solchen weiterzureichen.
    Schematisch:

    void f(int n, Args...) {
        if (__builtin_constant_p(n) && n == 16) {
        ... // fast code
        } else {
        ... // slow general code
        }
    }
    void g(int n, Args...) {
       ...
       f(n, ...);
       ...
    }
    void h(int n, Args...) {
       ...
       g(n, ...);
       ...
    }
    usw.
    Aufrufer:
       h(x);  // general code
       h(16); // fast code (hopefully!)
    

    Das können wir bereits mit relativ geringem Änderungsaufwand erreichen:

    template <typename T> // T = scalar or a spezialisation of std::integral_constant
    void foo(T n, Args...) {
        if constexpr (T{} == 16) {
            ... // fast code
        } else {
            ... // slow general code
        }
    }
    
    template <typename T>
    void g(T n, Args...) {
       ...
       f(n, ...);
       ...
    }
    template <typename T>
    void h(T n, Args...) {
       ...
       g(n, ...);
       ...
    }
    usw.
    Aufrufer:
       h(x);  // general code
       h(std::integral_constant<int, 16>{}); // fast code
       h(std::integral_constant<int, 8>{}); // general code
    

    Beim Aufruf ist ja statisch bekannt, ob ein bestimmter Ausdruck konstant ist oder nicht, so dass dort kein Test erforderlich ist.



  • Arcoth schrieb:

    Wenn du mittels __builtin_expect die Branch weights setzt, dann wird der Prozessor spekulativ den Standardpfad (not taken) ausführen. Wir verlieren damit im Normalfall quasi überhaupt nichts, weil die Branch einfach zusammen mit einer anderen Instruktion ausgeführt wird (superscalar), oder vielleicht einen Takt in der pipeline.

    __builtin_expect funktioniert je nach CPU gar nicht oder nur beim 1. mal, also mit kaltem Branch-Prediction Cache. Neuere Intel CPUs gehören z.B. in die 1. Kategorie, die machen einfach 50/50. IIRC verwenden die einfach das Ergebnis aus dem Cache, egal ob der Tag passt oder nicht.

    Möglicherweise gibt es eine dritte Kategorie CPUs, die bei __builtin_expect immer die angegebene Variante spekulieren, aber solche wären mir noch nicht untergekommen.

    Und generell verstehe ich nicht ganz wieso du dich so aufregst. Die Art Optimierungen die alexa19 machen möchte sind schliesslich etwas was Compiler andauernd machen. So Sachen wie Division durch Multiplikation + Shift zu ersetzen, Multiplikation oder Division durch Shift, Modulo durch AND usw. Wieso sollte das nur der Compiler "dürfen"?



  • @alexa19
    Kennst du Compiler-Explorer? Falls nicht, angucken: https://godbolt.org
    Da kannst du dir "live" angucken was verschiedene Compiler bei verschiedenen C++ Konstrukten so für Code generieren. Sehr hilfreich wenn man Mikro-Optimierungen von so Mini-Funktionen machen will.

    Und: Mich würde interessieren um was für Funktionen es dir geht. Kannst du 1-2 konkrete Beispiele bringen?


  • Mod

    hustbaer schrieb:

    Arcoth schrieb:

    Wenn du mittels __builtin_expect die Branch weights setzt, dann wird der Prozessor spekulativ den Standardpfad (not taken) ausführen. Wir verlieren damit im Normalfall quasi überhaupt nichts, weil die Branch einfach zusammen mit einer anderen Instruktion ausgeführt wird (superscalar), oder vielleicht einen Takt in der pipeline.

    __builtin_expect funktioniert je nach CPU gar nicht oder nur beim 1. mal, also mit kaltem Branch-Prediction Cache. Neuere Intel CPUs gehören z.B. in die 1. Kategorie, die machen einfach 50/50. IIRC verwenden die einfach das Ergebnis aus dem Cache, egal ob der Tag passt oder nicht.

    Ok, vergessen wir den Intrinsic. Moderne CPUs haben einen globalen branch predictor, und an dem sollte man sowieso nicht schrauben, weil er eben zur Laufzeit Korrelationen findet. So oder so wird eine korrekt vorausgesagte Branch sehr günstig sein.

    Und generell verstehe ich nicht ganz wieso du dich so aufregst. Die Art Optimierungen die alexa19 machen möchte sind schliesslich etwas was Compiler andauernd machen. So Sachen wie Division durch Multiplikation + Shift zu ersetzen, Multiplikation oder Division durch Shift, Modulo durch AND usw. Wieso sollte das nur der Compiler "dürfen"?

    Darum geht es doch gar nicht. Der TE spricht von Funktionen, die sehr, sehr kurz sind. Wo ich gar keine Motivation für eine Optimierung sehe, weil sie so simpel sind. Kurz gesagt, was er von sich gibt, scheint irgendwie fusselig und unfundiert. Gleichzeitig ist er sehr überzeugt von sich, und behauptet, ein Panakeia gefunden zu haben, und erwähnt "maximal mögliche Performance". Bin echt empört über diese Keckheit!

    hustbaer schrieb:

    Arcoth schrieb:

    Genauso wie man nicht zwei Versionen von Funktionen anbieten kann, eine die zur Compile- und eine die zur Laufzeit aufgerufen werden soll. Weil die Semantik einer Funktion identisch bleiben soll.

    Was aber IMO recht praktisch wäre. Was die Gefahr von unterschiedlichem Verhalten angeht: das selbe Problem ergibt sich doch auch bei anderen Dingen wo C++ nicht davor zurückschreckt sie zu machen/erlauben. z.B. const Overloads, T() + U() vs. U() + T() , + vs. += etc.

    Was wenn der Compiler nun feststellt, dass er einen gewissen Ausdruck, der nicht als core constant expression gewertet wird, zur Compilezeit auswerten könnte? Welche Funktion ruft er nun auf? Plötzlich von der non-compile-time abrücken und die compile-time Funktion aufrufen, und die Semantik des Programs klandestin ändern?

    Warum dann nich auch constexpr vs. non- constexpr ?

    Es wurde wahrscheinlich schon angesprochen, aber andererseits ist der Anreiz einfach zu schwach für eine Diskussion im Komitee.

    @camper: Du scheinst das Problem missverstanden zu haben. Der TE hat doch deutlich gesagt, dass ihn gerade die Abhängigkeit von Dingen wie Compilezeit-Konstanz nicht an der call site nerven soll, sodass er beim ändern eines Parameters nicht überall nachjustieren muss.


  • Mod

    Arcoth schrieb:

    @camper: Du scheinst das Problem missverstanden zu haben. Der TE hat doch deutlich gesagt, dass ihn gerade die Abhängigkeit von Dingen wie Compilezeit-Konstanz nicht an der call site nerven soll, sodass er beim ändern eines Parameters nicht überall nachjustieren muss.

    Wirklich? Selbst __builtin_constant_p funktioniert nur, wenn auf irgendeiner höheren Ebene ein konstanter Ausdruck vorliegt. Die ganze Mechanik dahinter besteht letzlich nur darin, die Faltung des Ausdrucks so lange wie möglich (insb. erst nach versuchtem Inlining) herauszuzögern.

    Nehme ich das Beispiel, das er vorher gebracht hat:

    #include <cassert>
    #include <utility>
    
    // anti-nerv, ggf. alternativ UD-Literal benutzen
    template <int i>
    constexpr auto int_ = std::integral_constant<int, i>{};
    
    template <typename T>
    int foo(T nbits)
    {
      if  constexpr (T{} == 16)
          return 4;
      else
          return 2;
    }
    
    int main()
    {
      auto nbits=int_<16>;
      assert(foo(nbits)==4);
    
      volatile int nbits2=int_<16>;
      assert(foo(nbits2)==2);
    
      return foo(nbits)*10+foo(nbits2);
    }
    

    Die Änderung ist so simpel, dass man sie fast automatisieren könnte.


  • Mod

    camper schrieb:

    Arcoth schrieb:

    @camper: Du scheinst das Problem missverstanden zu haben. Der TE hat doch deutlich gesagt, dass ihn gerade die Abhängigkeit von Dingen wie Compilezeit-Konstanz nicht an der call site nerven soll, sodass er beim ändern eines Parameters nicht überall nachjustieren muss.

    Wirklich? Selbst __builtin_constant_p funktioniert nur, wenn auf irgendeiner höheren Ebene ein konstanter Ausdruck vorliegt. Die ganze Mechanik dahinter besteht letzlich nur darin, die Faltung des Ausdrucks so lange wie möglich (insb. erst nach versuchtem Inlining) herauszuzögern.

    Es geht hier eher um Syntax ( __builtin_constant_p "funktioniert" vs "kompiliert"). D.h. das Problem dass du in deinem Beispiel löst. Ob ihm das reicht, bezweifle ich, sonst hätte ich es auch schon vorgeschlagen.


  • Mod

    hustbaer schrieb:

    0.5 Throughput ist mit Inlining überhaupt kein Problem.

    Wie, Du zählst Funktionen, die gar nicht tatsächlich aufgerufen werden?



  • Arcoth schrieb:

    hustbaer schrieb:

    0.5 Throughput ist mit Inlining überhaupt kein Problem.

    Wie, Du zählst Funktionen, die gar nicht tatsächlich aufgerufen werden?

    Nur um Misverständnisse zu vermeiden: ich meine

    inline void increment(int& i) { i++; }
    

    oder was ähnliches.
    So eine Funktion hat, wenn sie inlined wird, auf aktuellen CPUs vermutlich irgendwo zwischen 0.1 und 0.5 Zyklen "Throughput".
    Wenn der OP Funktionen meint die non-inline aufgerufen werden und es wirklich nur um 2-3 Sonderfälle pro Funktion geht, dann gebe ich dir Recht, dann ist das Unterfangen vermutlich halbwegs sinnfrei.

    Und ja, natürlich zähle ich die. Ein typisches C++ Programm besteht zum Grossteil aus solchen Funktionen - auf jeden Fall wenn man die Verwendung von STL Funktionen/Klassen mitrechnet.

    Arcoth schrieb:

    Und generell verstehe ich nicht ganz wieso du dich so aufregst. Die Art Optimierungen die alexa19 machen möchte sind schliesslich etwas was Compiler andauernd machen. So Sachen wie Division durch Multiplikation + Shift zu ersetzen, Multiplikation oder Division durch Shift, Modulo durch AND usw. Wieso sollte das nur der Compiler "dürfen"?

    Darum geht es doch gar nicht.

    Worum geht es nicht?

    Arcoth schrieb:

    Der TE spricht von Funktionen, die sehr, sehr kurz sind. Wo ich gar keine Motivation für eine Optimierung sehe, weil sie so simpel sind.

    Naja ne Multiplikation ist auch recht kurz, könnte man auch sagen das hat doch gar keinen Sinn daran 'was zu optimieren.

    Arcoth schrieb:

    Kurz gesagt, was er von sich gibt, scheint irgendwie fusselig und unfundiert. Gleichzeitig ist er sehr überzeugt von sich, und behauptet, ein Panakeia gefunden zu haben, und erwähnt "maximal mögliche Performance". Bin echt empört über diese Keckheit!

    Achje, ja, ich verstehe dass dich das nervt. Mich in diesem Fall ausnahmsweise mal nicht. Er will je niemandem von uns was einreden, er freut sich bloss über seine Idee und dass er damit bestimmte Dinge optimieren kann. Ob es dann was bringt und wie fundiert seine Aussagen sind bzw. eben nicht ist mir dann wörscht - betrifft letztendlich ja nur ihn.

    Arcoth schrieb:

    Was wenn der Compiler nun feststellt, dass er einen gewissen Ausdruck, der nicht als core constant expression gewertet wird, zur Compilezeit auswerten könnte? Welche Funktion ruft er nun auf? Plötzlich von der non-compile-time abrücken und die compile-time Funktion aufrufen, und die Semantik des Programs klandestin ändern?

    Das müsste man definieren. Ich sehe jetzt kein Problem mit einer der beiden Varianten. Ist jetzt nicht wirklich 'was grundlegend anderes als (N)RVO/Copy-Elision in C++98. Da entscheidet auch der Compiler einfach so "die Semantik des Programs klandestin zu ändern". Bzw. halt nicht, nämlich dann wenn der Programmierer sicherstellt dass die Semantik dadurch nicht verändert wird.

    hustbaer schrieb:

    Es wurde wahrscheinlich schon angesprochen, aber andererseits ist der Anreiz einfach zu schwach für eine Diskussion im Komitee.

    Ja, OK. Dein Argument war aber ein anderes 😉 Ich wollte ledliglich aufzeigen dass C++ bereits an vielen Stellen solche Dinge erlaubt, wo mal die eine mal die andere Implementierung einer Funktion aufgerufen wird, je nachdem ob z.B. eben const oder was auch immer.

    ----

    Und nochmal, nur um sicherzugehen dass wir hier nicht von zwei unterschiedlichen Dingen sprechen...
    Wenn der OP schreibt er will sowas wie

    int foo(int nbits)
    {
      if  (__builtin_constant_p(nbits) && nbits==16)return 4;
      return 2;
    }
    

    machen, dann gehe ich davon aus dass er in wirklichkeit sowas wie

    int foo(int nbits)
    {
      if  (__builtin_constant_p(nbits) && nbits==16)return 2;
      return nbits / 8; // In diesem Fall würde der Compiler die Optimierung für ihn machen,
                        // aber es wird wohl Fälle geben wo der Compiler aussteigt.
    }
    

    meint. Und die 4 vs. 2 nur hingeschrieben hat weil es dadurch einfach möglich ist zu gucken was der Compiler nun gemacht hat.
    Denn er schreibt ja dass es ihm darum geht das Programm lesbar/wartbar zu halten. Was nicht gehen wird wenn die zwei Implementierungen wirklich unterschiedliche Ergebnisse liefern.


Anmelden zum Antworten