C# Mehr Threads werden immer langsamer?

TriggerThumb87 schrieb:
Diese Aussage stieß bei mir auf Misstrauen, ich habe daher versucht, mich schlau zu machen. Das Ergebnis scheint tatsächlich "Nein" zu sein.
Und wir beide sind jetzt schlauer ohne dass es Beleidigungen hagelt :) So muss das sein!

Dann forme ich meine Aussage um: Ist eine Operation in mehrere Instruktionen aufgeteilt ist sie garantiert nicht atomar, besteht sie nur aus einer Instruktion kann sie atomar sein, muss es aber nicht. Als Faustregel reicht das allemal :)

Sobald man aber in diese Tiefen absteigt sollte man sich eher Gedanken über "kritische Sektionen", Locks und Barriers machen.
 
  • Gefällt mir
Reaktionen: TriggerThumb87
Ich bin mir nach der ganzen Lektüre nicht sicher, ob ich jetzt schlauer bin oder nicht.
Ich habe bis jetzt nur diese eine Quelle gefunden, die gezielter auf gewisse Sachen eingeht. Aber da sind so viele Begriffe drin, die irgendwie rumgewürfelt werden, auch außerhalb dieser Quelle, dass ich eher weiß, dass ich nichts weiß.
Es geht etwas am Thema vorbei, aber wenn da jemand Quellen empfehlen kann, wäre ich sehr dankbar.
 
Ich hätte jetzt nochmal zwei Fragen zu dem Thema:
1. Ich habe mal einige Versuche gemacht und festgestellt, dass ich auf globale Attribute lesend zugreifen kann, ohne nennenswerte Performanceeinbrüche zu verzeichnen. Sobald ich diese aber modifiziere bricht die Performance massivst ein. Das Verhalten ist dann normal, so wie ich das jetzt verstanden habe oder?

2. Ich habe das Beispiel nochmal angepasst und geschaut, was passiert, wenn ich eine Methode zum ,,Vergleichen" verwende und was passiert, wenn ich den Methodeninhalt direkt innerhalb eines Threads ausführe, also ohne einen Methodenaufruf.

Ohne Methodenaufruf komme ich auf 700.000.000 Aktionen / Sekunde und mit Methodenaufruf immer noch auf 200.000.000 / Sekunde d.h. sollten Methodenaufrufe bei Threads auch vermieden werden und alles sollte innerhalb des Threads berechnet werden?

Ich hätte eigentlich vermutet, dass die Performance annähernd identisch bleibt, da ja keine globalen Variablen verwendet werden, hier hat ja jeder Thread seine eigene Variable ,,einText" innerhalb der Methode :

Performance.jpg


Danke für die Hilfe und liebe Grüße Ghost Rider 😃
 
Wieso machst du "Equals ? True : false"? Equals gibt bereits einen boolean zurück. Den kannst du direkt zuweisen.

Methodenaufrufe kosten immer Performance, weil die Ausführung dafür springen muss. Sprünge benötigen extra Zyklen, extra Register, etc.

Wobei ich erwartet hätte, dass ein guter Compiler so eine kurze Methode direkt inline optimieren kann...

Btw, dein ZaehlerGlobal Zugriff ist immernoch nicht threadsafe.
 
hat zwar eigentlich keiner nach gefragt, aber obendrein eine analyse der performancezahlen: der 12-kerner unterstuetzt SMT, d.h. wir spielen mit 24 logischen kernen. meiner erfahrung nach rentiert es sich oft, auch wirklich die anzahl der logischen kerne als threadcount zu nutzen, auch wenn sich pro thread-doppelung die performance nicht mal annaehernd um die 100% erhoeht, die man naiv erwarten koennte. so ergibt sich auch der load von ~50% auf dem zweiten screenshot aus #14 - windows misst die prozessorauslastung als summe aller (hardware-)threadauslastungen (das ist nervig, weil es eigentlich nicht stimmt als wahre kapazitaetsangabe der cpu - aber nebensaechlich hier).

offensichtlich produzierst du auch nicht wie ein irrer millionen von threads - daher ist der overhead der threaderzeugung nicht der grund fuer den geringeren wert des counters.

das wirft fuer mich aber 'ne weitere frage auf: wieso ist single core gegenueber thread-unsafe-parallelisiertem multithreading um den faktor 32(!) groesser? naiv haette ich erwartet, dass das ergebnis des multicore-versuchs mit pech ein wenig unter dem des singlecore-versuchs liegt (amd turbo boost hoeher fuer 1 kern auslastung). im worst case der ausfuehrung wuerde ein multithreading-kern mit nicht-maximalem takt etwas langsamer als der singlethread-kern doch eine zahl in der gleichen groessenordnung produzieren - also aehnlich schnell arbeiten - und dann per write after write hazard alle arbeit der anderen threads ueberschreiben. offensichtlich ist was aehnliches hier der fall - das erklaert aber nicht, wieso der einzelne thread so viel besser performt.

gibt es da was ueber die CLR zu lernen, werte spezialisten?

E: ist kein CLR ding, konnte das verhalten in java reproduzieren.

E2: wohl ein cache coherency-effekt (https://en.wikipedia.org/wiki/MESIF_protocol)
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: TriggerThumb87
Ich gehe davon aus, dass Werte einfach überschrieben als komplett addiert werden. Mir reicht die Erklärung. Übersehe ich hier etwas?

EDIT: Was gefuttert, dann ist mir klar geworden was du meintest. Danke für's Präsentieren der Lösung.
 
Zuletzt bearbeitet:
Nimm für Zeitmessungen lieber die Stopwatch Klasse. DateTimeNow ist sehr langsam. DateTime.UtcNow wäre auch noch etwas schneller als .Now weil hier nicht auf unsere Zeitzone konvertiert werden muss.

Noch eine Frage. Wenn Du Deine Performance Tests machst. Baust Du die Anwendung auch im Release Modus? Der Debug Build wird nicht optimiert.

Hab mal einen ähnlichen Code geschrieben und ihn einmal als Debug und Release laufen lassen:
Debug:
47 (Kein Funktionsaufruf)
93 (Funktionsaufruf)
92 (Funktionsaufruf mit Attribut für aggressives Inlining)

Release:
38
45
32 (keine Ahnung warum das schneller als das erste ist)

Im Debug Modus findet anscheinend kein Inlining statt.

Code:
        static void Main(string[] args)
        {
            var watch = new System.Diagnostics.Stopwatch();

            string zufall = "12";
            bool gleich;

            watch.Start();
            for (int i=0;i<10000000;++i)
            {
                gleich = zufall.Equals("500");
            }
            watch.Stop();
            Console.WriteLine(watch.ElapsedMilliseconds);

            watch.Restart();
            for (int i = 0; i < 10000000; ++i)
            {
               gleich = F1(zufall);
            }
            watch.Stop();
            Console.WriteLine(watch.ElapsedMilliseconds);

            watch.Restart();
            for (int i = 0; i < 10000000; ++i)
            {
                gleich = F2(zufall);
            }
            watch.Stop();
            Console.WriteLine(watch.ElapsedMilliseconds);


            Console.ReadKey();
        }

        static bool F1(string wert)
        {
            return wert.Equals("500");
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static bool F2(string wert)
        {
            return wert.Equals("500");
        }


    }

Bei einem Funktionsaufruf wird das Statusregister von der CPU gesichert und am Ende wiederhergestellt. Außerdem müssen die Parameter in den Stack-Speicher kopiert werden. Und dann gibts noch einen Sprungbefehl. Funktionsaufrufe sollten deshalb generell langsamer sein.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Ghost_Rider_R
Ghost_Rider_R schrieb:
Ich hätte jetzt nochmal zwei Fragen zu dem Thema:
1. Ich habe mal einige Versuche gemacht und festgestellt, dass ich auf globale Attribute lesend zugreifen kann, ohne nennenswerte Performanceeinbrüche zu verzeichnen. Sobald ich diese aber modifiziere bricht die Performance massivst ein. Das Verhalten ist dann normal, so wie ich das jetzt verstanden habe oder?

2. Ich habe das Beispiel nochmal angepasst und geschaut, was passiert, wenn ich eine Methode zum ,,Vergleichen" verwende und was passiert, wenn ich den Methodeninhalt direkt innerhalb eines Threads ausführe, also ohne einen Methodenaufruf.
1. Das ist alles nicht so einfach. Vor allem, weil du dir ein Beispiel herausgesucht hast, wo es scheinbar oder aber anscheinden zufällig funktioniert.
Sobald auf geteilte (also "shared") Ressourcen (egal von wem, da spielt der Main-Thread auch mit!) sowohl lesend, als auch schreibend zugegriffen wird müssen diese unbedingt abgesichert werden.
Egal, ob beim Lesen oder Schreiben und auch egal, wie oft das eine oder das andere tatsächlich passiert.
(Es gibt Ausnahmen, die setzen aber ein umfassendes Verständnis des verwendeten Datentyps, der damit verbundenen Operationen und der Prozessorarchitektur, auf der alles am Ende ausgeführt wird, voraus.
Ist eines davon nicht gegeben, ist ohne entsprechende Absicherung die Wahrscheinlichkeit von Fehlern sehr groß!)
Stichwörter dafür wären z.B. Critical Section oder Mutex.

Beim Schreiben von geteileten Ressourcen hast du ja schon Erfahrungen gemacht, was da passieren kann.
Was beim Lesen passieren kann, wäre z.B. folgendes:
Nehmen wir mal an, es braucht zwei Instruktionen, um deiner Variable eines bestimmten Datentypen einen neuen Wert zuzuweisen.
Die erste Instruktion überschreibt dabei die erste hälfte dieser Variable im Speicher und die zweite Instruktion die zweite Hälfte.
Gehen wir davon aus, dass diese zwei Schreibvorgänge von keiner Seite her irgendwie abgesichert werden, dann steht es im Raum des Möglichen, dass das Betriebsystem direkt nach dem Schreiben der ersten Hälfte einen Kontext-Switch vom schreibenden Thread hin zu einen lesenden Thread vollziehen könnte.
Das würde dazu führen, dass der lesende Thread für deine Variable nun einen Wert bezieht, der zum einen Teil aus dem neuen und zum anderen Teil als aus dem alten Wert besteht.
Das ist jetzt bei Ordinalen Datentypen, wie Integern, Booleans, Chars o.ä., eher unwahrscheinlich, wird aber bei Behandlung von großen Datentypen (je nach Implementation z.B. sehr große (128bit+) Integer, Floats, etc.), Records/Structs oder gleich Objekten immer realistischer.

Eine Möglichkeit Absicherungen zu umgehen gibt es nur in der Form, geteilte Ressourcen zu vermeiden und stattdessen alle notwendigen Daten für jeden einzelnen Thread zu kopieren.
Dann kann jeder Thread machen, was er machen soll und am Ende der Thread-Lebenszeit (bzw. nach Task-Abschluss) können die Ergebnise gefahrenlos von einem einzelnen Thread (z.B. dem Main-Thread) gelesen und mit anderen Ergebnissen vereint werden.
Sowas geht aber natürlich nicht immer.
Dieser Ansatz ist aber vorzuziehen, da das noch einen anderen Vorteil hat:
Multithreading heißt nicht gleich auch automatisch "Multi-Core-Threading".
Ob ein Thread auf einem eigenen bzw. unabhängigen logischen oder physischen CPU-Kern ausgeführt wird, entscheidet das Betriebsystem und diese Entscheidung trifft das OS unter anderm anhand von Zugriffen auf Ressourcen.
Erkennt das OS dabei, dass der Thread in kurzen Abständen immer auch auf dieselben Ressourcen zugreift, wie z.B. dein Main-Thread (oder anders herum), kann das OS zur (wahrscheinlich richtigen) Einschätzung kommen, dass es unterm Strich Rechenintensiver ist, die Daten immer wieder in den kurzen Abständen zwischen den Registern/Caches zweier Kerne hin- und herzusynchonisieren, als beides auf demselben Kern laufen zu lassen.

2. Da muss noch irgend etwas anderes hineinspielen.
C# nutzt i.d.R. einen JIT-Compiler, der aus Code, hinterlegt in CIL (Common Intermediate Language), erst dann Maschinencode produziert, wenn er tatsächlich ausgeführt wird.
Aber ich kann mir kaum vorstellen, dass diese sehr kleine Methode einmalig so viel Zeit frisst, auch nicht in Verbindung mit dem Kopieren des Strings, der als Parameter übergeben wird, dem Aufbau des Aufrufstacks den Sprungmarkern für den Methodenaufruf und dem kopieren des Rückgabewertes.
 
Zuletzt bearbeitet:
Zu den Fragen:
Das neue Skript habe ich auf einem anderen Rechner laufen lassen (4 Kerner ohne HT) daher andere Ergebnisse & andere Parameter.

Das mit dem Equals ist doppelt gemoppelt, da hast du natürlich recht 😅

Ja, die Vermutung mit dem Overhead der Threads war nicht die Ursache, sondern wie von euch ermittelt der gemeinsame Zugriff auf die gloabale Variable. Das ist meine wichtigste Erkenntnis darauf!

Meistens teste ich im Debug Modus, rolle dann aber ausschließlich im Release Modus aus, allerdings habe ich noch nicht ganz.

Nächste wichtige Erkenntnis: Es macht einen rießen Unterschied, ob Debug oder Release ausgewählt wurde. Im Release-Modus ist die Methoden-Variante genau so schnell wie die in Thread Variante, also scheinen Methodenaufgrufe an sich erstmal kein Problem zu sein!

@michi.o

Vielen Dank für diesen wichtigen Tipp!
 
AW4 schrieb:
Sobald auf geteilte (also "shared") Ressourcen (egal von wem, da spielt der Main-Thread auch mit!) sowohl lesend, als auch schreibend zugegriffen wird müssen diese unbedingt abgesichert werden.
Das ist so pauschal nicht ganz richtig. Lesezugriffe auf immutable also unveränderliche Daten müssen in der Regel nicht abgesichert werden.

@Ghost_Rider_R das Konzept der Immutable Daten kann ich dir im Kontext von Multithreading nur ans Herz legen. Es löst von vornherein jede Menge Probleme.
 
Ghost_Rider_R schrieb:
Ja, die Vermutung mit dem Overhead der Threads war nicht die Ursache, sondern wie von euch ermittelt der gemeinsame Zugriff auf die gloabale Variable.
Bei mehreren Threads ist der Zugriff (egal ob lesend oder schreibend) immer ein Thema. Nichts desto trotzt erzeugt ein Thread einen Overhead - auch wenn dieser im Vergleich zum Nutzen im Regelfall nicht relevant ist. Bei sehr vielen Threads kann das wie schon mehrfach in diesem Thema erwähnt jedoch ein Problem werden.
Ghost_Rider_R schrieb:
Nächste wichtige Erkenntnis: Es macht einen rießen Unterschied, ob Debug oder Release ausgewählt wurde.
Korrekte, die ganzen Compiler-Optimierungen werden im Debug-Modus nicht angewendet - würde sich sonst auch nicht sinnvoll debuggen lassen. (Noch dazu ist das "debuggen" von Code mit mehreren Threads eh schon eine Kuns für sich.)
Ghost_Rider_R schrieb:
Release-Modus ist die Methoden-Variante genau so schnell wie die in Thread Variante, also scheinen Methodenaufgrufe an sich erstmal kein Problem zu sein!
Jeder Methodenaufruf kostet Leistung da wie schon erwähnt sowohl beim Einsprung in als auch Rücksprung aus der Methode zusätzliche Befehle von der Hardware (durch das OS gesteuert) stattfinden.

Mein Tipp an dich: Das Thema ist relativ komplex und es würde dir sicherlich helfen die entsprechenden Kapitel aus einem gutem Buch dazu zu lesen. Ich kann dir leider nur keines vorschlagen da ich das schon vor längerer Zeit erlernt hatte und generell nicht der beste "Erklärbar" bin. Evtl. kann dir hier jemand etwas gutes für Einsteiger im Bereich Multithreading empfehlen - es muss nicht zwangsweise auf C# zugeschnitten sein, wäre natürlich trotzdem ein Vorteil für dich ;).
 
Macht es doch nicht so kompliziert.
Unter .Net ist man mit Parallel-Klasse sehr verwöhnt. Damit muss das Rad nicht dauernd neu erfunden werden.
 
  • Gefällt mir
Reaktionen: kuddlmuddl und TriggerThumb87
Ephesus schrieb:
Unter .Net ist man mit Parallel-Klasse sehr verwöhnt.
Stimmt, aber auch da muss der TE aufpassen, was gemeinsam verwendet wird. Also Interlocked angucken oder mittels lock {} Schreibzugriffe beschränken.
Man muss sich schon etwas Gedanken machen:
  • bin ich I/O-limitiert (z.B. zahlreiche gleichzeitige Webanfragen) oder CPU-limitiert? Bei ersterem eher nach Tasks und async schauen, bei letzterem eher Threads und die Parallel-Klasse.
  • worauf wird innerhalb der parallelisierten Aufgaben zugegriffen? Unterstützen ggf. verwendete Objekte das? Z.B. kann ich nicht mehrere Queries gleichzeitig mit einer SqlConnection abfeuern, das braucht dann verschiedene Connections.
  • wie werden die Ergebnisse der Aufgabe - sofern es welche gibt - wieder zusammengeführt? Wird z.B. alles in eine gemeinsame Liste geschrieben? Dann darf der Schreibzugriff auf diese nicht gleichzeitig passieren, siehe wie gesagt lock { }
  • ach ja, und natürlich die Frage: Ist meine Aufgabe überhaupt vernünftig parallelisierbar?
 
  • Gefällt mir
Reaktionen: Ephesus
Ephesus schrieb:
Damit muss das Rad nicht dauernd neu erfunden werden.

Sagte er und benutzte .Net. :p
Ich war Fan des Ganzen bis 3.5 aufwärts oder so.
Jetzt habe ich 1 Problem, und 20 Möglichkeiten es zu lösen. Das macht für mich 8 Jahre nach dem Abi mit Mathe-LK 21 Probleme.
 
Zurück
Oben