
Erweiterte Speicherverwaltung und Optimierungstechniken in MQL5
- Einführung
- Verständnis der Speicherverwaltung in MQL5
- Profilierung und Messung der Speichernutzung
- Implementierung von nutzerdefinierten Speicherpools
- Optimierte Datenstrukturen für Handelsanwendungen
- Fortgeschrittene Techniken für den Hochfrequenzhandel
- Schlussfolgerung
Einführung
Herzlich willkommen! Wenn Sie schon einmal Zeit damit verbracht haben, Handelssysteme in MQL5 zu erstellen, sind Sie wahrscheinlich auf diese frustrierende Wand gestoßen - Ihr Expert Advisor beginnt zu verzögern, die Speichernutzung schießt in die Höhe, oder schlimmer noch, das ganze Ding stürzt ab, wenn der Markt interessant wird. Kommt Ihnen das bekannt vor?
MQL5 ist unbestreitbar leistungsstark, aber mit dieser Leistung geht auch Verantwortung einher - vor allem, wenn es um den Speicher geht. Viele Entwickler konzentrieren sich ausschließlich auf Strategielogik, Einstiegspunkte und Risikomanagement, während die Speicherverwaltung im Hintergrund zu einer tickenden Zeitbombe wird. Bei der Skalierung Ihres Codes - Verarbeitung von mehr Symbolen, höheren Frequenzen und umfangreicheren Datensätzen - kann die Vernachlässigung des Speichers zu Leistungsengpässen, Instabilität und verpassten Chancen führen.
In diesem Artikel gehen wir unter die Haube. Wir werden untersuchen, wie der Speicher in MQL5 wirklich funktioniert, die häufigsten Fallen, die Ihre Systeme verlangsamen oder zum Ausfall führen, und - was am wichtigsten ist - wie man sie beheben kann. Sie lernen praktische Optimierungstechniken kennen, die Ihre Handelsprogramme schneller, schlanker und zuverlässiger machen.
Hier ist eine effiziente Speichernutzung besonders wichtig:
-
Hochfrequenzhandel: Jede Millisekunde ist ein potenzieller Vorteil - oder ein potenzieller Verlust.
-
Multi-Timeframe-Analyse: Kombinierte Charts? Erwarten Sie, dass sich der Speicherdruck vervielfachen wird.
-
Schwere Indikatorlogik: Komplexe mathematische Berechnungen und große Datensätze können alles zum Stillstand bringen, wenn sie nicht verwaltet werden.
-
Backtests mit großen Kurshistorien: Ohne intelligente Optimierung können sich Backtests anfühlen, als würde man Farbe beim Trocknen zusehen.
Wenn Sie bereit sind, sich ernsthaft mit der Leistung auseinanderzusetzen, lassen Sie uns eintauchen - und Ihre MQL5-Systeme so effizient wie intelligent machen.
In den folgenden Abschnitten gehen wir Schritt für Schritt vor - von den grundlegenden Konzepten der MQL5-Speicherzuweisung bis hin zu fortgeschrittenen Techniken und Beispielen, die sich auf den Code beziehen. Wenn Sie diese Praktiken befolgen, verfügen Sie über das Know-how, um schnellere, schlankere und widerstandsfähigere Handelssysteme zu entwickeln, die den hohen Anforderungen des modernen algorithmischen Handels gewachsen sind. Fangen wir an!
Verständnis der Speicherverwaltung in MQL5
Wenn man sich an anspruchsvollere Optimierungsstrategien in MQL5 heranwagt, ist es wichtig, zunächst zu verstehen, wie die Sprache den Speicher hinter den Kulissen handhabt. Obwohl MQL5 im Allgemeinen Speicheraufgaben im Vergleich zu einer Sprache mit niedrigerem Niveau wie C++ rationalisiert, müssen die Entwickler immer noch effiziente Codierungspraktiken anwenden.
Unterscheidung zwischen Stack- und Heap-Speicher
MQL5, wie viele moderne Sprachen, teilt seine Speichernutzung zwischen dem Stack und dem Heap auf:
- Stack-Speicher: Dies ist der Ort, an dem lokale Variablen abgelegt werden, wenn ihre Größe zur Kompilierungszeit bekannt ist. Sie wird automatisch verwaltet, und die Zuteilung erfolgt hier sehr schnell.
- Heap-Speicher: Wird für Szenarien verwendet, in denen Sie Speicher dynamisch zuweisen müssen - etwa wenn die Größe erst zur Laufzeit bestimmt wird oder ein Objekt über den Geltungsbereich einer einzelnen Funktion hinaus bestehen bleiben muss.
void ExampleFunction() { // Stack allocation - automatically managed double price = 1.2345; int volume = 100; // Heap allocation - requires proper management double dynamicArray[]; ArrayResize(dynamicArray, 1000); // Use the array... // In MQL5, the array will be automatically deallocated // when it goes out of scope, but this isn't always optimal }
Obwohl MQL5 eine automatische Garbage Collection enthält, kann die alleinige Nutzung dieser Funktion zu Ineffizienzen führen, insbesondere in Hochfrequenzhandelsumgebungen.
Speicherlebenszyklus in MQL5
Um die Leistung zu optimieren, ist es hilfreich, den Weg des Speichers durch Ihr MQL5-Programm zu verfolgen:
- Initialisierung: Wenn Ihr Expert Advisor (EA) oder Indikator startet, gibt MQL5 Speicher für alle globalen Variablen und Klasseninstanzen frei.
- Ereignisbehandlung: Jedes Mal, wenn ein Ereignis ausgelöst wird - z. B. OnTick() oder OnCalculate() - richtet das System lokale Variablen auf dem Stack ein. Er kann auch auf den Heap zurückgreifen, wenn er mehr dynamische Zuweisungen benötigt.
- Freisetzung: In dem Moment, in dem lokale Variablen den Gültigkeitsbereich verlassen, wird der Stapelspeicher automatisch zurückgewonnen. Heap-Zuweisungen werden jedoch in der Regel später durch den Garbage Collector freigegeben.
- Beendigung: Sobald Ihr Programm beendet wird, wird der verbleibende Speicher vollständig freigegeben.
Der springende Punkt ist, dass MQL5 zwar die Deallokation handhabt, dies aber nicht immer sofort oder auf die für zeitkritische Handelsaufgaben optimale Weise tut.
Häufige Fallstricke bei der Speicherung
Trotz der automatischen Garbage Collection ist es möglich, dass Speicherlecks oder eine träge Speichernutzung auftreten.
Hier sind einige häufige Übeltäter:
- Exzessive Objekterstellung: Das ständige Erzeugen neuer Objekte in häufig aufgerufenen Funktionen (wie OnTick) kann Ressourcen verbrauchen.
- Große Arrays: Die Speicherung großer Arrays, die während des gesamten Programmlaufs bestehen bleiben, kann unnötig viel Speicher verschlingen.
- Zirkuläre Referenzen: Wenn zwei Objekte auf einander verweisen, kann dies die Garbage Collection verzögern oder stören.
- Unangemessenes Ressourcenmanagement: Wenn Sie vergessen, Dateien, Datenbankverbindungen oder andere Systemressourcen zu schließen, kann dies zu Speicherverschwendung führen.
// Inefficient approach - creates new arrays on every tick void OnTick() { // This creates a new array on every tick double prices[]; ArrayResize(prices, 1000); // Fill the array with price data for(int i = 0; i < 1000; i++) { prices[i] = iClose(_Symbol, PERIOD_M1, i); } // Process the data... // Array will be garbage collected eventually, but this // creates unnecessary memory churn }
Ein effizienterer Ansatz wäre folgender:
// Class member variable - created once double prices[]; void OnTick() { // Reuse the existing array for(int i = 0; i < 1000; i++) { prices[i] = iClose(_Symbol, PERIOD_M1, i); } // Process the data... }
Oft sind es nur geringfügige Änderungen, wie die Wiederverwendung von Objekten anstelle ihrer wiederholten Instanziierung, die einen wesentlichen Unterschied ausmachen können, insbesondere in schnelllebigen Handelsumgebungen.
Muster für die Speicherzuweisung in MQL5
MQL5 verwendet je nach Datentyp unterschiedliche Speicherzuweisungsmuster:
Schließlich ist es hilfreich zu wissen, wie MQL5 gängige Datentypen zuweist:
-
Primitive Typen (int, double, bool, etc.)
Diese werden in der Regel dem Stack zugewiesen, wenn sie als lokale Variablen deklariert werden. -
Arrays
Dynamische Arrays in MQL5 werden auf dem Heap gespeichert. -
Zeichenketten (Strings)
MQL5-Strings verwenden Referenzzählung und leben auf dem Heap. -
Objekte
Instanzen von Klassen befinden sich ebenfalls auf dem Heap.
Wenn Sie diese Zuweisungsmuster im Hinterkopf behalten, sind Sie besser gerüstet, um Code zu erstellen, der effizienter, stabiler und für reale Handelsbedingungen optimiert ist.
Profilierung und Messung der Speichernutzung
Wenn es darum geht, die Speichernutzung in MQL5 zu optimieren, besteht der erste Schritt darin, genau festzustellen, wo die Engpässe auftreten. Obwohl MQL5 über keine nativen Tools zur Speicherprofilerstellung verfügt, können wir die Ärmel hochkrempeln und einen hausgemachten Ansatz entwickeln.
Erstellung einer einfachen Speicher-Profilierung
Um die Speichernutzung besser in den Griff zu bekommen, können wir eine minimalistische Klasse für die Profilierung einrichten, die die Eigenschaft TERMINAL_MEMORY_AVAILABLE nutzt. Durch den Vergleich des anfänglichen und des aktuell verfügbaren Speichers können Sie verfolgen, wie viel Speicher Ihre Anwendung verbraucht.
//+------------------------------------------------------------------+ //| MemoryProfiler class for tracking memory usage | //+------------------------------------------------------------------+ class CMemoryProfiler { private: ulong m_startMemory; ulong m_peakMemory; string m_profileName; public: // Constructor CMemoryProfiler(string profileName) { m_profileName = profileName; m_startMemory = TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE); m_peakMemory = m_startMemory; Print("Memory profiling started for: ", m_profileName); Print("Initial available memory: ", m_startMemory, " bytes"); } // Update peak memory usage void UpdatePeak() { ulong currentMemory = TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE); if(currentMemory < m_peakMemory) m_peakMemory = currentMemory; } // Get memory usage ulong GetUsedMemory() { return m_startMemory - TerminalInfoInteger(TERMINAL_MEMORY_AVAILABLE); } // Get peak memory usage ulong GetPeakUsage() { return m_startMemory - m_peakMemory; } // Print memory usage report void PrintReport() { ulong currentUsage = GetUsedMemory(); ulong peakUsage = GetPeakUsage(); Print("Memory profile report for: ", m_profileName); Print("Current memory usage: ", currentUsage, " bytes"); Print("Peak memory usage: ", peakUsage, " bytes"); } // Destructor ~CMemoryProfiler() { PrintReport(); } };
Wenn Sie die Klasse CMemoryProfiler in Ihrem Projekt haben, sieht die Umsetzung etwa so aus:
void OnStart() { // Create a profiler for the entire function CMemoryProfiler profiler("OnStart function"); // Allocate some arrays double largeArray1[]; ArrayResize(largeArray1, 100000); profiler.UpdatePeak(); double largeArray2[]; ArrayResize(largeArray2, 200000); profiler.UpdatePeak(); // The profiler will print a report when it goes out of scope }
Bei der Initialisierung des Profilers wird bei seiner Erstellung eine Ausgangslage des verfügbaren Speichers aufgezeichnet. Jedes Mal, wenn Sie UpdatePeak() aufrufen, wird geprüft, ob der aktuelle Speicherbedarf Ihrer Anwendung die zuvor gemessene Höchstmarke überschritten hat. Die Methoden GetUsedMemory() und GetPeakUsage() zeigen Ihnen, wie viel Speicher Sie seit der Basislinie verbraucht haben, während PrintReport() eine Zusammenfassung auf dem Terminal protokolliert. Diese Zusammenfassung wird automatisch generiert, wenn der Profiler den Anwendungsbereich verlässt, und zwar mit Hilfe des Klassendestruktors.
Denken Sie daran, dass dieser Ansatz nur die gesamte Speichernutzung des Terminals misst und nicht den Verbrauch Ihres spezifischen Programms. Dennoch ist es eine praktische Möglichkeit, sich einen Überblick darüber zu verschaffen, wie sich Ihre Speichernutzung im Laufe der Zeit entwickelt.
Benchmarking von Speicheroperationen
Bei der Optimierung der Speichernutzung geht es nicht nur darum, zu wissen, wie viel Speicher Sie verwenden, sondern auch darum, zu verstehen, wie schnell verschiedene Speicheroperationen ausgeführt werden. Durch die Zeitmessung verschiedener Vorgänge können Sie feststellen, wo sich Ineffizienzen verbergen, und mögliche Leistungsverbesserungen entdecken.
//+------------------------------------------------------------------+ //| Benchmark different memory operations | //+------------------------------------------------------------------+ void BenchmarkMemoryOperations() { const int iterations = 1000; const int arraySize = 10000; // Benchmark array allocation ulong startTime = GetMicrosecondCount(); for(int i = 0; i < iterations; i++) { double tempArray[]; ArrayResize(tempArray, arraySize); // Do something minimal to prevent optimization tempArray[0] = 1.0; } ulong allocTime = GetMicrosecondCount() - startTime; // Benchmark array reuse double reuseArray[]; ArrayResize(reuseArray, arraySize); startTime = GetMicrosecondCount(); for(int i = 0; i < iterations; i++) { ArrayInitialize(reuseArray, 0); reuseArray[0] = 1.0; } ulong reuseTime = GetMicrosecondCount() - startTime; // Benchmark string operations startTime = GetMicrosecondCount(); for(int i = 0; i < iterations; i++) { string tempString = "Base string "; for(int j = 0; j < 100; j++) { // Inefficient string concatenation tempString = tempString + IntegerToString(j); } } ulong stringConcatTime = GetMicrosecondCount() - startTime; // Benchmark string builder approach startTime = GetMicrosecondCount(); for(int i = 0; i < iterations; i++) { string tempString = "Base string "; string parts[]; ArrayResize(parts, 100); for(int j = 0; j < 100; j++) { parts[j] = IntegerToString(j); } tempString = tempString + StringImplode(" ", parts); } ulong stringBuilderTime = GetMicrosecondCount() - startTime; // Print results Print("Memory operation benchmarks:"); Print("Array allocation time: ", allocTime, " microseconds"); Print("Array reuse time: ", reuseTime, " microseconds"); Print("String concatenation time: ", stringConcatTime, " microseconds"); Print("String builder time: ", stringBuilderTime, " microseconds"); Print("Reuse vs. Allocation speedup: ", (double)allocTime / reuseTime); Print("String builder vs. Concatenation speedup: ", (double)stringConcatTime / stringBuilderTime); }
Diese einfache Testfunktion zeigt, wie man die Ausführungsgeschwindigkeit verschiedener speicherintensiver Aufgaben messen kann. Er vergleicht die Leistung der wiederholten Zuweisung von Arrays mit der Wiederverwendung eines einzelnen, bereits zugewiesenen Arrays sowie den Unterschied zwischen einer einfachen Text-Verkettung und einem Ansatz im Stil eines „String Builders“. Diese Tests verwenden GetMicrosecondCount(), um die Zeit in Mikrosekunden zu messen, damit Sie einen genauen Überblick über etwaige Verzögerungen erhalten.
In der Regel werden Ihre Ergebnisse zeigen, dass die Wiederverwendung von Arrays einen klaren Leistungsvorteil gegenüber der Zuweisung neuer Arrays in jeder Schleife bietet und dass das Sammeln von Zeichenkettenteilen in einem Array (und das anschließende Zusammenfügen) stückweise Verkettungen übertrifft. Diese Unterscheidungen sind besonders kritisch in Hochfrequenzhandelsszenarien, in denen es auf den Bruchteil einer Millisekunde ankommen kann.
// Helper function for string array joining string StringImplode(string separator, string &array[]) { string result = ""; int size = ArraySize(array); for(int i = 0; i < size; i++) { if(i > 0) result += separator; result += array[i]; } return result; }
Wenn Sie den Benchmark ausführen, erhalten Sie konkrete Daten darüber, wie die verschiedenen Speicheroperationen in MQL5 abschneiden. Mit diesen Erkenntnissen sind Sie gut gerüstet, um Anpassungen vorzunehmen, die Ihre Handelsroboter schlank und effizient machen.
Implementierung von nutzerdefinierten Speicherpools
Wenn die Leistung im Vordergrund steht, ist eine herausragende Strategie zur Rationalisierung der Speichernutzung das Speicherpooling. Anstatt das System ständig nach Speicher zu fragen und ihn dann wieder zurückzugeben, besteht der Trick darin, einen Teil des Speichers vorab zuzuweisen und ihn selbst zu verwalten. In diesem Abschnitt wird untersucht, wie dies in einfachen und fortgeschrittenen Szenarien geschehen kann.
Grundlegende Objektpool-Implementierung
Stellen Sie sich vor, Sie haben eine Klasse namens CTradeSignal, die Sie häufig instanziieren und zerstören - vielleicht in einem Hochfrequenzhandelssystem. Anstatt wiederholt auf die Speicherzuweisung zuzugreifen, erstellen Sie einen eigenen Pool für diese Objekte. Nachstehend finden Sie ein einfaches Beispiel:
//+------------------------------------------------------------------+ //| Trade signal class that will be pooled | //+------------------------------------------------------------------+ class CTradeSignal { public: datetime time; double price; ENUM_ORDER_TYPE type; double volume; bool isValid; // Reset the object for reuse void Reset() { time = 0; price = 0.0; type = ORDER_TYPE_BUY; volume = 0.0; isValid = false; } }; //+------------------------------------------------------------------+ //| Object pool for CTradeSignal instances | //+------------------------------------------------------------------+ class CTradeSignalPool { private: CTradeSignal* m_pool[]; int m_poolSize; int m_nextAvailable; public: // Constructor CTradeSignalPool(int initialSize = 100) { m_poolSize = initialSize; ArrayResize(m_pool, m_poolSize); m_nextAvailable = 0; // Pre-allocate objects for(int i = 0; i < m_poolSize; i++) { m_pool[i] = new CTradeSignal(); } Print("Trade signal pool initialized with ", m_poolSize, " objects"); } // Get an object from the pool CTradeSignal* Acquire() { // If we've used all objects, expand the pool if(m_nextAvailable >= m_poolSize) { int oldSize = m_poolSize; m_poolSize *= 2; // Double the pool size ArrayResize(m_pool, m_poolSize); // Allocate new objects for(int i = oldSize; i < m_poolSize; i++) { m_pool[i] = new CTradeSignal(); } Print("Trade signal pool expanded to ", m_poolSize, " objects"); } // Get the next available object CTradeSignal* signal = m_pool[m_nextAvailable++]; signal.Reset(); // Ensure it's in a clean state return signal; } // Return an object to the pool void Release(CTradeSignal* &signal) { if(signal == NULL) return; // In a more sophisticated implementation, we would // actually track which objects are in use and reuse them. // For simplicity, we're just decrementing the counter. if(m_nextAvailable > 0) m_nextAvailable--; signal = NULL; // Clear the reference } // Destructor ~CTradeSignalPool() { // Clean up all allocated objects for(int i = 0; i < m_poolSize; i++) { delete m_pool[i]; } Print("Trade signal pool destroyed"); } };
Im obigen Ausschnitt weist CTradeSignalPool einen Stapel von CTradeSignal-Objekten vor und verwaltet sorgfältig deren Lebenszyklen. Wenn Sie Acquire() aufrufen, wird Ihnen der Pool ein verfügbares Objekt übergeben. Wenn er keine mehr zur Verfügung hat, vergrößert er sich und macht weiter. Sobald Sie mit einem Objekt fertig sind, gibt Release() es zurück in die Obhut des Pools.
Der Hauptvorteil besteht in einer erheblichen Verringerung des Overheads, der durch das ständige Zuweisen und Freigeben von Speicher entsteht. Dies ist besonders praktisch, wenn Sie Objekte in schnellem Tempo durchgehen, wie z. B. Handelssignale in einer Hochgeschwindigkeitsumgebung.
Nachfolgend ein kurzes Beispiel, wie Sie diesen Pool verwenden könnten:
// Global pool instance CTradeSignalPool* g_signalPool = NULL; void OnInit() { // Initialize the pool g_signalPool = new CTradeSignalPool(100); } void OnTick() { // Acquire a signal from the pool CTradeSignal* signal = g_signalPool.Acquire(); // Set signal properties signal.time = TimeCurrent(); signal.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK); signal.type = ORDER_TYPE_BUY; signal.volume = 0.1; signal.isValid = true; // Process the signal... // Return the signal to the pool when done g_signalPool.Release(signal); } void OnDeinit(const int reason) { // Clean up the pool delete g_signalPool; g_signalPool = NULL; }
Da der Pool seine Objekte recycelt, verringert er den Aufwand für die wiederholte Erstellung und Zerstörung, der sonst anfallen würde.
Erweiterter Speicherpool für Zuweisungen variabler Größe
Manchmal stehen Sie vor komplexeren Anforderungen, wie z. B. der Notwendigkeit, unterschiedlich große Speicherbereiche zu verwalten. Für diese Fälle können Sie einen fortgeschritteneren Pool bauen:
//+------------------------------------------------------------------+ //| Advanced memory pool for variable-size allocations | //| MQL5 version without raw pointer arithmetic | //+------------------------------------------------------------------+ #property strict class CMemoryPool { private: // Usage tracking bool m_blockUsage[]; // Size settings int m_totalSize; // Total bytes in the pool int m_blockSize; // Size of each block // Statistics int m_used; // How many bytes are currently in use public: // Memory buffer (dynamic array of bytes) uchar m_memory[]; // Constructor CMemoryPool(const int totalSize=1024*1024, // default 1 MB const int blockSize=1024) // default 1 KB blocks { m_totalSize = totalSize; m_blockSize = blockSize; m_used = 0; // Allocate the memory pool ArrayResize(m_memory, m_totalSize); // Initialize block usage tracking int numBlocks = m_totalSize / m_blockSize; ArrayResize(m_blockUsage, numBlocks); ArrayInitialize(m_blockUsage, false); Print("Memory pool initialized: ", m_totalSize, " bytes, ", numBlocks, " blocks of ", m_blockSize, " bytes each"); } // Allocate memory from the pool // Returns an offset (>= 0) if successful, or -1 on failure int Allocate(const int size) { // Round up how many blocks are needed int blocksNeeded = (size + m_blockSize - 1) / m_blockSize; int consecutive = 0; int startBlock = -1; // Search for consecutive free blocks int numBlocks = ArraySize(m_blockUsage); for(int i=0; i < numBlocks; i++) { if(!m_blockUsage[i]) { // Found a free block if(consecutive == 0) startBlock = i; consecutive++; // If we found enough blocks, stop if(consecutive >= blocksNeeded) break; } else { // Reset consecutive = 0; startBlock = -1; } } // If we couldn't find enough consecutive blocks if(consecutive < blocksNeeded) { Print("Memory pool allocation failed: needed ", blocksNeeded, " consecutive blocks"); return -1; // indicate failure } // Mark the found blocks as used for(int b=startBlock; b < startBlock + blocksNeeded; b++) { m_blockUsage[b] = true; } // Increase usage m_used += blocksNeeded * m_blockSize; // Return the offset in bytes where allocation starts return startBlock * m_blockSize; } // Free memory (by offset) void Free(const int offset) { // Validate offset if(offset < 0 || offset >= m_totalSize) { Print("Memory pool error: invalid offset in Free()"); return; } // Determine the starting block int startBlock = offset / m_blockSize; // Walk forward, freeing used blocks int numBlocks = ArraySize(m_blockUsage); for(int b=startBlock; b < numBlocks; b++) { if(!m_blockUsage[b]) break; // found an already-free block => done // Free it m_blockUsage[b] = false; m_used -= m_blockSize; } } // Get usage statistics in % double GetUsagePercentage() const { return (double)m_used / (double)m_totalSize * 100.0; } // Destructor ~CMemoryPool() { // Optionally free arrays (usually automatic at script end) ArrayFree(m_memory); ArrayFree(m_blockUsage); Print("Memory pool destroyed. Final usage: ", GetUsagePercentage(), "% of ", m_totalSize, " bytes"); } }; //+------------------------------------------------------------------+ //| Example usage in an Expert Advisor | //+------------------------------------------------------------------+ int OnInit(void) { // Create a memory pool CMemoryPool pool(1024*1024, 1024); // 1 MB total, 1 KB block size // Allocate 500 bytes from the pool int offset = pool.Allocate(500); if(offset >= 0) { // Write something in the allocated area pool.m_memory[offset] = 123; Print("Wrote 123 at offset=", offset, " usage=", pool.GetUsagePercentage(), "%"); // Free this block pool.Free(offset); Print("Freed offset=", offset, " usage=", pool.GetUsagePercentage(), "%"); } return(INIT_SUCCEEDED); } void OnTick(void) { // ... }
Diese Klasse CMemoryPool richtet einen großen, vorab zugewiesenen Speicherpuffer ein und unterteilt ihn dann in Stücke fester Größe. Wenn Sie Speicher anfordern, sucht es genügend benachbarte Blöcke, um diesen Bedarf zu decken, kennzeichnet sie als belegt und gibt einen Zeiger auf den Anfang dieser Reihe von Blöcken zurück. Wenn Sie den Speicher freigeben, werden diese Blöcke wieder in den Status „verfügbar“ versetzt.
Anstelle von Zuweisungen im C++-Stil verwendet dieser Ansatz MQL5-Array-Funktionen wie ArrayResize, ArrayInitialize und ArrayFree und fügt sich damit nahtlos in das Speicher-Ökosystem von MQL5 ein. Es nutzt auch GetPointer() , das einen sicheren Weg zur Handhabung von Array-Zeigern in MQL5 bietet.
Dies ist der Grund, warum dieser Ansatz so besonders ist:
- Weniger Fragmentierungen: Die Verwaltung des Speichers in übersichtlichen Chunks mit fester Größe hilft, die Fragmentierung zu vermeiden, die bei häufigen Zuweisungen auftritt.
- Verbesserte Leistung: Einen Speicherblock aus dem eigenen Pool anzufordern ist in der Regel schneller, als jedes Mal die Systemzuweisung in Anspruch zu nehmen.
- Verbesserte Sichtbarkeit: Detaillierte Nutzungsstatistiken Ihres Pools können Aufschluss über speicherbezogene Problembereiche geben.
- Vorhersehbarkeit: Die Vorab-Zuweisung verringert die Wahrscheinlichkeit von Fehlern, die sich in einem kritischen Moment außerhalb des Speichers befinden.
Dieser robustere Pool ist ideal, wenn Sie Speicherblöcke unterschiedlicher Größe benötigen, beispielsweise für komplizierte Datenstrukturen oder dynamische Arbeitslasten, die sich häufig ändern. Durch die Anpassung Ihrer Pools - sei es ein einfacher Objektpool oder ein leistungsfähigerer Pool mit variabler Größe - können Sie die Speichernutzung unter Kontrolle halten und die Leistung in anspruchsvollen Anwendungen optimieren.
Optimierte Datenstrukturen für Handelsanwendungen
Bei der Verarbeitung von Zeitreihen in Handelsumgebungen benötigen Sie Datenstrukturen, durch die die Leistung nicht hinter dem Markt zurückbleiben. Lassen Sie uns zwei leistungsstarke Strategien für die Speicherung und den Abruf Ihrer Preisdaten mit maximaler Effizienz untersuchen.
Datenspeicher für Zeitreihen, die keinen Tick verpassen
Ein Arbeitspferd in Handelssystemen ist der Puffer der Preishistorie - und ein optimierter Ringspeicher kann diese Last mit Leichtigkeit schultern. Im Folgenden finden Sie ein Beispiel, wie Sie ein solches System einrichten könnten:
//+------------------------------------------------------------------+ //| Circular buffer for price data | //+------------------------------------------------------------------+ class CPriceBuffer { private: double m_prices[]; int m_capacity; int m_head; int m_size; public: // Constructor CPriceBuffer(int capacity = 1000) { m_capacity = capacity; ArrayResize(m_prices, m_capacity); m_head = 0; m_size = 0; } // Add a price to the buffer void Add(double price) { m_prices[m_head] = price; m_head = (m_head + 1) % m_capacity; if(m_size < m_capacity) m_size++; } // Get a price at a specific index (0 is the most recent) double Get(int index) { if(index < 0 || index >= m_size) return 0.0; int actualIndex = (m_head - 1 - index + m_capacity) % m_capacity; return m_prices[actualIndex]; } // Get the current size int Size() { return m_size; } // Get the capacity int Capacity() { return m_capacity; } // Clear the buffer void Clear() { m_head = 0; m_size = 0; } // Calculate simple moving average double SMA(int period) { if(period <= 0 || period > m_size) return 0.0; double sum = 0.0; for(int i = 0; i < period; i++) { sum += Get(i); } return sum / period; } };
Hier verwendet die Klasse CPriceBuffer einen zirkulären Puffer, der um ein Array mit fester Größe herum aufgebaut ist. Der Zeiger auf den „Kopf“ verschiebt sich bis zum Ende des Arrays, sodass es möglich ist, neue Preiseinträge ohne aufwändige Größenänderungen hinzuzufügen. Wenn die Kapazität des Puffers erreicht ist, werden die ältesten Einträge einfach mit neuen Daten überschrieben, sodass ein reibungslos gleitendes Fenster mit den neuesten Preisen erhalten bleibt.
Warum dieser Ansatz so effizient ist:
- Der Speicher wird vorab zugewiesen und wiederverwendet, wodurch der Overhead durch ständige Erweiterungen entfällt.
- Das Hinzufügen neuer Preise und das Abrufen der neuesten Daten erfolgt in O(1) Zeit.
- Der Schiebefenster-Mechanismus verwaltet alte und neue Einträge automatisch und problemlos.
Nachfolgend finden Sie einen kurzen Ausschnitt, der zeigt, wie Sie diese Funktion nutzen können:
// Global price buffer CPriceBuffer* g_priceBuffer = NULL; void OnInit() { // Initialize the price buffer g_priceBuffer = new CPriceBuffer(5000); } void OnTick() { // Add current price to the buffer double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); g_priceBuffer.Add(price); // Calculate moving averages double sma20 = g_priceBuffer.SMA(20); double sma50 = g_priceBuffer.SMA(50); // Trading logic based on moving averages... } void OnDeinit(const int reason) { // Clean up delete g_priceBuffer; g_priceBuffer = NULL; }
Cache-freundliche Strukturen für extra Zip
Moderne CPUs leben von der Cache-Effizienz. Wenn Sie die Daten so anordnen, dass der Prozessor nur die Teile abruft, die er benötigt, können Sie die Zeitverschwendung erheblich reduzieren. Schauen Sie sich dieses Layout für die Speicherung von OHLC-Daten (Open, High, Low, Close) an:
//+------------------------------------------------------------------+ //| Cache-friendly OHLC data structure | //+------------------------------------------------------------------+ class COHLCData { private: int m_capacity; int m_size; // Structure of arrays (SoA) layout for better cache locality datetime m_time[]; double m_open[]; double m_high[]; double m_low[]; double m_close[]; long m_volume[]; public: // Constructor COHLCData(int capacity = 1000) { m_capacity = capacity; m_size = 0; // Allocate arrays ArrayResize(m_time, m_capacity); ArrayResize(m_open, m_capacity); ArrayResize(m_high, m_capacity); ArrayResize(m_low, m_capacity); ArrayResize(m_close, m_capacity); ArrayResize(m_volume, m_capacity); } // Add a new bar bool Add(datetime time, double open, double high, double low, double close, long volume) { if(m_size >= m_capacity) return false; m_time[m_size] = time; m_open[m_size] = open; m_high[m_size] = high; m_low[m_size] = low; m_close[m_size] = close; m_volume[m_size] = volume; m_size++; return true; } // Get bar data by index bool GetBar(int index, datetime &time, double &open, double &high, double &low, double &close, long &volume) { if(index < 0 || index >= m_size) return false; time = m_time[index]; open = m_open[index]; high = m_high[index]; low = m_low[index]; close = m_close[index]; volume = m_volume[index]; return true; } // Get size int Size() { return m_size; } // Process all high values (example of cache-friendly operation) double CalculateAverageHigh() { if(m_size == 0) return 0.0; double sum = 0.0; for(int i = 0; i < m_size; i++) { sum += m_high[i]; } return sum / m_size; } // Process all low values (example of cache-friendly operation) double CalculateAverageLow() { if(m_size == 0) return 0.0; double sum = 0.0; for(int i = 0; i < m_size; i++) { sum += m_low[i]; } return sum / m_size; } };
Die Klasse COHLCData unterteilt jedes Attribut (z. B. high oder low) in ein eigenes Array - eine Struktur der Arrays (SoA) - anstelle des traditionelleren Arrays der Structuren (AoS). Warum ist das wichtig? Angenommen, Sie möchten den Durchschnitt aller Hochs berechnen. Bei einem SoA-Setup gleitet der Prozessor durch eine zusammenhängende Reihe der Hochs und muss weniger Speicherzugriffe vornehmen. Im Gegensatz dazu ist die CPU bei einem AoS gezwungen, die Daten von Open, Low, Close und Volume zu überspringen, nur um jedes Hoch zu erfassen.
Mit COHLCData wird es Ihnen leicht fallen:
- Fügen Sie neue OHLC-Balken im laufenden Betrieb hinzu.
- Abrufen bestimmter Balken nach Index.
- Führen Sie Berechnungen auf jedem einzelnen Feld durch (z. B. alle Höchstwerte), ohne über nicht verwandte Daten zu stolpern.
Diese Design-Entscheidung bedeutet, dass Ihre technische Analyse - egal ob gleitende Durchschnitte, Volatilitätsberechnungen oder einfach nur die Suche nach Ausbrüchen - dank der besseren Cache-Lokalisierung viel effizienter abläuft.
Fortgeschrittene Techniken für den Hochfrequenzhandel
Der Hochfrequenzhandel (HFT) erfordert extrem niedrige Latenzzeiten und konstante Leistung. Selbst die kleinste Verzögerung kann die Ausführung des Handels stören und zu verpassten Chancen führen. Im Folgenden werden zwei zentrale Ansätze - Vorabzuweisung und simulierte Speicherzuordnung - untersucht, die dazu beitragen können, die Latenzzeit in MQL5 auf ein absolutes Minimum zu reduzieren.
Strategien vor der Zuteilung
Wenn Ihr System innerhalb von Mikrosekunden reagieren muss, können Sie sich keine unvorhersehbaren Verzögerungen leisten, die durch fliegende Speicherzuweisung verursacht werden. Die Lösung ist die Vorabzuweisung - reservieren Sie im Voraus den gesamten Speicher, den Ihre Anwendung möglicherweise benötigt, sodass Sie in Spitzenzeiten nie mehr Speicher zuweisen müssen.
//+------------------------------------------------------------------+ //| Pre-allocation example for high-frequency trading | //+------------------------------------------------------------------+ class CHFTSystem { private: // Pre-allocated arrays for price data double m_bidPrices[]; double m_askPrices[]; datetime m_times[]; // Pre-allocated arrays for calculations double m_tempArray1[]; double m_tempArray2[]; double m_tempArray3[]; // Pre-allocated string buffers string m_logMessages[]; int m_logIndex; int m_capacity; int m_dataIndex; public: // Constructor CHFTSystem(int capacity = 10000) { m_capacity = capacity; m_dataIndex = 0; m_logIndex = 0; // Pre-allocate all arrays ArrayResize(m_bidPrices, m_capacity); ArrayResize(m_askPrices, m_capacity); ArrayResize(m_times, m_capacity); ArrayResize(m_tempArray1, m_capacity); ArrayResize(m_tempArray2, m_capacity); ArrayResize(m_tempArray3, m_capacity); ArrayResize(m_logMessages, 1000); // Pre-allocate log buffer Print("HFT system initialized with capacity for ", m_capacity, " data points"); } // Add price data void AddPriceData(double bid, double ask) { // Use modulo to create a circular buffer effect int index = m_dataIndex % m_capacity; m_bidPrices[index] = bid; m_askPrices[index] = ask; m_times[index] = TimeCurrent(); m_dataIndex++; } // Log a message without allocating new strings void Log(string message) { int index = m_logIndex % 1000; m_logMessages[index] = message; m_logIndex++; } // Perform calculations using pre-allocated arrays double CalculateSpread(int lookback = 100) { int available = MathMin(m_dataIndex, m_capacity); int count = MathMin(lookback, available); if(count <= 0) return 0.0; double sumSpread = 0.0; for(int i = 0; i < count; i++) { int index = (m_dataIndex - 1 - i + m_capacity) % m_capacity; sumSpread += m_askPrices[index] - m_bidPrices[index]; } return sumSpread / count; } };
Die Klasse CHFTSystem veranschaulicht, wie die Vorabzuteilung in einen HFT-Rahmen integriert werden kann. Sie richtet alle Arrays und Puffer im Voraus ein und stellt sicher, dass keine zusätzlichen Speicheranforderungen auftreten, sobald die Handelsmaschine in Betrieb ist. Zirkuläre Puffer werden verwendet, um ein gleitendes Fenster aktueller Preisdaten aufrechtzuerhalten, wodurch eine kostspielige Neuzuweisung entfällt. Temporäre Arrays für Berechnungen und ein spezieller Puffer für Protokollmeldungen werden ebenfalls im Voraus eingerichtet. Auf diese Weise vermeidet diese Strategie das Risiko von plötzlichen Allokationsspitzen, wenn die Marktbedingungen am kritischsten sind.
Memory-Mapped Files für große Datensätze
Einige Handelsstrategien stützen sich auf riesige Mengen historischer Daten - manchmal mehr, als Ihr verfügbarer Arbeitsspeicher verarbeiten kann. Während MQL5 keine nativen memory-mapped Dateien unterstützt, können Sie den Ansatz mit Standard-Dateieingabe/Ausgabe emulieren:
//+------------------------------------------------------------------+ //| Simple memory-mapped file simulation for large datasets | //+------------------------------------------------------------------+ class CDatasetMapper { private: int m_fileHandle; string m_fileName; int m_recordSize; int m_recordCount; // Cache for recently accessed records double m_cache[]; int m_cacheSize; int m_cacheStart; public: // Constructor CDatasetMapper(string fileName, int recordSize, int cacheSize = 1000) { m_fileName = fileName; m_recordSize = recordSize; m_cacheSize = cacheSize; // Open or create the file m_fileHandle = FileOpen(m_fileName, FILE_READ|FILE_WRITE|FILE_BIN); if(m_fileHandle != INVALID_HANDLE) { // Get file size and calculate record count m_recordCount = (int)(FileSize(m_fileHandle) / (m_recordSize * sizeof(double))); // Initialize cache ArrayResize(m_cache, m_cacheSize * m_recordSize); m_cacheStart = -1; // Cache is initially empty Print("Dataset mapper initialized: ", m_fileName, ", ", m_recordCount, " records"); } else { Print("Failed to open dataset file: ", m_fileName, ", error: ", GetLastError()); } } // Add a record to the dataset bool AddRecord(double &record[]) { if(m_fileHandle == INVALID_HANDLE || ArraySize(record) != m_recordSize) return false; // Seek to the end of the file FileSeek(m_fileHandle, 0, SEEK_END); // Write the record int written = FileWriteArray(m_fileHandle, record, 0, m_recordSize); if(written == m_recordSize) { m_recordCount++; return true; } return false; } // Get a record from the dataset bool GetRecord(int index, double &record[]) { if(m_fileHandle == INVALID_HANDLE || index < 0 || index >= m_recordCount) return false; // Check if the record is in cache if(index >= m_cacheStart && index < m_cacheStart + m_cacheSize) { // Copy from cache int cacheOffset = (index - m_cacheStart) * m_recordSize; ArrayCopy(record, m_cache, 0, cacheOffset, m_recordSize); return true; } // Load a new cache block m_cacheStart = (index / m_cacheSize) * m_cacheSize; int fileOffset = m_cacheStart * m_recordSize * sizeof(double); // Seek to the start of the cache block FileSeek(m_fileHandle, fileOffset, SEEK_SET); // Read into cache int read = FileReadArray(m_fileHandle, m_cache, 0, m_cacheSize * m_recordSize); if(read > 0) { // Copy from cache int cacheOffset = (index - m_cacheStart) * m_recordSize; ArrayCopy(record, m_cache, 0, cacheOffset, m_recordSize); return true; } return false; } // Get record count int GetRecordCount() { return m_recordCount; } // Destructor ~CDatasetMapper() { if(m_fileHandle != INVALID_HANDLE) { FileClose(m_fileHandle); Print("Dataset mapper closed: ", m_fileName); } } };
Die Klasse CDatasetMapper simuliert das Memory Mapping, indem sie Datensätze fester Größe in eine Binärdatei liest und schreibt und die zuletzt aufgerufenen Elemente in einem kleinen In-Memory-Cache speichert. So können Sie mit Datensätzen von praktisch unbegrenzter Größe arbeiten und gleichzeitig den Leistungsaufwand beim Lesen von sequentiellen Daten oder nahe beieinander liegenden Datensätzen in Grenzen halten. Obwohl es sich nicht um ein echtes Speicher-Mapping auf Betriebssystemebene handelt, bietet es viele der gleichen Vorteile - insbesondere die Möglichkeit, umfangreiche Datensätze zu verarbeiten, ohne den Systemspeicher zu erschöpfen.
Schlussfolgerung
Bei der Speicheroptimierung geht es nicht nur darum, ein paar Bytes einzusparen - es geht um Geschwindigkeit, Stabilität und Kontrolle. Bei MQL5, wo jede Millisekunde zählt, wird die intelligente Speicherverwaltung zu einem echten Wettbewerbsvorteil.
In diesem Artikel haben wir praktische Strategien untersucht, die weit über die Theorie hinausgehen: das Verständnis des internen Speichermodells von MQL5, die Wiederverwendung von Objekten zur Verringerung des Overheads, die Entwicklung von Cache-freundlichen Datenstrukturen und der Aufbau von nutzerdefinierten Speicherpools für Hochfrequenzhandelsumgebungen.
Die goldene Regel? Raten Sie nicht, sondern messen Sie. Die Profilierung deckt auf, wo die wirklichen Engpässe liegen, sodass Sie präzise optimieren können. Ob es sich um die Vorabzuweisung von Speicher zur Vermeidung von Laufzeitlatenz oder um die Simulation von Speicherzuordnungen zur effizienten Arbeit mit großen Datensätzen handelt, jede von uns behandelte Technik dient einem Zweck: Ihre MQL5-Anwendungen schneller und stabiler zu machen.
Wenden Sie auch nur einige dieser Techniken an, und Sie werden den Unterschied spüren. Ihre Systeme werden schlanker, schneller und besser für die Anforderungen des modernen algorithmischen Handels gerüstet sein.
Das ist nicht das Ende - es ist nur der Anfang. Experimentieren Sie weiter, verfeinern Sie weiter und bringen Sie Ihre Leistung auf die nächste Stufe.
Viel Spaß beim Handeln! Viel Spaß beim Coding!
Dateiname | Beschreibung |
---|---|
BenchmarkMemoryOperations.mq5 | Code, der zeigt, wie man Speicheroperationen wie Array-Zuweisung, Wiederverwendung und String-Verkettung in MQL5 bewertet und vergleicht. |
MemoryPoolUsage.mq5 | Beispielimplementierung, die zeigt, wie man nutzerdefinierte Speicherpools für Zuweisungen variabler Größe in MQL5 verwendet. |
PriceBufferUsage.mq5 | Beispielskript, das die praktische Verwendung eines zirkulären Preispuffers für die effiziente Verarbeitung von Zeitreihendaten zeigt. |
SignalPoolUsage.mq5 | Beispiel für die Verwendung eines Objektpools zur effizienten Verwaltung häufig verwendeter Handelssignalobjekte. |
CDatasetMapper.mqh | Header-Datei, die die Implementierung eines simulierten Memory-Mapped-File-Mechanismus zur Verarbeitung großer Datenmengen enthält. |
CHFTSystem.mqh | Header-Datei, die eine Klasse für Hochfrequenzhandelssysteme definiert, die Vorabzuweisungsstrategien zur Minimierung der Latenzzeit verwenden. |
CMemoryProfiler.mqh | Header-Datei, die eine einfache Klasse zur Profilierung des Speichers zur Messung der Speichernutzung in MQL5-Anwendungen definiert. |
COHLCData.mqh | Header-Datei mit einer cache-freundlichen Datenstruktur, die für die effiziente Speicherung von OHLC-Preisdaten optimiert ist. |
CPriceBuffer.mqh | Header-Datei, die die Implementierung des Ringspeichers enthält, der für die schnelle Speicherung und den schnellen Abruf von Preisdaten optimiert ist. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17693





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Der Artikel sieht sehr fragwürdig aus (nur ein paar Punkte).
Was ist die Klasse, die Sie hier erwähnt haben?
Das Vorhandensein des OnTick-Handlers und die Art und Weise, wie auf das Array zugegriffen wird, deutet darauf hin, dass Sie das Preis-Array in den globalen Bereich eingefügt haben, was eine schlechte Idee ist (wegen der Namespace-Verschmutzung, wenn das Array nur im Bereich des Handlers benötigt wird). Wahrscheinlich wäre es angemessener, den ursprünglichen Code aus dem gleichen Beispiel beizubehalten, aber das Array statisch zu machen, so dass jeder den Unterschied klar erkennen kann:
Auch wenn Sie Array of Structures (AoS) mit Structure of Arrays (SoA) für OHLCV ersetzen - der Zugriff auf die Preise der gleichen Bar braucht mehr Referenzen (Umschalten zwischen Arrays statt Inkrementierung Offset innerhalb einer einzigen Struktur) und verlangsamt den Prozess, aber solche Operationen sind sehr häufig.
Für dieses Beispiel mit OHLCV wäre es aus Gründen der Speicher- und Zeiteffizienz wahrscheinlich interessanter, alle Werte in ein einziges 2D- oder sogar 1D-Array zu packen:
Dies ist möglich, weil alle Werte der Typen (double, datetime, long) die gleiche Größe von 8 Byte haben und direkt ineinander gecastet werden können.
Für dieses Beispiel mit OHLCV wäre es aus Gründen der Speicher- und Zeiteffizienz wahrscheinlich interessanter, alle Werte in ein einziges 2D- oder sogar 1D-Array zu packen:
Ein 2D-Array anstelle eines Arrays von Strukturen spart vielleicht ein wenig Prozessorzeit, aber es erhöht den Zeitaufwand des Entwicklers für die Entwicklung und Pflege des Codes erheblich. Meiner persönlichen Meinung nach stimme ich mit dem Rest Ihrer Aussagen überein.
https://www.mql5.com/de/articles/17693#sec2
Schauen wir uns ein problematisches Beispiel an:
Ein effizienterer Ansatz wäre:
Der Artikel sieht sehr fragwürdig aus (nur ein paar Punkte).
Was ist die Klasse, die Sie hier erwähnt haben?
Das Vorhandensein des OnTick-Handlers und die Art und Weise, wie auf das Array zugegriffen wird, deutet darauf hin, dass Sie das Preis-Array in den globalen Bereich eingefügt haben, was eine schlechte Idee ist (wegen der Namespace-Verschmutzung, wenn das Array nur im Bereich des Handlers benötigt wird). Wahrscheinlich wäre es angemessener, den ursprünglichen Code des gleichen Beispiels beizubehalten, aber das Array statisch zu machen, so dass jeder den Unterschied klar erkennen kann:
Soweit ich es verstanden habe, handelt es sich bei dem Beispiel (das ich oben zitiert habe) grob gesagt um Pseudocode, d.h. der Autor achtet nicht auf das Folgende (um sich auf das zu konzentrieren, wovon er genau spricht, nehme ich an):
Im Hinblick auf die Effizienz, ich vermute, es wäre besser, die gesamte folgende Schleife mit einem einzigen CopySeries Aufruf zu ersetzen:
Korrigieren Sie mich, wenn ich falsch liege, aber soweit ich mich erinnere, enthält jeder iClose-Aufruf einen CopySeries-Aufruf unter der Haube.
Der vorliegende Artikel enthält aufschlussreiche und zum Nachdenken anregende Inhalte, die zur Diskussion anregen.
Die technische Darstellung ist klar und gut erklärt, so dass der Leser ihr leicht folgen und sich mit ihr beschäftigen kann.
Ich danke Ihnen vielmals.