Most EA developers focus on strategy logic while ignoring memory consumption—until optimization runs start crashing with mysterious “out of memory” errors. In large-scale parameter optimizations with multiple agents, memory inefficiency directly limits the number of parallel passes you can run. Understanding and optimizing memory usage is the difference between 8-agent parallelism and 16-agent parallelism.
1. The Hidden Cost Of Dynamic Array Resizing
The most common memory trap in MQL programs is inefficient array resizing. Consider this dangerous pattern:
```cpp
// DANGEROUS - causes repeated memory reallocation
void BadResizeExample() {
double arr[];
for(int i = 0; i < 10000; i++) {
ArrayResize(arr, i); // Physical reallocation on EVERY iteration!
arr[i-1] = i;
}
}
```
Each `ArrayResize()` call without a reserve triggers physical memory reallocation, fragmenting the heap and slowing down execution. The correct approach uses the reserve parameter:
```cpp
// OPTIMIZED - single reallocation with reserve
void OptimizedResizeExample() {
double arr[];
// Reserve 10,000 slots upfront, allocate 1,000 initially
ArrayResize(arr, 1000, 9000);
for(int i = 1000; i < 10000; i++) {
ArrayResize(arr, i, 9000); // No physical reallocation needed
arr[i-1] = i;
}
}
```
The reserve parameter tells the terminal to allocate physical memory for the specified capacity while keeping the logical array size smaller. All subsequent resizes within the reserved capacity incur zero memory reallocation overhead.
2. Memory Monitoring Library Implementation
A simple but powerful memory tracking library can detect which optimization passes are memory-hungry:
```cpp
// Memory.mqh - Memory consumption monitor
#property library
class CMemoryMonitor {
private:
long m_maxMemory;
long m_currentMemory;
long GetCurrentMemory() {
// MQL_MEMORY_USED returns allocated memory in bytes
return MQLInfoInteger(MQL_MEMORY_USED);
}
public:
CMemoryMonitor() {
m_maxMemory = 0;
m_currentMemory = 0;
}
void Update() {
m_currentMemory = GetCurrentMemory();
if(m_currentMemory > m_maxMemory)
m_maxMemory = m_currentMemory;
}
long GetMaxMemory() const { return m_maxMemory; }
long GetCurrentMemory() const { return m_currentMemory; }
void Reset() {
m_maxMemory = 0;
m_currentMemory = 0;
}
};
CMemoryMonitor g_Memory;
// Usage in EA
double OnTester() {
return (double)g_Memory.GetMaxMemory() / (1024 * 1024); // MB
}
```
Add `#property tester_no_cache` to prevent caching of memory-heavy passes, ensuring each optimization iteration runs fresh.
3. Custom Symbol Memory Footprint Optimization
When working with custom symbols in MT5, each synthetic instrument consumes significant memory for tick storage. For Renko or Range bar generation, implement streaming aggregation instead of storing all ticks:
```cpp
// Streaming bar aggregator - minimal memory footprint
class CStreamingBarBuilder {
private:
MqlRates m_currentBar;
double m_threshold; // Renko block size or Range value
int m_barType; // 0=Renko, 1=Range, 2=Volume
long m_tickCount;
public:
void ProcessTick(const MqlTick &tick) {
if(m_tickCount == 0) {
// Initialize first bar
m_currentBar.open = tick.bid;
m_currentBar.high = tick.bid;
m_currentBar.low = tick.bid;
m_currentBar.close = tick.bid;
m_currentBar.time = tick.time;
} else {
// Update current bar
m_currentBar.high = MathMax(m_currentBar.high, tick.bid);
m_currentBar.low = MathMin(m_currentBar.low, tick.bid);
m_currentBar.close = tick.bid;
m_currentBar.volume++;
}
m_tickCount++;
// Check bar completion condition
if(BarComplete()) {
SaveBarToCustomSymbol(m_currentBar);
StartNewBar(tick);
}
}
bool BarComplete() {
switch(m_barType) {
case 0: // Renko - price movement threshold
return (MathAbs(m_currentBar.close - m_currentBar.open) >= m_threshold);
case 1: // Range - high-low range threshold
return ((m_currentBar.high - m_currentBar.low) >= m_threshold);
case 2: // Equal Volume
return (m_tickCount >= (long)m_threshold);
}
return false;
}
};
```
This approach processes ticks on-the-fly without storing the entire tick history, reducing memory consumption from O(n) to O(1).
4. Detecting Memory Leaks In Custom Indicators
Indicator buffers can silently leak memory when incorrectly managed. Always pre-allocate with `ArraySetAsSeries()` and avoid dynamic resizing in `OnCalculate()`:
```cpp
// Memory-safe indicator pattern
int OnCalculate(const int rates_total, const int prev_calculated,
const datetime &time[], const double &open[],
const double &high[], const double &low[],
const double &close[], const long &tick_volume[],
const long &volume[], const int &spread[]) {
static double buffer1[];
static double buffer2[];
// ONE-TIME allocation - critical for memory stability
if(prev_calculated == 0) {
ArrayResize(buffer1, rates_total);
ArrayResize(buffer2, rates_total);
ArraySetAsSeries(buffer1, true);
ArraySetAsSeries(buffer2, true);
}
// Calculate only new bars
int start = MathMax(prev_calculated - 1, 0);
for(int i = start; i < rates_total; i++) {
// Calculation logic here
}
return rates_total;
}
```
5. Complete Memory Stress Test EA
The following EA demonstrates memory profiling during optimization:
```cpp
//+------------------------------------------------------------------+
//| MemoryStressTest.mq5 |
//+------------------------------------------------------------------+
#property copyright "Memory Optimization Lab"
#property version "1.00"
#property tester_no_cache // Prevent caching for accurate per-pass results
#include
input int inDataSizeMB = 50; // Target data size in MB
input int inArrayCount = 5; // Number of parallel arrays
input double inMemoryLimitMB = 100; // Fail passes exceeding this
double g_Arrays[][];
CMemoryMonitor g_Monitor;
int OnInit() {
g_Monitor.Reset();
// Pre-calculate total elements needed
int elementsNeeded = (inDataSizeMB * 1024 * 1024) / sizeof(double);
int perArray = elementsNeeded / inArrayCount;
ArrayResize(g_Arrays, inArrayCount);
for(int i = 0; i < inArrayCount; i++) {
// Allocate with reserve to avoid fragmentation
ArrayResize(g_Arrays[i], perArray / 2, perArray / 2);
}
return INIT_SUCCEEDED;
}
void OnTick() {
// Simulate processing
static int callCount = 0;
callCount++;
if(callCount % 1000 == 0) {
g_Monitor.Update();
}
}
double OnTester() {
double maxMemMB = g_Monitor.GetMaxMemory() / (1024.0 * 1024.0);
if(maxMemMB > inMemoryLimitMB) {
Print("FAIL: Memory exceeded limit: ", maxMemMB, " MB > ", inMemoryLimitMB, " MB");
return -1.0; // Signal optimization to deprioritize this pass
}
Print("PASS: Max memory = ", maxMemMB, " MB");
return maxMemMB; // Lower memory = better fitness
}
```
6. Memory Optimization Checklist For Production EAs
Before running large optimizations, audit your EA against these rules:
| Rule | Implementation |
|------|----------------|
| Always use ArrayResize with reserve | `ArrayResize(arr, size, reserve)` |
| Static allocation for indicator buffers | Allocate once in `OnInit()` or first `OnCalculate()` |
| Clear large arrays after use | `ArrayFree()` or `ArrayResize(arr, 0)` |
| Avoid global tick storage | Stream process ticks instead of storing |
| Monitor with `MQL_MEMORY_USED` | Add memory logging to `OnTester()` |
Reference: MQL5 Documentation, “Memory Management and Optimization” (mql5.com/docs); fxsaber, “Memory.mqh Library” (MQL5 Code Base, 2026).