Caching: volatile, critical section, gar nichts?



  • Hallo!
    Ich habe des öfteren gelesen, dass eine Variable "gecached" werden kann. Dies soll bei Multicore-Systemen Probleme bereiten können.

    Nehmen wir mal diesen Code:
    (Hier wird ja eigentlich kein Lock benötigt, da kontrolliert zuerst der Mainthread zuweist, dann ein zweiter Thread gestartet wird, der auch zuweist, während der Mainthread aber auf diesen wartet! Aber sieht der Mainthread nach dem Wait.. auch die aktualisierten Werte?)

    class ThreadTest
    {
    	int x = 42;
    	std::string s = "jo";
    public:
    	void Start()
    	{
            // Mainthread
    		x = 43;
    		s = "jop";
    
            void* thread = (void*)_beginthreadex(0, 0, StartThread, this, 0, 0);
    		WaitForSingleObject(thread, INFINITE);
            CloseHandle(thread);
    
            // Werte aktuell? Oder vielleicht noch gecached?
    		cout << x << endl;
    		cout << s << endl;
    	}
    private:
    	static unsigned int __stdcall StartThread(void* param)
    	{
    		((ThreadTest*)param)->Thread();
    		return 0;
    	}
    
    	void Thread()
    	{
            // zweiter Thread
    		x = 44;
    		s = "jops";
    	}
    };
    
    int main()
    {
    	ThreadTest t;
    	t.Start();
    }
    

    Ausgabe wie erwartet 44 / jops, jedoch glaube ich, dass da eigentlich ein volatile benötigt wird. Doch für std::string geht das nicht.
    Aber wenn ich eine critical section nur in Thread() rund um die Zuweisungen mache, reicht das ja dann auch nicht, oder? In Start() nach dem Wait müsste ich dann noch eine haben, und darin die member x und s lokalen Variablen zuweisen, damit die nochmal neu gelesen werden... umständlich!
    Oder muss man gar nichts machen?

    😕



  • loboo schrieb:

    Oder muss man gar nichts machen?

    Genau.



  • Kannst du das etwas genauer erklären?

    Habe u.A. hier geguckt:
    http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/
    http://coders-corner.net/2013/03/02/multithreading-in-c-teil-7-caching/

    Ist zwar C#, aber über C++ habe ich sowas auch schon gelesen.



  • loboo schrieb:

    Ich habe des öfteren gelesen, dass eine Variable "gecached" werden kann. Dies soll bei Multicore-Systemen Probleme bereiten können.

    Nicht nur bei denen, sondern bei allen halbwegs modernen Prozessoren.
    Die "Probleme" haben auch nicht unbedingt mit Caching zu tun. Ein Prozessor kann nicht deine Gedanken lesen, wenn er entscheiden muss, wann genau er was in welcher Reihenfolge erledigt. Er hat genau definierte Freiheiten die Operationen in einer anderen Reihenfolge auszuführen als man sie in C++ geschrieben hat. Wenn man diese Freiheiten einfach ignoriert, bekommt man "Probleme". Außerdem darf der Compiler noch die Reihenfolge ändern, wenn er das für sinnvoll erachtet.
    Das ist aber nicht schlimm, denn jeder Prozessor, der solche Späße im Hintergrund treibt, bietet Instruktionen an, um die Reihenfolge zu diktieren, wenn es nötig ist. Und der Compiler hält sich auch an bestimmte Regeln.
    Schau dir mal std::atomic und so an. Verstehe warum man das braucht.

    loboo schrieb:

    (Hier wird ja eigentlich kein Lock benötigt, da kontrolliert zuerst der Mainthread zuweist, dann ein zweiter Thread gestartet wird, der auch zuweist, während der Mainthread aber auf diesen wartet! Aber sieht der Mainthread nach dem Wait.. auch die aktualisierten Werte?)

    Ja, das garantieren die Threading-APIs.

    loboo schrieb:

    Ausgabe wie erwartet 44 / jops, jedoch glaube ich, dass da eigentlich ein volatile benötigt wird.

    Glaube ich nicht.

    loboo schrieb:

    und darin die member x und s lokalen Variablen zuweisen, damit die nochmal neu gelesen werden...

    Wo hast du diese Idee her?

    loboo schrieb:

    Ist zwar C#, aber über C++ habe ich sowas auch schon gelesen.

    Du liegt etwas über C# und wunderst dich dann warum das in der völlig anderen Programmiersprache C++ nicht genau so ist... Ok



  • loboo schrieb:

    Kannst du das etwas genauer erklären?

    Salopp gesagt musst du deswegen nichts machen, da sowohl das Erstellen eines neuen Threads wie auch das Joinen eines Threads (=das WaitForSingleObject) passende "Synchronisierungspunkte" sind.

    Wobei du, um die Garantie vom C++11 Standard zu bekommen, vermutlich auf die Standard-Threads (std::thread) umstellen müsstest.

    loboo schrieb:

    Habe u.A. hier geguckt:
    http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/
    http://coders-corner.net/2013/03/02/multithreading-in-c-teil-7-caching/

    Da wird auch was ganz anderes gemacht als in deinem Code. Dort ist auf jeden Fall irgend ein zusätzliches Konstrukt nötig, da es ja keine "impliziten" Synchronisierungspunkte wie das Erstellen bzw. das Joinen eines Threads gibt.
    Und in C# ist volatile dafür ausreichend.

    loboo schrieb:

    Ist zwar C#, aber über C++ habe ich sowas auch schon gelesen.

    Aufpassen! Das volatile von C# ist wesentlich "stärker" als das von C++. In C++ kannst du volatile kaum jemals brauchen, es sei denn es geht um die Programmierung von Hardware (Kernel, Treiber).
    Dort wo du in C# volatile verwenden kannst, brauchst du in C++ meist Atomics.
    Oder eben einfach Locks bzw. anderen Synchronisierungsmechanismen, die ohne volatile /Atomics auskommen.



  • Etwas verspätet..*g*

    Okay, danke.
    Dann ist es aber doch so, dass hier running eigentlich std::atomic sein muss?
    Es funktioniert aber trotzdem... nach einer Sekunde erscheint "DONE!".

    bool running = true;
    
    unsigned int __stdcall Thread(void* param)
    {
    	Sleep(1000);
    	running = false; // Warum sieht der Haupt-Thread diese Zuweisung? Ist das garantiert?
    	return 0;
    }
    
    int main()
    {
    	_beginthreadex(0, 0, Thread, 0, 0, 0);
    
    	while(running);
    
    	cout << "DONE!";
    	cin.get();
    }
    


  • Dann bau' das ganze mal im Release.



  • Ouch!

    Ok.. da muss ich echt vorsichtiger sein in Zukunft.
    So ähnliche Konstrukte hab ich nämlich schon öfters verwendet, allerdings bis jetzt ohne Probleme..



  • In diesem Zusammenhang habe ich auch noch eine Frage. Und zwar lässt sich folgendes Programm durch Strg+C nicht mehr beenden, wenn ich es mit clang++ und -O3 kompiliere (beim gcc geht’s):

    #include <csignal>
    
    static std::sig_atomic_t g_running = 1;
    
    static void signal_handler(int sig);
    
    int main()
    {
    	std::signal(SIGINT, &signal_handler);
    
    	while (g_running)
    	{
    	}
    }
    
    void signal_handler(int)
    {
    	g_running = 0;
    }
    

    Edit1: Unsinn entfernt.

    Wenn ich hier die Variable volatile mache funktioniert das Programm wie gewünscht unter beiden Compilern.
    Hat mich Tage gekostet einen ähnlichen Fehler zu finden, nur weil ein volatile fehlte 😞
    Was sagt der Standard hierzu?

    Edit2:
    Hab den Übeltäter im Assembler identifiziert:

    4006a0:	a8 01                	test   al,0x1
    4006a2:	b8 00 00 00 00       	mov    eax,0x0
    4006a7:	74 f7                	je     4006a0 <main+0x20>
    

    Kein Wunder, dass hier nix passiert.

    Interessant ist auch, dass das Verhalten hier variiert, wenn ich das selbe Programm in C11 unter verschiedenen Einstellungen kompliere:
    Folgende Tabelle gibt an wie oft ich Strg+C drücken muss um das Programm zu beenden

    | (kein Flag) | -O3 
    ------------------------------
    clang    | 1           | 2
    gcc      | 1           | 2
    musl-gcc | 1           | beendet gar nicht
    


  • der compiler darf optimieren wenn er im scope vom programmfluss sichergehen kann dass es keine seiteneffekte gibt und das ist ja in deinem fall gegeben. das macht der gcc eigentlich auch, vielleicht gibt es da noch mehr flags die du triggern musst. (ich hatte das auf einer console auch mal debuggen duerfen, von daher bin ich mir recht sicher).

    deswegen gibt es das 'volatile' keyword, um dem compiler mitzuteilen, dass das wissen ueber das verhalten dieser variable ausserhalb seines scopes ist.

    wundert mich dass er hlt macht, muesste es nicht einfach ein infinity loop werden? das hatte der gcc bei mir gemacht, quasi

    if(g_running)
            while(true);
    


  • rapso schrieb:

    wundert mich dass er hlt macht, muesste es nicht einfach ein infinity loop werden? das hatte der gcc bei mir gemacht, quasi

    if(g_running)
            while(true);
    

    Jupp, hatte die falsche Stelle im Code als den Übeltäter gehalten, sorry. Der Edit hat’s korrigiert 😉



  • Die Compiler haben recht.

    n3797 schrieb:

    1.9/6
    When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects which are neither
    — of type volatile std::sig_atomic_t nor
    — lock-free atomic objects (29.4)
    are unspecified during the execution of the signal handler, and the value of any object not in either of these two categories that is modified by the handler becomes undefined.



  • camper schrieb:

    Die Compiler haben recht.

    n3797 schrieb:

    1.9/6
    When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects which are neither
    — of type volatile std::sig_atomic_t nor
    — lock-free atomic objects (29.4)
    are unspecified during the execution of the signal handler, and the value of any object not in either of these two categories that is modified by the handler becomes undefined.

    g_running ist aber std::sig_atomic_t.



  • Nathan schrieb:

    camper schrieb:

    Die Compiler haben recht.

    n3797 schrieb:

    1.9/6
    When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects which are neither
    — of type volatile std::sig_atomic_t nor
    — lock-free atomic objects (29.4)
    are unspecified during the execution of the signal handler, and the value of any object not in either of these two categories that is modified by the handler becomes undefined.

    g_running ist aber std::sig_atomic_t.

    aber eben nicht volatile



  • Der Einfachkeit halber würde ich std::sig_atomic_t ganz aus dem Gedächtnis streichen und nur mehr std::atomic verwenden.
    Vor allem weil der std::sig_atomic_t und std::atomic so klingen als hätten sie was gemeinsam. Also wenn man std::atomic kennt und dann std::sig_atomic_t liest könnte man annehmen "passt eh alles".

    Tut es aber, ohne volatile , wie man hier ja schön sehen konnte, eben nicht 😉

    (Und auch mit volatile sollte man vorsichtig sein - gibt z.B. Umgebungen wo Signals immer in nem eigenen Thread ausgeführt werden, und wenn diese dann bei volatile keine C#-volatile-ähnliches Verhalten implementieren, dann könnte es Probleme geben.)



  • hustbaer schrieb:

    Der Einfachkeit halber würde ich std::sig_atomic_t ganz aus dem Gedächtnis streichen und nur mehr std::atomic verwenden.

    wenn man alles was probleme bereiten kann der einfachheit halber laesst, koennte es am ende sperrlich mit den skills aussehen.

    ich wuerde vorschlagen sich mit den dingen die man nicht kann auseinander zu setzen bis man sie kann. als programmierer sollte man bereit sein das ganze leben lang dazu zu lernen. my2cent.



  • @rapso
    Verstehe dich nicht.
    Wieso sollte man std::sig_atomic_t noch verwenden?
    Was kann man damit machen was man mit std::atomic nicht genau so gut oder besser machen kann? 😕
    (Vorausgesetzt natürlich man hat std::atomic zur Verfügung.)

    rapso schrieb:

    ich wuerde vorschlagen sich mit den dingen die man nicht kann auseinander zu setzen bis man sie kann. als programmierer sollte man bereit sein das ganze leben lang dazu zu lernen. my2cent.

    Und genau das mache ich hier.

    Ich weiss echt nicht was du willst.
    Und vor allem was der mMn. unangebrachte "dazulernen" Kommentar soll.



  • hustbaer schrieb:

    @rapso
    Verstehe dich nicht.
    Wieso sollte man std::sig_atomic_t noch verwenden?
    Was kann man damit machen was man mit std::atomic nicht genau so gut oder besser machen kann? 😕

    ich nahm an du kennst den unterschied wenn du den tipp gibst und sagst nur 'der einfachheit halber' .

    also, sowas einfaches wie z.b.

    while(g_running)
    {
    }
    g_running=1;
    

    sind schnelle operationen, du liest den cache, du schreibst den cache. wenn du hingegen std::atomic verwendest hast du dafuer immer atomic operationen (und bei std::atomic sind auch die operatoren so ueberladen), und dann gehst du ueber den memory controller, je nach system kann das 200mal langsammer sein.

    dabei bremst du nicht nur den loop aus, je nach protokol dass fuer die synchronisierung verwendet wird, kannst du andere threads verlangsammen.

    besonders wenn du contention hast, z.b. weil in einem job system mehrere threads auf ein flag pollen, kann das den bus/controller ziemlich auslasten.

    rapso schrieb:

    ich wuerde vorschlagen sich mit den dingen die man nicht kann auseinander zu setzen bis man sie kann. als programmierer sollte man bereit sein das ganze leben lang dazu zu lernen. my2cent.

    Und genau das mache ich hier.

    Und vor allem was der mMn. unangebrachte "dazulernen" Kommentar soll.

    ich finde dass du das hier nicht machst, jedenfalls nicht sonderlich gut. dein vorschlag "nimm immer nur std::atomic" und die begruendung ist "der einfachheit halber". Ich finde das ist echt kein gutes argument, stell dir vor er soll jemandem anderen erklaeren weshalb er das verwendet "ist halt einfacher, ich weiss nicht, ob es schlechter oder besser ist".

    und damit es nicht langweilig und trocken bleibt, hier ein (poor man's) test source:

    #include <atomic>
    #include <csignal>
    #include <Windows.h>
    
    int main(int argc,char* argv[])
    {
    	const size_t T0=GetTickCount();
    	for(std::atomic<int> a=0;a<~0u;a++)
    	{
    	}
    	const size_t T1=GetTickCount();
    	for(volatile std::sig_atomic_t a=0;a<~0u;a++)
    	{
    	}
    	const size_t T2=GetTickCount();
    	for(int a=0;a<~0u;a++)
    	{
    	}
    	const size_t T3=GetTickCount();
    	printf("Test:%d %d %d\n",T1-T0,T2-T1,T3-T2);
    }
    

    release build, VS2012, auf einem i7-4770k:

    Test:59140 7098 0
    

    wenn wir von 3.5GHz ausgehen und jeweils ein add,compare,conditional jump annehmen, sind das
    59140->16 cycle/instruction
    7098->1.9cycle/instruction
    0->wegoptimiert, wie zu erwarten war, sanity check, dass es release build ist 🙂

    Ich weiss echt nicht was du willst.

    sorry wenn das wie ein angriff oder denunzierend klang, das war wertneutral gemeint und nicht gegen dich persoenlich gerichtet. du bist einer der netteren hier, hab kein beduerfniss dich grundlos zum feind zu machen 😉



  • rapso schrieb:

    for(int a=0;a<~0u;a++)
    	{
    	}
    }
    

    Bei einem Vergleich zwischen int und unsigned wird nach unsigned konvertiert und dann verglichen, richtig?
    std::numeric_limits<int>::max() ist auf jedem System, das ich kenne, kleiner als ~0u .
    a läuft über, also hast du da undefiniertes Verhalten.
    Du misst also potentiell Mist.

    Außerdem hat deine Variante mit atomic zwei atomare Operationen ( < und ++ ), obwohl das mit nur einer möglich wäre ( fetch_add , glaube ich).



  • TyRoXx schrieb:

    ...

    ok, du hast mich da erwischt, ich gebe es zu, es ist poor man's code. verflixt.

    Außerdem hat deine Variante mit atomic zwei atomare Operationen ( < und ++ ),

    jup, das schrieb ich ja weiter oben, jegliche operation ist atomar, selbst wenn du es nicht brauchst.

    obwohl das mit nur einer möglich wäre ( fetch_add , glaube ich).

    ist es nicht ein wenig sinnfrei ein beispiel zu optimieren? der naechste kommt und sagt, dass man die variable auch in eine nicht atomic stecken koennte, und am ende wieder zuweist. der naechste sagt dann dass man alles auskommentieren koennte weil das beispiel nichts nuetzliches macht. und ja, ihr habt alle recht, aber das ist doch am punkt vorbei, zu demonstrieren, dass atomics sogar im einfachsten fall kosten, und zwar nicht gerade wenig. ich finde praktische beispiele nett, damit wir wissen worueber wir reden. ich haette auch meine "200mal langsammer" stehen lassen koennen ohne jegliches beispiel. dann glaubst du mir entweder oder denkst ich bin ein idiot, aber auf jeden fall hast du keine erfahrung gewonnen dein wissen zu untermauern.


Anmelden zum Antworten