Laden...

c# Consolenprogramm nutzt nur 1 CPU-Core

Erstellt von Arti vor einem Jahr Letzter Beitrag vor einem Jahr 961 Views
A
Arti Themenstarter:in
9 Beiträge seit 2022
vor einem Jahr
c# Consolenprogramm nutzt nur 1 CPU-Core

Hallo,

ich habe ein c# Consolenprogramm (.Net6) geschrieben mit dem ich 2 Listen vergleiche:

Ich bin kein Profi oder ähnliches, ich habe mir das alles per Try-Error beigebracht... also bitte nicht so streng sein...


// Step 1: Create a union list from the first and the second (only single entries. Duplicates will be removed automatically)
var unionList = md5lines1.Union(md5lines2, new DoCompare());
// Step 2: Create a list with the entries, which are in both lists 
var intersectList = md5lines1.Intersect(md5lines2, new DoCompare());
// Step 3: Get the entries which only in the union list and not in the intersect list
var exceptList = unionList.Except(intersectList);
// Step 4: Create a list with entries only in file1
var onlyFile1 = exceptList.Except(md5lines2);
// Step 4: Create a list with entries only in file2
var onlyFile2 = exceptList.Except(md5lines1);

Die Listen sind zum Teil sehr groß und haben mehrere Millionen Einträge.

Beim Ausführen fällt mir nun auf, das nur einer meiner 16 CPU-Cores benutzt wird.
Im Taskmanager sehe ich, das alle Cores auf 1-5% rumdümpeln, nur einer läuft auf 100%.

Um das ganze zu beschleunigen möchte ich, das die Software alle Cores nutzt, damit das alles schneller geht...

Ich war der Meinung das das automatisch von Windows gemanaged wird, scheint aber nicht so zu sein...

Kann mir jemand sagen wo/wie ich das ganze "Multicore-Fähig" machen kann, bzw. wie aufwändig das ist?!?

Danke euch!

T
2.219 Beiträge seit 2008
vor einem Jahr

Du solltest dich mit dem Thema Multithreading bzw. generell mit Threading beschäftigen.
Windows managed nur die Threads, kümmert sich aber nicht darum deine Software magisch auf alle Kerne zu verteilen.
Dies muss der Entwickler selbst mit Threads bzw. mit Tasks lösen.

Auf den ersten Blick wüsste ich nicht, wie man den Code per Threading/Tasks optimieren sollte.
Gerade der Vergleich von Listen muss zwangsweise auch durch einen Thread erledigt werden.

Nachtrag:
Anstelle von Listen wären ggf. HashSet zum ermitteln von eindeutigen Einträgen besser.
Ggf. lässt sich dein Problem also durch andere Datenstrukturen optimaler lösen als durch die Listen.

Nachtrag 2:
Try-Error ist übrigens kein guter Ansatz um programmieren zu lernen.
Lies dich mit guten Büchern ein oder mit Lernvideos.
Try-Error kann dir nicht die Grundlagen sauber vermitteln und ist auch keine optimale Problemlösung.
Besser ist es, wenn man die jeweilige Programmiersprache sauber lernt und dem Umgang auch durch einen Blick in die Doku lernt.

T-virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.807 Beiträge seit 2008
vor einem Jahr

Kann mir jemand sagen wo/wie ich das ganze "Multicore-Fähig" machen kann, bzw. wie aufwändig das ist?!?

Parallel Programming ist ein Thema, bei dem wirklich Try and Error der absolut falsche Weg ist, weil Du die Basis verstehen musst, denn Du kannst zwar Compiler-valide alles ohne Build-Errors programmieren; wenn Du aber Konzepte falsch anwendest, dann macht Dein Programm sehr kuriose Dinge - und Du verstehst nicht warum. Auch kann die falsche Anwendung dazu führen, dass Deine Anwendung instabiler und sogar insgesamt langsamer wird.
Es ist daher wirklich wichtig zu verstehen, was Prozesse, Threads und bezogen auf .NET Tasks sind und wie man sie anwendet. Damit mein ich nicht die nächsten 6 Monate nur das lernen zu müssen, sondern wirklich die Grundlagen dahinter zu verstehen.

Ein Einstieg kann das hier sein: Task-based asynchronous programming - .NET
Hinweis: asynchrone und parallele Programmierung sind nah bei einander, aber nicht das gleiche. Dennoch oft in Dokumentationen zusammen behandelt.

--
Ein paralleles Mergen von zwei Listen wie hier ist auch nicht ohne Code-Umbau möglich, da Listen und deren Write-Operationen nicht Thread-Safe sind.
Ab .NET 7 gibts jedoch ParallelEnumerable.Intersect Method (System.Linq)

A
Arti Themenstarter:in
9 Beiträge seit 2022
vor einem Jahr

Ich habe mein Performance-Problem eingrenzen können:


foreach (md5list Hashline in intersectList)
            {
                //..
            }

Der Durchlauf dauert auf meinem i9-11900K nur eine Minute (hier entwickel ich).
Auf einem Xenon W-2265 dauert das 20 Minuten...

Ich kann mir vorstellen das der Xenon X-2265 langsamer ist, aber so extrem, ich verstehe es nicht.

Jemand eine Idee?

T
2.219 Beiträge seit 2008
vor einem Jahr

Hast du mal die Taktraten der CPUs verglichen?
Dein i9 wird mehr Dampf haben als ein Xenon CPU weil das eine ein Dekstop mitmeistens hohen Takraten ist und letzteres eine Server CPU bei der weniger Task dafür aber mehr Kerne vorgesehen sind.
Bei Server CPU geht es mehr darum möglichst viele Anfragen gleichzeitig verarbeiten zu können.

Ansonsten wäre noch interessant was du eigentlich genau erreichen willst.
Eine Laufzeit von rund 1 Min. dürfte noch um einige Größen optimiert werden können.
Dazu müsste aber auch klar sein, was dein Ziel ist.

Wie sieht deine md5list den genau aus?
Wenn du nur einen String mit dem Hash hast, kannst du eigentlich mit HashSet<string> alle Dubletten eliminieren ohne großen Performance Aufwand.
Oder hast du hier noch andere Anforderungen?

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.807 Beiträge seit 2008
vor einem Jahr

Jemand eine Idee?

Mess mal bitte richtig - nicht Zeit von "irgendwas schätzen". Sonst reden wir hier sehr wahrscheinlich über falsche Eindrücke.

Für verlässliche Performance-Messung macht man eine CPU-Messung mit einem Performance Analyse Werkzeug, zB eingebaut in Visual Studio.
Measure CPU usage in your apps - Visual Studio (Windows)

Die Schleife allein wird nämlich mit Sicherheit keinen Zeitunterschied von 19 Minuten ausmachen.

Aber wir raten ohnehin nur:

  • Wir können an "var" nicht erkennen, von welchen Typen Du redest
  • Wir sehen ne leere Schleife ohne Inhalt
    => Wir kennen defacto nichts von Deinem Code ausser einem Intersect. Wir können nicht mal Vorschläge machen, weshalb der Vorschlag mit dem HashSet auch eher reines Gerate ist.

Irgendwie nen typisches Forenthema:

  • Thread-Ersteller hat ein Problem, sucht den Holzhammer
  • Genaues Problem und Ursache eigentlich unklar
  • Helfer müssen wild raten 😉

Daher: [Hinweis] Wie poste ich richtig? -> bitte Situation so darstellen, dass man sie nachvollziehen kann.
Sonst kann man auch nicht richtig helfen. 😉

A
Arti Themenstarter:in
9 Beiträge seit 2022
vor einem Jahr

Die Struktur der Liste ist wie folgt:


public class md5list
        {
            public string Hash { get; set; }

            public string Path { get; set; }

            public override string ToString()
            {
                return "Hash: " + Hash + " Path: " + Path;
            }
        }

Dh die Liste hat einmal einen 32-Stelligen MD5-Hash und dazu ein dazugehören Pfad/dateiname

Beispiel:
1234567890abcdef1234567890abcdef c:\test\word.exe
Hash + Path

Wenn ich diese Liste mit


foreach (md5list Hashline in intersectList)
            {
                // HIER PASSIERT GARNIX!!!
            }

durchgehe, geht das auf dem i9-11900K in einer Minute, auf dem Xeon W-2265 dauert es 20 minuten.

Um zu testen wird in der foreach-Schleife wirklich garnix gemacht!

Das was auf dem Xeon so langsam ist, ist einfach das Durchgehen durch die Liste mit foreach.

Und irgendwie kann ich mir nicht vorstellen das der Xeon soooooooo langsam ist... die CPU läuft gerade mal auf 15%, einer der Kerne vielleicht auf 60%...
Ich habe das nun auch einem Xeon W2265 und mehrern VMs (alle haben Server-Prozessoren) getestet...
Ergebnis: Server-CPU = ultra-lahm
i9-Desktop = schnell

Gibt es evtl. Compiler-Einstellungen die evtl falsch gesetzt sein könnten?

D
152 Beiträge seit 2013
vor einem Jahr

Was macht denn new DoCompare() ?

T
2.219 Beiträge seit 2008
vor einem Jahr

Hast du dein Programm im Release Mode gebuildet oder einfach klassisch alles aus dem Debug Ordner genommen?
Der Compiler würde im Release vermutlich die leere Schleife schon wegoptimieren.
Kann man aber auch z.B. mit ILSpy und konsorten prüfen.

Der gesamte Code, den du aktuell laufen lässt, wäre aber nicht verkehrt.
Ggf. hast du noch andere Stellen die langsam sind iwe z.B. IO Aufgaben mit lesen/schreiben in Dateien.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.807 Beiträge seit 2008
vor einem Jahr

Gibt es evtl. Compiler-Einstellungen die evtl falsch gesetzt sein könnten?

Nein, so funktioniert .NET auch nicht.

Willst Du eine valide Antwort haben, was das Performance Problem verursacht, brauchst Du immer einen Profiler.
Measure CPU usage in your apps - Visual Studio (Windows)

Erklärung:
Wenn eine Schleife durchlaufen wird, passieren vielen Dinge. Wir sehen zB nicht mal welcher Typ intersectList ist.
Wenn intersectList eine List<md5list> ist hat das auf das foreach völlig andere Auswirkungen, als wenn intersectList IEnumerable<md5list> ist.
Sollte es wirklich eine IEnumerable (oder ähnlich nicht-materialisierte Liste sein), dann sieht das für Dich so aus, dass es die Schleife sei, dabei ist es der Enumerator der Materialisierung im Hintergrund.
Gründe werden hier erklärt: Intermediate materialization (C#)

Deswegen: mach bitte eine ordentliche Performance-Analyse, sonst ist das totaler Käse was hier diskutiert wird.


Ich gehe davon aus, dass Du eine nicht-materialisierte Liste im foreach() hast und irgendeine CPU-intensive Aufgabe das Performance-Problem ist (zB DoCompare).
Das würde zumindest die Verhaltens-Infos, die wir hier haben, erklären.

Edit:

Der Compiler würde im Release vermutlich die leere Schleife schon wegoptimieren.

Nein, würde er aufgrund des Enumerators nichts tun.

A
Arti Themenstarter:in
9 Beiträge seit 2022
vor einem Jahr

Ich poste mal einen Demo-Code der so komplett ist:


using System.IO;

namespace Speed
{
    class Program
    {

        public static List<CListe> Liste = new List<CListe>();

        static void Main(string[] args)
        {
            Console.WriteLine("Start: " + DateTime.Now);
            for (int i = 0; i < 100000; i++)
            {
                Liste.Add(new CListe() { Hash = i.ToString(), Path = "c:\\Temp\\Test\\Word.exe" });

            }
            Console.WriteLine("Befüllt: " + DateTime.Now);

            string a = "";
            foreach (CListe Hashline in Liste)
            {
                a = a + Hashline.Path;
            }

            Console.WriteLine("Fertig: " + DateTime.Now);
        }
        public class CListe
        {
            public string Hash { get; set; }

            public string Path { get; set; }

            public override string ToString()
            {
                return "Hash: " + Hash + " Path: " + Path;
            }
        }
    }
}

Der Teil


            foreach (CListe Hashline in Liste)
            {
                a=a+Hashline.Hash;
            }

dauert auf dem i9-11900K 50 Sekunden
Auf dem Xeon 180 Sekunden - also 3,x mal so lange.

Vielleicht liegt es wirklich nur an der CPU-Performance...

PS: Oben hab ich geschrieben das die Performance auch langsamer ist wenn die Schleife leer ist... Das stimmt nicht.
Mache ich in der foreach-Schleife nichts, sind beide Maschinen in einer Sekunde durch!

4.931 Beiträge seit 2008
vor einem Jahr

Das Problem daran ist nicht die Schleife (bzw. die Größe der Liste), sondern


a=a+Hashline.Hash;

Dadurch erzeugst du einen riesigen String (und intern wird jedesmal der vorherige String umkopiert) - also sehr schlechtes Memorymanagement.
Teste besser mit:


string a = String.Empty;
foreach (CListe hashline in Liste)
{
    a = hashline.Hash;
}
Console.WriteLine(a);
Console.WriteLine("Fertig: " + DateTime.Now);

PS: s.a. [Artikel] Strings verketten: Performance-Betrachtung

A
Arti Themenstarter:in
9 Beiträge seit 2022
vor einem Jahr

Wenn ich


a = hashline.Hash;

sind beide Maschinen in einer Sekunde durch...🙂

Ich denke es ist ein Zusammenspiel zwischen CPU/Speicher/IO - ich denke die langsameren PCs sind einfach deutlich Leistungsschwacher als mein i9

Hat bestimmt auch was mit dem Speicherzugriff zu tun - der scheint bei den Servern viel langsamer zu sein...

T
2.219 Beiträge seit 2008
vor einem Jahr

@Arti
Th69 hat dir schon den Grund genannt.
Du wirfst durch dein zusammen setzen der String Instanzen quasi mit monströsen String Instanzen um sich.
Damit setzt du sowohl CPU als auch RAM unter druck ebenfalls hat der GC dann ordentlich zu tun den Datenmüll wieder wegzuräumen.

Man merkt, dass dir die Grundlagen und das Verständis fehlt.
Daran solltest du arbeiten, ansonsten verweise ich nochmal auf die Antworen von Abt.
Nicht schätzen sondern messen!

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

2.078 Beiträge seit 2012
vor einem Jahr

Spannend ist: Was ist das Ziel?
Dateipfade 100000fach aneinander ketten klingt jedenfalls ziemlich sinnlos.

Wenn das doch dein Ziel sein soll, dann nimm eine Liste oder, wenn Du die Anzahl vorher kennst, ein Array, das dürfte bei der Größe nochmal ein wenig schneller sein.
Da wirfst Du dann jeden Pfad rein und string.Concat verkettet die dann alle.


string[] array = new string[Liste.Count];
for (int i = 0; i < Liste.Count; i++)
{
    array[i] = Liste[i].Path;
}
string a = string.Concat(array);

Ist immer noch nicht optimal, weil ein riesiger String erstellt werden muss, was natürlich auch Zeit und Platz benötigt, aber sollte schneller sein.
Und wenn Du z.B. Duplikate suchen willst, hilft ein HashSet.

Aber besser wäre natürlich, wenn Du das tatsächliche Ziel entsprechend optimierst.