![]() |
Geschwindigkeitsunterschiede bei Objekten/Pointern?
Angenommen, ich möchte einen sehr großen (nur begrenzt durch die Leistungsfähigkeit des Computers) Baum erstellen bzw mit demselben Sachen auswerten:
Ist der Geschwindigkeits-/Speicherplatzunterschied zwischen der Implementation mit reinen Zeigern und der Implementation als Klasse bei einigen 100.000 Baumknoten sehr groß? Hier ist ein Beispiel, wie ich das meine:
Delphi-Quellcode:
Oder gibt sich das nicht viel, da Objekte auch nur Zeiger auf etwas sind?
//Implementation mit reinen Zeigern
type PBaum = ^TBaum TBaum = record info : TInfo; links,rechts : PBaum; end; //Implementation als Klasse type CBaum = class info : TInfo; links,rechts : CBaum; function bli : Tbla; procedure blubb; end; |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Deine Wortwahl ist nicht passend. Entweder schreibst du Zeiger und Referenzen, was aber im Endeffekt wieder dasselbe ist, also keinen Geschwindigkeitsunterschied hat. Oder du schreibst Records und Klassen, wobei dadurch, dass Klassen mehr overhead haben die Records klar die Nase vorne haben, wenn auch nicht all zu weit.
|
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Exemplare von Klassen (=Objekte) halten neben den Exemplarvariablen noch eine "Referenz auf die Klasse", mit deren Hilfe Konstrukte der Art
Delphi-Quellcode:
und Polymorphie iA möglich werden.
if myObject is TMyClass then
Der hierfür benötigte Speicher ist die Größe einer Referenz selbst (zZt 4 Bytes). Darüber hinaus sind Klassen iA für "optimierten Zugriff" ausgerichtet. Diese Optimierung des Compilers hat bei der aktuellen Implementierung zB zur Folge, dass zwei Bytes tatsächlich 8 Bytes "belegen". Bei Records hingegen kann dieses Verhalten explizit mit packed vermieden werden, um Speicher auf Kosten der Performance zu sparen. Der Zugriff auf Exemplarvariablen ist identisch mit dem Zugriff bei Records, allerdings sollten bei Objekten die Exemplarvariablen im Sinne der OOP generell verborgen sein (Geheimnisprinzip). Delphi bietet hier das Konzept der Eigenschaften (Properties), bei der zwar Setter-Methoden zur Zusicherung von Invarianten, zum Lesen jedoch ein direkter Zugriff auf eine verborgene (private) Exemplarvariable angegeben werden kann. Übersetzt wird dieses Konstrukut beim Lesen dann wieder wie der "optimale Zugriff". Lediglich beim Erzeugen und Freigeben von Exemplaren ist die Lösung mit Objekten etwas träger, bietet im Gegenzug aber die vielen Möglichkeiten der OOP :) |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Werden in Klassen nur statische Methoden benutzt so gibt es in fakt keinerlei Performance Unterschiede zu normalen dynamischen Records. Ich empfehle dir also aus Sicht der Übersichtlichkeit Klassen zu benutzen.
Allerdings, werden sehr große Bäume benutzt, so gibt es andere Methoden sie um ein mehrfaches zu beschleunigen. 1. Methode sind Records die ohne Zeigeroperationen auskommen. Dabei wird ein Array of Record benutzt. Man prealloziert eine Array mit zB. 1024 Elementen. Somit wird nur bei jeder 1024'ten Node ein großer Speicher alloziert, statt 1024 mal viel kleine Speicher zu allozieren. 2. Methode ist es in deinem TNode Object die methoden .NewInstance und .FreeInstance zu überschreiben. Beide Methoden greifen nun nicht auf den Borland Speicher manager direkt zu, sondern auf ein Array of TMyObjectRecord. Beide Methoden versuchen den langsammen Speicher Manager zu umgehen, und Chunkweise ganze Objecte auf einmal zu allozieren. Freigegeben Objecte/Records werden auch nicht mehr zum MM durchgereicht, sondern in einem schenllen Pool verlinkter Objecte/Records verwaltet. D.h. eine Neuallokation solcher Objecte/Records ist extrem schnell. Insgesamt ist der Performanceboost weit größer 10%, meistens aber 2-3 mal schneller als der Borland MM. Gruß Hagen |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Zitat:
über den Einsatz eines Nodemanagers, der auch recyceln kann, habe ich im Zusammenhang mit Delphi das erste Mal in "The Tomes of Delphi, Aglorithms and Data Structures", Julian Bucknall gelesen. Obwohl ich eine ähnliche Technik bereits erfolgreich bei Tupel-Mengen verwendet habe, war meine Entscheidungsgrundlage nicht wirklich... metrisch fundiert. Zwar ist in meinem aktuellen Projekt nicht das Speicherhandling sondern die Referenzzählung der Flaschenhals, mich würde trotzdem interessieren, ob Du konkrete Erfahrungswerte für den Einsatz eines Nodemanagers für die Objekterstellung anbieten kannst: Für welche Exemplargrößes lohnt sich der Einsatz? Wie groß ist der tatsächliche Zeitgewinn bei welchen Mengen von Exemplaren? Was gewinnt man bei eine Initialisierung direkt in NewInstance? Wie groß sollten die Chunks gewählt sein? Gibt es elegante Lösung für Polymorhpie, also unterschiedliche Exemplargrößen? etc. |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Zitat:
Zitat:
Für variable Speichergrößen, würde ich beim Borland MM bleiben, denn dieser MM ist eigentlich schon enorm optimiert und einer der schnellsten überhaupt. Zitat:
Zitat:
Zitat:
Zitat:
Gruß Hagen |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Ach eines noch. Der Performancegewinn wurde aber wahrscheinlich aus einem ganz anderen Grund erreicht. Angenommen man hat viele Speicherkopieroperationen von einer Node in die andere (bei mir sind es Large Integer). Dann ist es wichtig zu wissen wie und warum die internen CPU Caches arbeiten. Die Caches arbeiten immer dann optimial wenn 1. die Speicherbereich an 64 Byte Grenzen ausgereichtet sind und 2. die Speichergrenzen der beiden Nodes/Large Integer nicht in der selben Page liegen. Besonders Punkt 2. ist enorm wichtig. Da mein Pool als FIFO arbeitet rotieren sozusagen die allozierten Speicher im Pool. Dadurch passiert es das der Pool succesive Speicherbereich auch meistens succesive im Cache halten kann. Von der Wahrscheinlichkeit her gesehen ist es also so das dieser Pool Objecte zur Verfügung stellt die in unterschiedlichen Cache-Pages liegen. Eine Speicherkopieroperation zwischen zwei solcher unabhäniger Cachelines ist dann sehr viel scheller und verhindert Cachmisses der CPU. Eineinzigster solcher Cachemiss kann ca. 200 Taktzyklen beanspruchen.
Wird beim Borland MM wiederholt ein Object mit gleicher Speichergröße alloziert und dealloziert so wird immer wieder der selbe Speicherbereich benutzt. Dies erscheint auf den ersten Blick als Cache-freundlich, ist es aber nicht. Da durch den anderen Inhalt des neuallozierten Objects ein Cache-refresh auf gleicher Cacheline nötig wird. Eine Cacheline ist aber meistens größer als 512 Bytes. Somit muß die CPU ganze 512 Bytes auffrischen und nachladen. Der Algorithmus heutger Cahces ist aber darauf optimiert das zufällige Speicheradressen nachgeladen und aufgefrischt werden müssen. Eben auf denn meist praktischen Gebrauch in einem PM OS. D.h. der Cache arbeitet relativ ineffizient wenn man sehr geordnete Speicherzugriffe durchführt. Deshalb gibt es ja auch im MMX/SSE Bereich Befehle die für's Audio/Video Streaming den Cache umgehen können. Bei solchen Operationen ist nämlich der Cache eine Bremse. Ähnliches passiert nun wenn man sehr viele wiederholte gleichgroße Speicherchunks alloziert und dealloziert. Gruß Hagen |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
wow, viele neue infos und wörter (chunk etc), von denen ich noch nie etwas gehört habe ;)
wie alloziere ich denn ein objekt in meinem dynamischen array? über so etwas in der art von
Delphi-Quellcode:
?
procedure CBaum.NewInstance(info:TInfo);
begin self := CBaumArray.Add(info); end; function CBaumArray.Add(info:TInfo):CBaum; begin if AnzKnoten = MaxAnzKnoten then ;//Arraylänge verdoppeln knotenarray[AnzKnoten + 1].create(info); result := knotenarray[AnzKnoten + 1]; inc(AnzKnoten); end; Ist das nicht unsauber, da CBaumarray CBaum kennen muß und umgekehrt? vor allem, wenn ich die beide in zwei verschiedene units packe gibts doch kreuzverweise...? |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Zitat:
Zitat:
Zitat:
Zitat:
Zitat:
Danke Hagen, für Deine ausführliche Darstellung! |
Re: Geschwindigkeitsunterschiede bei Objekten/Pointern?
Hallo Tiefflieger,
Deine Implementierung von NewInstance stützt sich nach meinem Eindruck wieder auf einen Konstruktor (und der eigentlichen Aufruf verstehe ich nicht ganz!) und damit auf die ursprüngliche Implementierung? Zitat:
Delphi-Quellcode:
so zu ersetzten, dass der Aufruf von GetMem, also die Verwendung des Borland Memory Managers (BMM) entfällt. Die Funktion GetMem gibt einen Pointer auf einen Speicherbereich der Größe des Exemplars zurück. Und eben dieser Pointer könnte innhalb einer selbstgewählten Struktur (zb einem Array) effizienter ermittelt werden, zB so
class function TObject.NewInstance: TObject;
begin Result := InitInstance(_GetMem(InstanceSize)); end;
Delphi-Quellcode:
Weil dieses Vorgehen an sich aber wiederkehrend ist, könnte man es generisch in einen sog. Nodemanager implementieren, der dann den BMM bei der Erzeugung von Exemplaren einer konkreten Klasse vollständig ersetzt...
Result:= InitInstance(@MyClassArray[GetIndexOfNextFreeNode()]);
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 05:21 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