Autor Beitrag
Luccas
Hält's aus hier
Beiträge: 11

Win 8.1, Win 10
C# ( VS 2015 )
BeitragVerfasst: Mi 24.08.16 17:05 
Hallo zusammen

Nachdem mein letztes Problem mehr um die Anzahl der Durchläufe ging, geht es jetzt rein um die Laufzeiten. Parallel.For ist ein Konstrukt um Schleifen Tempo zu machen. Jedoch stellt sich bei mir der Effizienzgewinn nicht ein - im Gegenteil. Die synchrone Variante ist sogar schneller. Wieder habe ich mir ein einfaches Testprogramm geschrieben. Zwei Methoden in Konkurrenz: synchron gegen asynchron:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
        static void AktionSynchron(int limit)
        {
            double Dummy = 1000.0;
            for(int i= 1; i <= limit; i++)
            {
                Dummy = Math.Sqrt(Dummy) * Math.Sqrt(Dummy);
                Dummy = Math.Sqrt(Dummy) * Math.Sqrt(Dummy);
            };           
        }

        static void AktionAsynchron(int limit)
        {
            double Dummy = 1000.0;
            Parallel.For(1, limit+1, AnzKerne, obj =>
            {
                Dummy = Math.Sqrt(Dummy) * Math.Sqrt(Dummy);
                Dummy = Math.Sqrt(Dummy) * Math.Sqrt(Dummy);
            });
        }


beide durchlaufen den Test mit folgenden Werten für 'limit' { 10, 1000, 10000, 100000, 1000000 }

Das Ergebnis (s. Dateianhang ) Ich habe jeweils die Ticks gezählt (MSek bringt gleiches Rangfolge-Ergebnis).

Der Parallel.For-Durchlauf benötigt mehr Ticks als die normale For-Schleife, auch unabhängig von der Anzahl der Kerne (1-8 getestet).
Der Start ohne Debug mit Exe-Datei zeigt absolut gleiches Laufzeitverhalten.

Was mache ich jetzt falsch?
Gruss
Luccas
Einloggen, um Attachments anzusehen!
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Mi 24.08.16 17:40 
Beachte: Die Dummy-Variable wird in dem Beispiel von mehreren Threads gleichzeitig verwendet.
Das wird vermutlich nicht so extrem auffallen, aber das kann faszinierende Fehler erzeugen, die in komplexeren Anwendungen sch***e schwer zu finden sind :D
Ich hab die bei meinem Test also mit in die for-Schleibe bzw. in den For-Delegaten genommen.
Ich messe mit 100000 Durchläufen.

Wenn ich im Debug-Mode teste, ist bei mir die Performance bei der asynchronen massiv langsamer als die synchrone Variante. Hab da teilweise fast eine halbe halben Sekunde gemessen, während die synchrone Variante unter 10 ms blieb.
Ohne Debugger bewegt sich die asynchrone Variante um die 10ms, während die synchrone Variante um die 3ms liegt.

Wenn ich jetzt pro Durchlauf noch ein Thread.Sleep(10) einfüge, dann sieht das auf einmal ganz anders aus.
Ich hab nur noch 1000 Durchläufe eingestellt, da ich keine Lust hatte, minutenlang zu warten :D
Debugger oder nicht ist bei mir diesmal egal, die synchrone Variante bewegt sich um die 10 Sekunden, während die asynchrone Variante zwischen 2 bis 3 Sekunden liegt.



Ich hab das auch im anderen Thread gesagt:
Das lohnt sich nur, wenn die einzelnen Durchläufe lange dauern.
Diese kurzen mathematischen Berechnungen sind nicht lange, davon kriegst Du massig in einer Millisekunde durch.
Das siehst Du ja auch schon, wenn Du synchron 100000 einstellst, kein Sleep, dann dauert das bei mir unter 10ms. Also 400000 Aufrufe von Math.Sqrt, 200000 Multiplikationen, 200000 Variablen-Zuweisungen und 100000 Variablen-Deklarationen in unter 10 Millisekunden.
Da dauert das Starten und verwalten der Threads deutlich länger als die Aufgaben selber.
Muss ich noch weiter erklären, warum das zu schnell ist? :D

Das Thread.Sleep soll die lang andauernde Aufgabe simulieren.
Und jetzt stell dir vor, Du musst irgendwelche Musik-Titel im RAM convertieren.
Davon wirst Du wohl keine 10 Stück haben, aber ein Titel dauert dann auf einmal mehr als nur ein paar Millisekunden.
Da lohnt sich das dann wirklich
Wichtig ist dabei aber, dass die Bilder bereits geladen sind, wenn die von einer HDD geladen werden, dann würde das ganze noch ausbremsen, weil die HDD ständig springen muss.

Für diesen Beitrag haben gedankt: Luccas
Luccas Threadstarter
Hält's aus hier
Beiträge: 11

Win 8.1, Win 10
C# ( VS 2015 )
BeitragVerfasst: Do 25.08.16 17:16 
Auf die Idee bin ich auch schon gekommen. In jeder Prozedur ein Thread.Sleep(2000) und 10 Durchläufe. Leider wieder gleiches Bild: praktisch Gleichstand zw. synchron und asynchron.
Dann habe ich - wie Du - einen Sleeper von 10 und 1000 Durchläufe. Im Gegensatz zu Dir habe ich aber wieder absoluten Gleichstand.

Trotzdem Danke, ich lass die Sache jetzt erst mal etwas ruhen und probiere das Ganze in 3-4 Wochen noch mal. Erfahrungsgemäß kann ein Pause ganz sinnvoll sein. Nur schade, dass diese Techniken nicht so funzen wie es in den Büchern steht. Beim Thema async / await (auf Konsolenebene ohne GUI) ist es noch schlimmer. Da funktioniert kein einziges Beispiel so, wie es in Büchern beschrieben ist.

Aber egal, guck mir jetzt WPF :shock: an, gaaaanz anderes Thema. Das klappt bestimmt :(

Gruss
Luccas
Palladin007
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 1282
Erhaltene Danke: 182

Windows 11 x64 Pro
C# (Visual Studio Preview)
BeitragVerfasst: Do 25.08.16 17:24 
Ich kenne die Bücher nicht, kann dazu also nichts sagen :D

Aber poste doch mal das Beispiel, wo es auch mit einem Thread.Sleep nicht funktioniert?
Ich hab dein Beispiel verwendet und Thread.Sleep eingefügt, damit habe ich dann einen deutlichen Vorteil

Auch bei Async kann man nur sagen:
Es ist nicht so einfach wie es scheint ;)
Dahinter steckt ein ziemlich komplexes System, das man erst einmal durchschauen muss.
Daher: Beispiel? :D
Luccas Threadstarter
Hält's aus hier
Beiträge: 11

Win 8.1, Win 10
C# ( VS 2015 )
BeitragVerfasst: Sa 27.08.16 09:57 
Hallo Palladin007

Alles in Butter :D

Hab einfach einen saublöden Fehler gemacht (die Hitze ?) :oops: . Hatte bei dieser Variante vergessen, dass ich die Anzahl der Kerne einen Tag zuvor auf 1 gesetzt hatte.

Das konnte ja nicht funzen.

Jetzt die Änderung mit AnzKerne = 8!

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
        static void AktionSynchron(int limit)
        {
            for(int i= 1; i <= limit; i++)
            {
                Thread.Sleep(5000);            // simuliert eine lange anwendung
            };           
        }

        static void AktionAsynchron(int limit)
        {
            Parallel.For(1, limit+1, AnzKerne, obj =>
            {
                Thread.Sleep(5000);
            });
        }


wenn ich beide mit einem limit von 10 starte, ergeben sich folgende Ticks:

synchron: Ticks: 715.977.360 Msek: 50.002
asynchron: Ticks: 143.839.201 Msek: 10.045

Bei 8 beteiligten Kernen immerhin Faktor 5 !!! Ein wenig Overhead darf man dem
System ja zugestehen. :wink:

Fazit 1: Parallel.For sehr sinnvoll bei zeitaufwändigen Routinen.

Dass sich Parallel.For aber auch bei kurzlaufenden Aufträgen und vielen Wiederholungen lohnt zeigt das
folgende Beispiel, dass ich einem Buch entnommen habe (Kühnel):

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
 static void SynchronTest(int loops)
        {
            double[] arr = new double[loops];
            for (int i = 0; i < loops; i++)
                arr[i] = Math.Pow(i, 0.333) * Math.Sqrt(Math.Sin(i));
        }

        static void ParallelTest(int loops)
        {
            double[] arr = new double[loops];
            Parallel.For(0, loops, i =>
            {
                arr[i] = Math.Pow(i, 0.333) * Math.Sqrt(Math.Sin(i));
            });
        }


Laufzeiten s. Anhang.

Fazit 2: Parallel.For lohnt bei geringen Durchläufen nicht, wird aber umso effizienter, je mehr Durchläufe gestartet werden.

(Warum mein eigenes, ganz ähnlich aufgebautes Ausgangsbeispiel, bei 8 Kernen ganz andere Laufzeiten erzeugt, ist mir aber immer noch unklar).

Gruss, bis zum nächsten Mal und noch schönes Rest-Sonnen-WE.
Luccas
Einloggen, um Attachments anzusehen!
Ralf Jansen
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 4700
Erhaltene Danke: 991


VS2010 Pro, VS2012 Pro, VS2013 Pro, VS2015 Pro, Delphi 7 Pro
BeitragVerfasst: Sa 27.08.16 16:19 
Zitat:
Fazit 2: Parallel.For lohnt bei geringen Durchläufen nicht, wird aber umso effizienter, je mehr Durchläufe gestartet werden.


Das ist zu simpel. Du musst beachten ob sich die Threads Resourcen teilen müssen (wie unabhängig/abhängig die Threads voneinander sind) und ob der Threading Overhead überhaupt noch eine Gewinn an Performance zulässt. Je nach Resource die du in den Durchläufen verwendest ergibt sich da ein anderes Bild. In deinem Ausgangsbeispiel machst du halt sehr oft näherungsweise nichts (Sqrt geht in Hardware und ist gegenüber anderen Rechenarten sehr schnell) und errechnest quasi den Overhead durch Threading. Ein intelligenter Compiler würde deinen Code vielleicht sogar komplett verwerfen und einfach nicht ausführen weil er ~merkt~ das er eine No Operation darstellt.

Zitat:
(Warum mein eigenes, ganz ähnlich aufgebautes Ausgangsbeispiel, bei 8 Kernen ganz andere Laufzeiten erzeugt, ist mir aber immer noch unklar).


Weil Sqrt gegenüber Sin und Pow der um ein vielfacheres schnellerer Algorithmus ist. Du hast einfach das Verhältnis zwischen eigentlichem Code und Threading Overhead günstiger gestaltet womit du scheinbar den Break Even überschritten hast wo Parallelisierung anfängt sich so lohnen.