static initialization order fiasco



  • Hallo allerseits,

    falle ich mit folgendem Code dem static initialization order fiasco zum Opfer?

    Baz.h

    struct Baz
    {
        double v;
    };
    
    Baz foo(double input);
    Baz bar(double input);
    

    Baz.cpp

    #include "Baz.h"
    namespace
    {
        const double c1 = 42.0;
        const double c2 = c1 * c1;
        // ... viele weitere Konstanten, welche in den folgenden
        //     Funktionen zur Verfügung stehen sollen
    }
    
    Baz foo(double input)
    {
        static const double x1 = 123.0;
        static const double x2 = x1 * x1 + 456.0;
        // ... viele weitere Konstanten, die nur innerhalb von foo() benötigt werden
    
        Baz baz;
        baz.v = input + c1 + c2 + x1 + x2; // unglaublich lange und komplizierte Berechnung, welche
                                           // alle globalen Konstanten c_n und lokalen Konstanten x_n
                                           // enthält
        return baz;
    }
    
    Baz bar(double input)
    {
        static const double y1 = 123.0;
        static const double y2 = y1 * y1 + 456.0;
        // ... viele weitere Konstanten, die nur innerhalb von bar() benötigt werden
    
        Baz baz;
        baz.v = input + c1 + c2 + y1 + y2; // unglaublich lange und komplizierte Berechnung, welche
                                           // alle globalen Konstanten c_n und lokalen Konstanten y_n
                                           // enthält
        return baz;
    }
    

    main1.cpp

    #include "Baz.h"
    
    int main()
    {
    	Baz b = foo(999.0);
        return 0;
    }
    

    main2.cpp

    #include "Baz.h"
    
    Baz b = foo(999.0);
    
    int main()
    {
        return 0;
    }
    

    Ist das Design problematisch? Habt ihr bessere Vorschläge? main1.cpp dürfte ja unkritisch sein; wie siehts mit main2.cpp aus?



  • Die Konstanten im anonymen Namespace ( c1 , c2 , ...) werden alle per constant initialization initialisiert.

    N3337 [basic.start.init]/1 schrieb:

    Constant initialization is performed:

    • [...]
    • if an object with static or thread storage duration is not initialized by a constructor call and if every full-expression that appears in its initializer is a constant expression.

    Und daher werden sie vor b in main2.cpp initialisiert, da dieses Objekt dynamisch initialisiert wird.

    Together, zero-initialization and constant initialization are called static initialization; all other initialization is dynamic initialization. Static initialization shall be performed before any dynamic initialization takes place.

    Damit ist auch main2.cpp richtig.
    Mach auch ggf. die Konstanten constexpr .



  • Natürlich gilt das nur, solange tatsächlich nur konstante Ausdrücke vorkommen. In deinem Beispiel ist das der Fall. Um das zu erzwingen, qualifiziere die Konstanten nochmal mit constexpr (wenn dein Compiler genügend C++11 unterstützt). Sollte da aber irgendwo = std::sin(5.) stehen, hast du AFAICS ein Problem.



  • Sone schrieb:

    Sollte da aber irgendwo = std::sin(5.) stehen, hast du AFAICS ein Problem.

    Nicht, wenn sin ein Intrinsic ist.
    Wird wohl mit jedem Compiler gehen, aber verlassen kann man sich darauf nicht.



  • Danke für die Anworten. Also wenn ich in den Assembler-Output schaue, dann werden trotz voller Optimierungen nicht sämtliche Konstanten im unnamed namespace constant initialized. Bei manchen steht

    c5$initializer$ DQ FLAT:void __cdecl `anonymous namespace'::`dynamic initializer for 'c5''(void)
    

    und dann später halt die entsprechenden Anweisungen zum berechnen der Werte. Das wundert mich auch sehr, dass hier die constant propagation nicht so funktioniert, wie ich mir das gedacht hatte. VS2012 scheint die double-Konstanten so ca. 3 Ausdrücke weiterzupropagieren, die dann auch alle schön brav zur Compilezeit berechnet werden, aber nachfolgende (immer noch const ) Konstanten werden nicht mehr zur Compilezeit bestimmt. Und das alles, obwohl ich zur Berechnung der Konstanten nur +-*/ verwende (also nix mit Sinus o.ä.).

    C++11/constexpr steht mir leider nicht zur Verfügung.



  • Hm, ist meine Frage so schwierig, dass keiner sich mehr traut was zu schreiben, oder ist die Antwort so offensichtlich? 😕

    Wäre für weitere Kommentare sehr dankbar!

    Grüße


  • Mod

    Zeig mal den Code zu den Initialisierungen, die nicht optimiert werden.


  • Mod

    Gleitkommaliterale sind in C++03 die einzigen konstanten Ausdrücke mit Gleitkommatyp.
    Entsprechend ist die Initialisierung von c2/x2/y2 in C++03 dynamisch.
    Natürlich könnte ein Compiler die Initialisierung trotzdem statisch durchführen. Traditionell wurde das aber nichtz gemacht, weil es unterschiedliche Implementationen für Gleitkommaarithmetik gibt, und der Compiler diese (ggf. unterschiedlich je nach Zielplattform) implementieren müsste.

    In C++11 ist das anders, dort wird aber eben auch nicht ausdrücklich verlangt (allerdings nach Möglichkeit empfohlen), dass das Ergebnis einer Berechnung beim Compilieren mit dem einer Berechnung erst beim Programmablauf exakt übereinstimmen muss.



  • SeppJ schrieb:

    Zeig mal den Code zu den Initialisierungen, die nicht optimiert werden.

    namespace
    {
    	const double c1 = 123.0;
    	const double c2 = 456.0;
    	const double c3 = c1 * (c2 - 1.0) / c2;
    	const double c4 = ((c1 * c1) - (c3 * c3)) / (c1 * c1);
    }
    
    int main()
    {
    	return c4;
    }
    

    c3 schafft er noch, c4 nicht mehr:

    ; Listing generated by Microsoft (R) Optimizing Compiler Version 17.00.60315.1 
    
    include listing.inc
    
    INCLUDELIB OLDNAMES
    
    c1	DQ	0405ec00000000000r		; 123
    c2	DQ	0407c800000000000r		; 456
    CONST	ENDS
    PUBLIC	main
    PUBLIC	__real@40cd8c8000000000
    EXTRN	_fltused:DWORD
    c4	DQ	01H DUP ()
    _BSS	ENDS
    CRT$XCU	SEGMENT
    c4$initializer$ DQ FLAT:void __cdecl `anonymous namespace'::`dynamic initializer for 'c4''(void)
    CRT$XCU	ENDS
    ;	COMDAT __real@40cd8c8000000000
    CONST	SEGMENT
    __real@40cd8c8000000000 DQ 040cd8c8000000000r	; 15129
    c3	DQ	0405eaebca1af286cr		; 122.73
    _DATA	ENDS
    ; Function compile flags: /Ogtpy
    ; File c:\dev\test\test.cpp
    ;	COMDAT void __cdecl `anonymous namespace'::`dynamic initializer for 'c4''(void)
    text$yc	SEGMENT
    void __cdecl `anonymous namespace'::`dynamic initializer for 'c4''(void) PROC			; `anonymous namespace'::`dynamic initializer for 'c4'', COMDAT
    
    ; 6    : 	const double c4 = ((c1 * c1) - (c3 * c3)) / (c1 * c1);
    
    	movsdx	xmm2, QWORD PTR c3
    	movsdx	xmm1, QWORD PTR __real@40cd8c8000000000
    	mulsd	xmm2, xmm2
    	subsd	xmm1, xmm2
    	divsd	xmm1, QWORD PTR __real@40cd8c8000000000
    	movsdx	QWORD PTR c4, xmm1
    	ret	0
    void __cdecl `anonymous namespace'::`dynamic initializer for 'c4''(void) ENDP			; `anonymous namespace'::`dynamic initializer for 'c4''
    text$yc	ENDS
    ; Function compile flags: /Ogtpy
    ; File c:\dev\test\test.cpp
    ;	COMDAT main
    _TEXT	SEGMENT
    main	PROC						; COMDAT
    
    ; 11   : 	return c4;
    
    	cvttsd2si eax, QWORD PTR c4
    
    ; 12   : }
    
    	ret	0
    main	ENDP
    _TEXT	ENDS
    END
    

    camper schrieb:

    Gleitkommaliterale sind in C++03 die einzigen konstanten Ausdrücke mit Gleitkommatyp.
    Entsprechend ist die Initialisierung von c2/x2/y2 in C++03 dynamisch.
    Natürlich könnte ein Compiler die Initialisierung trotzdem statisch durchführen. Traditionell wurde das aber nichtz gemacht, weil es unterschiedliche Implementationen für Gleitkommaarithmetik gibt, und der Compiler diese (ggf. unterschiedlich je nach Zielplattform) implementieren müsste.

    In C++11 ist das anders, dort wird aber eben auch nicht ausdrücklich verlangt (allerdings nach Möglichkeit empfohlen), dass das Ergebnis einer Berechnung beim Compilieren mit dem einer Berechnung erst beim Programmablauf exakt übereinstimmen muss.

    Okay, das erklärt einiges. Und was heißt das jetzt bezogen auf meine ursprüngliche Frage? Habe ich also in der Tat bei main2.cpp ein Problem? Oder gibts hier sowas wie "beim ersten Aufruf einer Funktion aus einer Übersetzungseinheit sind alle statischen Variablen dieser Übersetzungseinheit initialisiert"? Ich meine, ich hätte im C++-Standard mal irgendwas von Initialisierung statischer Variablen bevor erster ODR-Nutzung einer Funktion in derselben Übersetzungseinheit gelesen. Habe es aber ehrlich gesagt nicht wirklich geblickt.

    Was wäre denn ein gutes alternatives Design (C++03), um einer Übersetzungseinheit eine gewisse Menge (ca. 10-20) von Konstanten zur Verfügung zu stellen, welche von mehreren Funktionen dieser Übersetzungseinheit genutzt wird?


  • Mod

    Bloops schrieb:

    Und was heißt das jetzt bezogen auf meine ursprüngliche Frage? Habe ich also in der Tat bei main2.cpp ein Problem?

    ja.

    Bloops schrieb:

    Oder gibts hier sowas wie "beim ersten Aufruf einer Funktion aus einer Übersetzungseinheit sind alle statischen Variablen dieser Übersetzungseinheit initialisiert"? Ich meine, ich hätte im C++-Standard mal irgendwas von Initialisierung statischer Variablen bevor erster ODR-Nutzung einer Funktion in derselben Übersetzungseinheit gelesen.

    Sofern main bereits betreten wurde, ist das garantiert.

    Bloops schrieb:

    Was wäre denn ein gutes alternatives Design (C++03), um einer Übersetzungseinheit eine gewisse Menge (ca. 10-20) von Konstanten zur Verfügung zu stellen, welche von mehreren Funktionen dieser Übersetzungseinheit genutzt wird?

    Spricht etwas dagegen, die Konstanten einfach in einem Header zu definieren (und auf external linkage zu verzichten)?



  • camper schrieb:

    Spricht etwas dagegen, die Konstanten einfach in einem Header zu definieren (und auf external linkage zu verzichten)?

    Meinst du mit static const double ... ? Hab ich dann nicht das gleiche Problem? Dann "sehen" die einzelnen Übesetzungseinheiten doch immer noch verschiedene Konstanten - oder meinst du was anderes?

    Die Idee war halt, die (Berechnung der) Konstanten in der Bibliothek komplett wegzukapseln. Ich könnte natürlich die Konstanten auch separat vorberechnen und wirklich nur die Gleitkommaliterale dann in C++ verwenden. Würde mich das dann vor der Initialisierungsproblematik bewahren? Wäre aber halt nicht so elegant.


Anmelden zum Antworten