if-Anweisung ausschließlich zur Compilezeit auswerten



  • 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