![]() |
Delphi-Version: XE7
Freigeben von Listen durch Threads beschleunigen
Liste der Anhänge anzeigen (Anzahl: 1)
Moin,
also Sachen gibt's ... :gruebel: Ich habe hier eine Liste mit etwa 5-10 Mio. Einträgen. Ab und an muss ich diese Liste freigeben, um sie neu aufzubauen. Die Objekte sind etwas komplexer, so dass selbst das Freigeben dieser Liste eine messbare Zeit benötigt. Im ersten Ansatz bin ich treudoof von vorn nach hinten durch die Liste und habe die Elemente freigegeben. Das funktioniert selbstverständlich absolut zuverlässig. Nun wollte ich mit zwei Threads daran: Jeder soll sich um eine Hälfte der Liste kümmern und von den Objekten den Destruktor aufrufen. Solange ich dabei nicht die Anzahl an Elementen in der Liste verändere, kann ich ja beliebig darauf herumkaspern - so mein Gedanke. Pustekuchen. Zwei Threads arbeiten länger an der Freigabe der Liste als meine simple Schleife. Mir ist klar, dass Threads einen gewissen Overhead haben, aber ob ich nun eine halbe Mio., 5 Mio. oder 15 Mio. Einträge habe - die Threads sind immer ein wenig langsamer. Da ich nur lesend auf die Liste zugreife und jeder der beiden Threads exklusiv seinen Bereich der Liste für sich hat, habe ich mir jede Form der Synchronisation gespart. Was übersehe ich da? Eigentlich sollte ich mit Threads doch einen Vorteil erziehen können oder nicht? Demo anbei, falls es wen interessiert. Anhang 42631
Delphi-Quellcode:
program FreeWilly;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Classes, System.Diagnostics, System.Threading, System.Generics.Collections; type TMoep = class(TObject); TMoepManager = class(TObject) private FList : TList<TMoep>; public constructor Create; destructor Destroy; override; procedure Build; procedure Clear1; procedure Clear2; end; { TMoepManager } procedure TMoepManager.Build; var i : integer; begin for i := 0 to 49999999 do // fuempfzig mio begin FList.Add( TMoep.Create ); end; end; procedure TMoepManager.Clear1; var i : integer; begin for i := 0 to FList.Count-1 do FList[i].Free; FList.Clear; end; procedure TMoepManager.Clear2; var LTasks : Tarray<ITask>; med : integer; begin med := FList.Count DIV 2; // etwa die mitte SetLength( LTasks, 2 ); LTasks[0] := TTask.Create( procedure var i : integer; begin for i := 0 to med do FList[i].Free; end ); LTasks[1] := TTask.Create( procedure var i : integer; begin for i := med+1 to FList.Count-1 do FList[i].Free; end ); LTasks[0].Start; LTasks[1].Start; TTask.WaitForAll( LTasks ); FList.Clear; end; constructor TMoepManager.Create; begin inherited; FList := TList<TMoep>.Create; end; destructor TMoepManager.Destroy; begin FList.Free; inherited; end; var m : TMoepManager; s : TStopwatch; begin s := TStopwatch.Create; m := TMoepManager.Create; m.Build; s.Start; m.Clear1; s.Stop; WriteLn( 'Clear1: ', s.ElapsedMilliseconds, ' ms' ); s.Reset; m.Build; s.Start; m.Clear2; s.Stop; WriteLn( 'Clear2: ', s.ElapsedMilliseconds, ' ms' ); m.Free; ReadLn; end. |
AW: Freigeben von Listen durch Threads beschleunigen
Mir dünkt, dass FastMM das Problem ist. Da war doch mal was, dass der nicht vollständig skaliert.
Komisch, dass man auf sowas erst kommt, wenn man das Problem niedergeschrieben hat. |
AW: Freigeben von Listen durch Threads beschleunigen
Gibt es einen Grund warum du keine
Delphi-Quellcode:
nimmst? Wenn du die Liste neu aupfbaust, dann kannst du die Liste doch auch einem Thread zum Löschen geben und ein anderer Thread baut eine neue Liste auf.
TObjectList
Hast du übrigens schon mal die Zeit gemessen, die nur das
Delphi-Quellcode:
benötigt? Das dürfte auch nicht unerheblich sein.
FList.Clear
|
AW: Freigeben von Listen durch Threads beschleunigen
Mein echtes Projekt nutzt eine TObjectList, testweise habe ich auch dort mal eine TList versucht. Doch sobald ich sicherstelle, dass alle Destruktoren aufgerufen werden, zeigen die beiden Listenklassen keinen Unterschied in der Laufzeit.
Gleichzeitig freigeben und neu aufbauen habe ich noch nicht versucht, da der Neuaufbau mit n Threads erfolgt und schon alle CPU-Kerne auslastet. DORT habe ich noch Vorteile durch den Einsatz von Threads. ;-) |
AW: Freigeben von Listen durch Threads beschleunigen
Üblicherweise ist Speicherverwaltung und mehrere Threads nicht wirklich toll. Im schlimmsten Fall serialisierst du das Freigeben durch einen Lock wieder vollständig (und hast dann noch den Overhead).
Wenn du die Liste eh wieder aufbaust: Kannst du die Objekte wiederverwenden? Die Performance-Leute für Sprachen mit GC machen das auch ganz "gerne". Bei diesen Dimensionen und je nach Komplexität der Objekte könnte man auch darüber nachdenken, den Speicher für diese Objekte selbst zu verwalten. EDIT: Wenn das einfach möglich ist, probiere auch mal die Objekte von dem Thread freigeben zu lassen, der sie erstellt hat. Wenn jeder Thread einen eigenen Heap hat (übliche Optimierung), dann solltest du so Konflikten aus dem Weg gehen. |
AW: Freigeben von Listen durch Threads beschleunigen
Hast du mal versucht zwei komplett getrennte Listen zu benutzen und zu schauen wie lange deren parallele Freigabe dauert? Dort kannst du dann nämlich messen wie der Unterschied zwischen nur eine der Listen (ohne die zweite Liste) freigeben und parallel freigeben ist.
Wenn das dann pro Liste entsprechend länger dauert, bleibt nur noch der Speichermanager als Flaschenhals. Mir war aber auch so als gäbe es da einen anderen Speichermanager, der genau bei vielen Threads sinnvoller ist, auch wenn er weniger als FastMM kann. Mir fällt nur gerade nicht ein wie der hieß... Ach doch, kurz Google gefragt, scalemm: ![]() |
AW: Freigeben von Listen durch Threads beschleunigen
Im echten Projekt habe ich eine Baum-Struktur. Im konkreten Testfall mit rund 1.800 Elementen auf unterster Ebene. Diese Elemente haben ihrerseits Unter-Elemente in jeweils eigenen Listen, so dass ich in Summe auf die eingangs beschriebene Größenordnung komme.
Die beiden Threads hätten sich die 1.800 Root-Elemente aufteilen sollen. Das zeitliche Verhalten lässt sich mit einer langen Liste identisch nachstellen, so dass ich den Speichermanager als Flaschenhals vermute. FastMM macht ja im Allgemeinen einen guten Job, so dass ich vorläufig an ihm festhalten möchte. Das Thema ist nicht kritisch - ich halte fest, dass es nicht "mal eben so" möglich ist, hier spürbare Vorteile zu erzielen. Ein großartiges Umbauen der Datenstruktur kommt vorläufig nicht in Frage - zumindest nicht mit der alleinigen Motivation, lediglich das Freigeben zu beschleunigen. Manchmal ist die serielle Abarbeitung ja auch ganz charmant. ;-) Nun lege ich wenigstens die in einen Thread, so dass die App nicht blockiert. |
AW: Freigeben von Listen durch Threads beschleunigen
Was für Zeiten hast Du denn wo gemessen?
|
AW: Freigeben von Listen durch Threads beschleunigen
Gemessen habe ich wie oben in meinem Beispiel: Den Gesamtaufwand für Clear1 bzw. Clear2. Also inklusive Erzeugen der Thread-Objekte etc. Das war für mich am naheliegensten, da ich wissen will, wie lange es dauert, mich der Daten wieder zu entledigen.
Die konkreten Werte sind natürlich hardware- und projektabhängig. In meinem Testprojekt messe ich konstant um 300ms für das Zerstören der Liste. Das ist ein Wert, der mir - gerade nach den obigen Überlegungen - zu wenig Leidensdruck verschafft, um hier noch mehr Zeit zu investieren. Es war für mich einfach eine neue Erkenntnis, dass da im Kontext des Speichermanagements offenbar Grenzen existieren. |
AW: Freigeben von Listen durch Threads beschleunigen
10-15 ms kannst Du sparen, wenn Du vorberechnete Werte für die Schleifen verwendest:
Delphi-Quellcode:
procedure TMoepManager.Clear1;
var i : integer; c : Integer; begin c := FList.Count -1; for i := 0 to c do FList[i].Free; FList.Clear; end; |
AW: Freigeben von Listen durch Threads beschleunigen
Die Werte der For-Schleife sind immmer vorberchnet, da Delphi den Endwert zwischenspeichert.
15ms ... wenn du das mit GetTickCount oder Dregleichen gemessen hast, dann ist das eher ein Messfehler. TTask? Warum nimmst du nicht die neue "coole" threaded For-Schleife? Bezüglich des Threads: Wenn die FList und deren Objekte nirgendwo dran hängen, also wie z.B. Owner/Parent-Bezieungen zu deiner Basisklasse (TMoepManager). (auch bei TComponent oder Dregleichen aufpassen, daß es nirgendwo weitere globale Listen/Registrierungspunkte gibt)
PS: Im Gegenzug, kannst du dann auch das erstellen auch in Threads/Tasks machen, oder die neue Liste schonmal vorher erstellen und dann ebenso austauschen. |
AW: Freigeben von Listen durch Threads beschleunigen
Moin,
ich bin mir nicht so ganz sicher, wie Delphi die Listen verwaltet. Aber wenn es eine normale verkettete Liste ist könnte es effizienter sein von hinten anzufangen...(auch der Vergleich auf 0 als Endkriterium für eine Schleife ist normalerweise schneller als der Vergleich auf einen festen Wert) Gruß Dirk P.S.: hoffe habe nicht allzu großen Schwachsinn erzählt... |
AW: Freigeben von Listen durch Threads beschleunigen
Nein, nur leider baut die TList nicht auf eine (doppelt) verkettete Liste auf, sondern auf ein mitwachsendes Array.
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Der Cache heutiger Prozessoren ist eher dafür geeignet große zusammenhängende Speicherbereiche zu verarbeiten. Passt das gesamte Array in den Cache (Objekte oder Pointer brauchen nur 8 Byte je Element), ist auch das Verschieben der Elemente kein Problem. Selbst der oft angeführte Fall des häufigen Einfügen oder Entfernen von Objekten inmitten der Liste ist in der Regel bei verketteten Listen nicht spürbar schneller. Das man erst einmal die richtige Stelle zum Einfügen finden muss, wird bei solchen Vergleichen meist vergessen. |
AW: Freigeben von Listen durch Threads beschleunigen
Habe ja nicht gesagt dass ich das schlecht finde- Das "leider" bezog sich auf Vermutung "Wenn es eine verkettete Liste ist, dann..." ;-)
Auf jeden Fall wieder was gelernt. |
AW: Freigeben von Listen durch Threads beschleunigen
Zum Thema Objekte wiederverwenden:
Dafür könnten diese Methoden der Klasse TMoep überschieben werden:
Delphi-Quellcode:
Benötigt wird eine globale Liste für verfügbare TMoep-Objekte.
class function NewInstance: TObject; override;
procedure FreeInstance; override; FreeInstance speichert "freigegebenen" Objekte in dieser. NewInstance kann sich aus der Liste bedienen, so lange noch "freigegebene" Objekte vorhanden sind. |
AW: Freigeben von Listen durch Threads beschleunigen
Vielleicht fällt dir der Speichermanager auf die Füsse?
Kleine Speicherblöcke legt FastMM doch in mehreren größeren Blöcken an, die durch CriticalSections geschützt sind. Zwei Threads, die auf den selben Verwaltungsblock zugreifen, sperren sich dann natürlich. Insgesamt hätte ich dennoch eine Beschleunigung erwartet, außer der Overhead durch die Threads/Tasks hebt das wieder auf. Auch mehr Threads versucht? (4 oder 8 ... k.A. was du für eine CPU hast) |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
@Blup: Spannende Idee, aber ich sehe mich schon mit Löchern im Strumpf, weil ich mir damit mit hoher Wahrscheinlichkeit selbst in den Fuß schieße. Ich werde das am Wochenende aus Interesse mal ausprobieren, wenngleich ich noch Zweifel habe, ob das der Wartbarkeit des Gesamtprojekts entgegenkommt. |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Zitat:
Zitat:
Wenn man wirklich ein Muster hat, in der man eine große Datenstruktur erzeugt, dann zerstört und wieder neu aufbaut, dann kann man mit mehr Aufwand noch einiges machen ... z.B. braucht man Objekte nicht einzeln freigeben, sondern verwendet einen Block einfach wieder. Dadurch das der dann wieder leer ist, sind selbst Allokationen supergünstig. Das hat dann natürlich mehr Einfluss auf die Wartbarkeit :mrgreen: |
AW: Freigeben von Listen durch Threads beschleunigen
Du kannst ja mal
![]() Aber ich frage mich, ob es überhaupt Sinn machen kann, das Freigeben durch mehrere Threads zu beschleunigen, da der Arbeitsspeicher der Flaschenhals sein sollte und nicht die CPU. Du kannst noch so viele Threads auf das Problem ansetzen, der Durchsatz des Arbeitsspeichers erhöht sich dadurch ja nicht. Selbst mit einem perfekt skalierenden Speichermanager wird da vermutlich kaum etwas rauszuholen sein, wenn überhaupt. |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Und dann spielt eventuell noch die eine oder andere Cache mit. |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Die Datenmange kann es kaum sein.
Konkrete Zahlen aus meinem System / Testprojekt: Das Abbauen der Liste dauert rund 300ms - für einen Rechner als eine durchaus nennenswerte Zeit. Der Taskmanager listet mir für die Anwendung einen Speicherverbrauch von 360 MBytes. Ich weiß, dass der Task-Manager nicht besonders präzise ist, aber für eine grobe Schätzung sollte der Wert langen. Selbst wenn sich das alles in einer VM abspielt, müsste doch mehr an Daten durch den Bus passen. Zudem wird doch gar nicht das komplette Speicher-Abbild durch den Bus gepresst, oder? Ich überschreibe die Bereiche ja nicht mit Nullen oder dgl. |
AW: Freigeben von Listen durch Threads beschleunigen
Aber außer Speicherzugriffen dürfte doch kaum etwas passieren. Ich wüsste zumindest nicht was. Außer du machst irgendwelche komplizierten Dinge im Destruktor.
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Ich habe gerade mal ScaleMM getestet. Der reserviert deutlich mehr RAM dabei, so dass es ein Out of Memory gibt und ist langsamer...
sapmm z.B. reserviert auch mehr, aber nicht so viel mehr, ist aber auch langsamer, insbesondere mit zwei Threads deutlich. Ich bekomme so langsam das Gefühl, dass FastMM doch nicht so schlecht ist. ;-) |
AW: Freigeben von Listen durch Threads beschleunigen
FastMM hat von den kleineren Blöcken extra mehrere, so daß bis zu 3 Threads gleichzeitig RAM "bestellen" können,
aber beim Freigeben oder Ändern kann, je Gruppe, natürlich immer nur Einer gleichzeitig, wenn die Speicher zufällig im selben Block liegen. |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Es gibt gewisse Dinge, die kann man ebend nicht mit multithreading schneller machen (manchmal geht das sogar eher nach hinten los). ScaleMM und Co mögen schneller sein, wenn man innerhalb mehrerer Threads Instanzen erzeugt und wieder freigibt oder mit strings hantiert (siehe ![]() |
AW: Freigeben von Listen durch Threads beschleunigen
Ich hab' mich jetzt eh an anderer Stelle im Projekt verfuddelt. Da hilft alles nix mehr. ;-)
Threads 1..4 bauen die Datenstruktur auf, Threads 5 und 6 solchen selbige durchsuchen, wenn die ersten vier Threads fertig sind. In der Theorie simpel, in der Praxis ein Deadlock. :mrgreen: Ich glaub' das geht grundsätzlich nicht. *g* |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Wo genau lockts denn? Grundsätzlich sollten ja für den start von Task 5 und 6 die ersten 4 fertig sind. Also können nur entweder die ersten 4 sich gegenseitig locken oder 5 und 6. |
AW: Freigeben von Listen durch Threads beschleunigen
Das Projekt sollte auch noch für XE4 kompatibel bleiben, daher nutze ich ganz klassisch TThread.
Jetzt eben das war ein unglücklicher Copy&Paste-Fehler aus der Rubrik "Ich probier mal eben was aus.". ;-) |
AW: Freigeben von Listen durch Threads beschleunigen
Ich würde mal schauen, ob Du dir nicht einen eigenen Speichermanager für deine Objekte schreibst. Dem gibst Du einen Block an Speicher und wenn Du deine neuen Objekte bauen willst, dann dann musst Du gar nichts freigeben. Der Block muss halt nur groß genug sein (der Einfachheit halber). Alternativ kann er auch mehrere Blöcke verwalten. Der ist der einzige, der Speicher wirklich anfordert. Deine Sonderobjekte holen sich ihren Speicher von deinem MM.
Wenn deine Objekte nur aufgebaut werden, aber sonst nicht großartig leben, d.h. freigegeben, neu aufgebaut etc. sollte das doch kein Problem sein:
Delphi-Quellcode:
Wenn Du deine Objekte dann neu aufbauen willst, Rufst Du ClearMemory auf und fertig ist die Laube. Klar, ist nicht gerade Speicheroptimal, wenn Dispose öfter aufgerufen wird, aber ich sagte eingangs ja: Wenn...
function TMyMM.GetMem (size : Integer) : Pointer;
Begin if NextFreeByte+size > maxSizeAvail then raise EOutOfMemory.Create; // oder neuen Block anfordern result := @MyMemoryBlockAsByteArray[NextFreeByte]; inc (NextFreeByte, size); end; function TMyMM.Dispose (p : Pointer; size : Integer); Begin // WTF; End; Procedure TMyMM.ClearMemory; Begin NextFreeByte := 0; End; |
AW: Freigeben von Listen durch Threads beschleunigen
Zitat:
Zitat:
|
AW: Freigeben von Listen durch Threads beschleunigen
@Daniel
Du brauchst dazu 7 Threads Thread 1
|
AW: Freigeben von Listen durch Threads beschleunigen
Klingt wie ein Fall für die
![]() |
Alle Zeitangaben in WEZ +1. Es ist jetzt 18:19 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz