if-Anweisung ausschließlich zur Compilezeit auswerten



  • DocShoe schrieb:

    MIt C++17 gibt es if constexpr , wo dem Compiler erlaubt ist, den nicht benutzten Zweig nicht zu übersetzen.

    Falsch. Der Ausdruck muss eine constant expression sein. Der nicht genommene Zweig wird garantiert nicht instantiiert. Wäre sonst ein völlig inkohärentes Feature, weil die Wohlgeformtheit des Programs von der Qualität der Analyse abhängen würde....

    Ist es möglich, eine if-Anweisung zu erstellen, die entweder zur Compilezeit oder überhaupt nicht ausgewertet wird?

    Nein. 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.

    Deine Optimierung hat folglich gar keinen Zweck, oder ist unzulässig.

    alexa19 schrieb:

    D.h. für den allgemeinen Fall (wenn nicht geinlined wird) oder wenn zwar geinlined wird, aber nbits==16 nicht bewiesen werden kann (wie im zweiten Fall wegen dem volatile), muss die Funktion sich so verhalten, als sähe sie so aus:

    int foo(int nbits)
    {
      return 0;
    }
    

    Was ist denn das für ein Schwachsinn. Die Semantik deiner Funktion hängt völlig von der Raffinesse deiner Implementierung ab?



  • Dass unterschiedliche Optimierungsflags zu unterschiedlichen Ergebnissen führen können ist zu erwarten, aber auch hinnehmbar.

    Nein. Optimierungen sollen die Semantik eben nicht verändern. Optimierungen, die Semantik zugunsten der Performance verändern, werden zumindest in C++ so gut wie nie eingesetzt, aber selbst dann handelt es sich um relativ insignifikante Fließkomma-Abweichungen. Diese Abweichungen werden nicht so krass sein wie in deinem Beispiel (außer vielleicht in Fällen von numerischer Instabilität der optimierten Algos).

    Das ist kein Qualitätsmangel, sondern ein Fehler. Viel Spaß beim Debuggen, und so.

    Edit: Ein klein wenig die Aggressivität runtergefahren. Es handelte sich um ein Missverständnis. 🤡



  • Arcoth schrieb:

    Die Semantik deiner Funktion hängt völlig von der Raffinesse deiner Implementierung ab?

    Natürlich nicht, die Beispiele hier dienten der Illustration. Der eigentliche Zweck ist es natürlich, unter gewissen Bedingungen einen schnelleren Algorithmus bzw. eine alternative Operation auszuführen. Das Endergebnis bleibt dabei immer gleich, alles andere würde keinen Sinn machen.

    Die angestrebte Lösung macht es möglich, solche Unterscheidungsfälle auch in Funktionen zu verwenden, die viele hundert Millionen Mal pro Sekunde aufgerufen werden. Für spezielle Fälle lassen sich damit erhebliche Performancegewinne erzielen und zwar ohne dass dabei alle anderen Fälle langsamer werden.



  • alexa19 schrieb:

    Die angestrebte Lösung macht es möglich, solche Unterscheidungsfälle auch in Funktionen zu verwenden, die viele hundert Millionen Mal pro Sekunde aufgerufen werden.

    Du rufst eine Funktion Milliarden mal zur Compilezeit auf?



  • Arcoth schrieb:

    DocShoe schrieb:

    MIt C++17 gibt es if constexpr , wo dem Compiler erlaubt ist, den nicht benutzten Zweig nicht zu übersetzen.

    Falsch. Der Ausdruck muss eine constant expression sein. Der nicht genommene Zweig wird garantiert nicht instantiiert. Wäre sonst ein völlig inkohärentes Feature, weil die Wohlgeformtheit des Programs von der Qualität der Analyse abhängen würde....

    Das musste mal näher erklären. Wieso soll die Wohlgeformtheit leiden, wenn zusätzlicher Code in die Anwendung kompiliert wird, der zur Laufzeit nie ausgeführt wird?



  • Arcoth schrieb:

    Du rufst eine Funktion Milliarden mal zur Compilezeit auf?

    Aber nein. Es geht hier nicht um constexpr-Funktionen, falls du das meinst.
    Es geht um Fälle, wo man unter gewissen Bedingungen das gleiche auf eine effizientere Art tun kann, z.B.:

    `if (n==16)fast_algo_16();

    else generic_algo();`

    Wird so eine Funktion mit einer Konstante aufgerufen, ist alles ok - es wird zur Compilezeit entschieden, ob fast_algo_16() oder generic_algo() verwendet wird, es wird optimaler Code generiert.

    Wird die Funktion aber mit einem variablen/unbekannten n aufgerufen, wird eine cmp...jXX-Instruktionssequenz generiert. n ist nur mit einer gewissen Wahrscheinlichkeit 16, die Kosten dieser Instruktionen fallen aber für alle möglichen Werte von n an. Nun muss man von Fall zu Fall abwägen, ob der Geschwindigkeitsvorteil von fast_algo_16() unter Betrachtung der Wahrscheinlichkeit von 16 die Kosten der zusätzlichen Instruktionen aufwiegt.

    Hat man zu viele solcher Sonderfälle, verbringt das Programm schnell mehr Zeit mit solchen Überprüfungen als es damit verbringt, reale Arbeit zu tun. Eine gängige Variante ist daher, mehrere Funktionen bereitzustellen und die günstigste für den aktuellen Fall aufzurufen. Aber damit ist die Codequalität automatisch im Eimer. Womöglich kann es passieren, dass man den gesamten Code anpassen muss, nur weil man irgendwo eine ehemalige Compilezeit-Konstante in eine konfigurierbare Option umgewandelt hat. Das kommt für selbstrespektierende Entwickler nicht in Frage - so etwas ist die Aufgabe eines Compilers.

    Und damit wären wir bei __builtin_constant_p() . So etwas wie if (__builtin_constant_p(n) && n==16) wird immer zur Compilezeit zu true oder false ausgewertet, es wird daraus niemals auch nur eine einzige Maschineninstruktion generiert. Man kann jetzt völlig ungeniert beliebig viele Sonderfälle definieren und diese können immer nur zu einem Performancezuwachs führen, weil sich die Codeabschnitte in den ungünstigen Fällen vollständig in Luft auflösen. Sämtliche vorher notwendigen Abwägungen haben sich damit erledigt, denn die einzig mögliche Richtung ist bergauf.

    Das Resultat ist Code, der unglaublich schnell ist, wenn viel mit konstanten (und performancetechnisch günstigen) Argumenten gearbeitet wird - aber immer noch exakt so schnell wie vorher ist, wenn das nicht getan wird. Und es muss absolut nichts an der Codestruktur geändert werden, ich brauche keine zig Varianten der gleichen Funktion. Das erledigt nun der Compiler automatisch während des Inlinings. Und das ganze skaliert dynamisch mit der Güte des Optimizers und auch mit der Verfügbarkeit von __builtin_constant_p() - wenn ein Compiler das nicht kennt, kann man pauschal false annehmen und der zusätzliche Code stört den Compiler nicht weiter.
    Deswegen kann man das ganze im Lexikon unter dem Begriff "vollendete Perfektion" nachschlagen.



  • 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.

    alexa19 schrieb:

    Hat man zu viele solcher Sonderfälle, verbringt das Programm schnell mehr Zeit mit solchen Überprüfungen als es damit verbringt, reale Arbeit zu tun.

    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, und wenn das entsprechende Argument nicht krass unwahrscheinlich ist, hast Du am Ende hunderte Takte gespart. Wenn es also mindestens 1% der Inputs darstellt, was man bei einer solchen Optimierung ja annehmen sollte..

    Eine gängige Variante ist daher, mehrere Funktionen bereitzustellen und die günstigste für den aktuellen Fall aufzurufen. Aber damit ist die Codequalität automatisch im Eimer.

    So etwas nennt man Optimierung. Dass der Code dadurch an Schlichtheit verliert, ist "hinnehmbar", wie Du so schön sagst. Der Vorteil ist, dass wir mit unserem Verständnis der spezifischen call sites einige Fälle ausschließen können. Aber jetzt sprechen wir von Mikrooptimierung, da kann man auch gleich erstmal profilen gehen!

    Das Resultat ist Code, der unglaublich schnell ist, wenn viel mit konstanten (und performancetechnisch günstigen) Argumenten gearbeitet wird

    Schnell verglichen womit--dem gleichen Code, der kein __builtin_constant_p einsetzt? Kaum. Und abhängig von der Verteilung der Inputs ist er langsamer.

    PS: Deine Methode hat auch wenig mit Vollendung zu tun. Schau dir mal Supercompilation an. Du kannst nicht behaupten, die signifikantesten Konfigurationen zu optimieren.



  • alexa19 schrieb:

    Aber wenn ich mir so ansehe, was manche Leute hinbekommen, dann ist mir, als ob das Problem generall lösbar sein müsste.

    Der Artikel ist m.A.n. schlecht benannt. Es geht dort nicht um konstante Ausdrücke, die irgendwie nicht konstant wären, sondern dass der Wert bestimmte konstanter Ausdrücke auch davon abhängen kann, ob eine bestimmte constexpr-Funktion bereits zuvor definiert wurde, bzw. im Fall eines constexpr-Funktionstemplates ob eine bestimmte Spezialisierung bereits instantiiert wurde. Das ist exotisch, aber letztlich eine rein lexikalische Frage, wo im Quelltext der jeweilige Ausdruck auftaucht.
    Mit deinem Problem hat das sehr wenig zu tun.

    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.



  • 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.



  • 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.



  • 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?



  • 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.



  • 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.



  • 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.



  • 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.