Обобщенный пул объектов в MQL5: Снижение накладных расходов на выделение памяти в высокочастотных индикаторах
Введение
Каждый вызов оператора new в MQL5 запрашивает непрерывный блок памяти из управляемой кучи терминала. Аллокатор ищет свободный блок достаточного размера, помечает его как занятый и возвращает указатель. Оператор delete, напротив, снова помечает этот блок как доступный. По отдельности эти операции выполняются быстро. Однако непрерывное выполнение операций с высокой частотой приводит к возникновению накладных расходов, которые накапливаются с течением времени.
Основные временные затраты при многократном выполнении new/delete в OnCalculate() связаны не с уплотнением кучи (подробности устройства внутреннего аллокатора терминала MQL5 не документированы публично на таком уровне), а с непосредственными накладными расходами на каждый вызов выделения и освобождения памяти. К ним относятся вызовы конструкторов и деструкторов, служебные операции аллокатора, инициализация памяти и джиттер времени выполнения, возникающие на часто выполняемом участке кода (горячем пути). Даже если отдельные операции выделения памяти в среднем выполняются быстро, непредсказуемость временных затрат на них важна в торговле, где стабильность задержки зачастую ценится выше, чем средняя пропускная способность сама по себе.
Пользовательские индикаторы, выполняющие OnCalculate() при каждом тике, увеличивают эти накладные расходы. Во время важных новостных событий частота поступления тиков может превышать 100 в секунду. При такой частоте аллокатор выполняет две операции с кучей на каждый тик, сопровождаемые созданием и уничтожением объекта. Эта нагрузка приводит к постепенному увеличению разброса времени выполнения в течение торговой сессии.

Рисунок 1: Сравнение состояния памяти и сложности поиска (после 500 циклов).
Архитектура пула объектов
Реализация пула состоит из трёх компонентов. CSignalEvent — это класс полезных данных, экземпляры которого используются повторно и представляют результат вычисления одного сигнала. CObjectPool<T> — это обобщенный механизм пула, управляющий жизненным циклом объектов любого типа, допускающего использование в пуле. Пользовательский индикатор PoolBenchmark.mq5 тестирует оба компонента в условиях имитации поступления тиков и регистрирует измеримые различия во временных затратах между стратегиями выделения памяти с использованием пула и без него.
Пул работает по модели списка свободных элементов. При создании пул выделяет в куче память ровно для m_capacity экземпляров типа T и сохраняет указатели на них в плоском массиве. Второй массив целых чисел той же длины содержит индексы всех доступных в данный момент объектов. Целочисленная переменная m_free_count хранит число действительных записей в этом массиве индексов.
При получении объекта значение m_free_count уменьшается на единицу, считывается индекс слота, хранящийся в m_free_indices[m_free_count], и возвращается указатель, хранящийся в m_objects[slot_index]. Обращений к аллокатору кучи не происходит. При возврате объекта в пул с помощью GetPoolIndex() считывается индекс слота, хранящийся в самом объекте, проверяется указатель, вызывается Reset() для сброса полей полезных данных, сбрасывается флаг занятости и индекс слота записывается обратно в стек свободных индексов. И Acquire(), и Release() имеют сложность O(1). Перебор для проверки принадлежности не требуется.
![Внутренняя структура памяти CObjectPool, иллюстрирующая взаимосвязь между m_free_indices[] и m_objects[]. Внутренняя структура памяти CObjectPool, иллюстрирующая взаимосвязь между m_free_indices[] и m_objects[].](https://c.mql5.com/2/223/NEWFigure_2_CObjectPool_Internals.png)
Рисунок 2: Схема внутренней организации памяти CObjectPool, иллюстрирующая взаимосвязь между m_free_indices[] и m_objects[]. Курсор m_free_count разделяет стек индексов на зоны занятых и доступных слотов. При получении объекта считывается индекс целевого слота и извлекается соответствующий указатель за время O(1). При возврате объекта в пул считывается индекс слота, хранящийся внутри самого объекта, сбрасываются полезные данные, а индекс слота возвращается в стек свободных индексов за время O(1).
Почему для списка свободных элементов используется массив индексов, а не стек указателей
В более простой реализации в массиве свободных элементов хранились бы сами доступные указатели, а не их индексы. Реализация на основе индексов предпочтительна по двум причинам. Во-первых, это сохраняет возможность проверить, действительно ли возвращаемый объект принадлежит данному экземпляру пула: проверка идентичности m_objects[slot] == obj подтверждает это за время O(1). Во-вторых, при таком подходе массив свободных элементов остается обычным массивом int[], а не T*[], что позволяет избежать ограничений, связанных с типом указателя, для параметра типа обобщенного пула и упрощает операции ArrayResize, необходимые при инициализации.
Вместимость пула и политика обработки переполнения
Пул имеет фиксированную вместимость, задаваемую при его создании. Когда пул исчерпан, функция Acquire() возвращает NULL. Вызывающая сторона должна обработать этот случай. Резервное выделение памяти в куче с помощью new T() не предусмотрено.

Рисунок 3: Блок-схемы принятия решений для функций Acquire() и Release(). Метод Acquire() возвращает указатель на объект за время O(1), если есть свободные слоты, либо NULL с предупреждением в журнале при исчерпании пула. Метод Release() проверяет указатель, предотвращая повторный возврат с помощью IsInUse(), подтверждает принадлежность объекта пулу с помощью IsPooled(), затем определяет слот за время O(1) с помощью GetPoolIndex(), после чего сбрасывает полезные данные и возвращает слот в стек свободных индексов.
Класс полезных данных CSignalEvent
CSignalEvent представляет результат вычисления одного сигнала и содержит направление, опорную цену, значение силы сигнала и временную метку. Он полностью реализует контракт пула, требуемый классом CObjectPool<T>.
Метод Reset() очищает только поля полезных данных, относящиеся к состоянию сигнала: m_direction, m_price, m_strength и m_timestamp. Метаданные принадлежности пулу, а именно m_is_pooled, m_pool_index и m_in_use, находятся исключительно под управлением CObjectPool<T>; метод Reset() никогда их не изменяет. Это разделение сделано намеренно. Класс полезных данных не должен зависеть от аллокатора, управляющего памятью его экземпляров. Включение решений, касающихся жизненного цикла, в метод Reset() приведет к привязке бизнес-логики к управлению памятью, что затруднит анализ и независимое повторное использование данного класса. Пул задает и поддерживает все данные о принадлежности объектов пулу с момента создания и на протяжении всего цикла выдачи и возврата объектов.
Таблица свойств CSignalEvent
| Свойство | Тип | По умолчанию | Назначение |
|---|---|---|---|
| m_direction | int | 0 | Направление сигнала: 1 — на покупку, -1 — на продажу, 0 — нейтральное |
| m_price | double | 0.0 | Опорная цена на момент формирования сигнала |
| m_strength | double | 0.0 | Нормализованное значение силы сигнала (от 0.0 до 1.0) |
| m_timestamp | datetime | 0 | Время сервера на момент вычисления сигнала |
| m_is_pooled | bool | false | Флаг принадлежности пулу; устанавливается только классом CObjectPool<T> |
| m_pool_index | int | -1 | Индекс слота внутри пула; обеспечивает вызов Release() со сложностью O(1) |
| m_in_use | bool | false | Флаг занятости; предотвращает повреждение данных при повторном возврате |
Методы контракта CSignalEvent
Конструктор инициализирует поля полезных данных с помощью метода Reset() и задает метаданным пула безопасные значения по умолчанию (m_is_pooled = false, m_pool_index = -1, m_in_use = false). CObjectPool<T> сразу же перезаписывает их правильными значениями во время создания самого пула.
Метод Reset() обнуляет все поля полезных данных и ничего больше:
//+------------------------------------------------------------------+ //| Reset business-state payload fields to defaults for pool reuse. | //| Does NOT touch m_is_pooled, m_pool_index, or m_in_use. | //| Pool ownership metadata is the pool's responsibility, not the | //| object's payload logic. | //+------------------------------------------------------------------+ void CSignalEvent::Reset(void) { m_direction = 0; m_price = 0.0; m_strength = 0.0; m_timestamp = 0; //--- m_is_pooled, m_pool_index, and m_in_use are intentionally NOT reset here. //--- Pool lifecycle metadata is managed exclusively by CObjectPool<T>. }
Методы SetPooled() и IsPooled() позволяют задать и проверить признак того, что объект был заранее создан пулом. Методы SetPoolIndex() и GetPoolIndex() позволяют задать и получить индекс слота, который метод Release() использует для возврата объекта за время O(1). Методы SetInUse() и IsInUse() обеспечивают защиту от повторного возврата: Release() проверяет IsInUse() перед приемом объекта и отклоняет его с предупреждением, если флаг уже имеет значение false.
Механизм работы шаблона класса CObjectPool
MQL5 поддерживает шаблоны классов с тем же синтаксисом template<typename T>, что и в C++. Параметр шаблона T в классе CObjectPool<T> должен быть типом класса, реализующим полный контракт пула: Reset(), SetPooled(bool), IsPooled(), SetPoolIndex(int), GetPoolIndex(), SetInUse(bool) и IsInUse(). Если тип T не реализует эти методы, компилятор выдаст ошибку в месте инстанцирования, а не в месте определения шаблона. Это ожидаемое поведение шаблонов классов MQL5.
Таблица интерфейса методов CObjectPool
| Метод | Тип возвращаемого значения | Доступ | Операции с памятью |
|---|---|---|---|
| CObjectPool(int capacity) | — | общедоступный | Выделяет память в куче для всех экземпляров T; записывает индексы слотов; помечает все экземпляры как незанятые |
| ~CObjectPool() | — | общедоступный | Удаляет все экземпляры T, хранящиеся в m_objects[]; освобождает память обоих массивов |
| Acquire() | T* | общедоступный | O(1): уменьшает значение m_free_count, устанавливает флаг занятости, возвращает указатель |
| Release(T* obj) | void | общедоступный | O(1): считывает индекс слота из obj.GetPoolIndex(); проверяет, не является ли возврат повторным; вызывает obj.Reset(); записывает индекс в стек свободных индексов |
| FreeCount() | int | общедоступный | Возвращает текущее значение m_free_count — операция с памятью не выполняется |
| Capacity() | int | общедоступный | Возвращает m_capacity — операция с памятью не выполняется |
| Utilization() | double | общедоступный | Возвращает (m_capacity − m_free_count) / m_capacity × 100.0 |
| IsExhausted() | bool | общедоступный | Возвращает значение m_free_count == 0 |
Конструктор
Конструктор выполняет все операции выделения памяти в куче за весь срок существования пула. Размер каждого из двух массивов устанавливается равным m_capacity одним вызовом ArrayResize. В цикле создаются все объекты типа T. В каждом объекте с помощью SetPoolIndex() сохраняется индекс его слота, объект помечается как незанятый и принадлежащий пулу, а его индекс записывается в стек свободных индексов. После завершения конструктора при обычном получении объектов из пула и возврате их в пул обращений к аллокатору кучи больше не происходит.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ template<typename T> CObjectPool::CObjectPool(int capacity) : m_capacity(capacity), m_free_count(capacity) { ArrayResize(m_objects, m_capacity); ArrayResize(m_free_indices, m_capacity); //--- Instantiate all objects and record their slot indices for(int i = 0; i < m_capacity; i++) { m_objects[i] = new T(); m_objects[i].SetPoolIndex(i); // Store slot index inside the object (O(1) release) m_objects[i].SetInUse(false); // Mark as free at construction m_objects[i].SetPooled(true); // All pre-allocated objects belong to this pool m_free_indices[i] = i; } }
Acquire()
Метод Acquire() выполняется за время O(1). Если в пуле есть свободные слоты, значение m_free_count уменьшается на единицу, из m_free_indices[m_free_count] считывается индекс слота, у объекта устанавливается флаг занятости и возвращается указатель m_objects[slot]. Обращений к аллокатору кучи не происходит. Когда пул исчерпан, возвращается значение NULL и выводится предупреждение. Резервное выделение памяти в куче не предусмотрено.
//+------------------------------------------------------------------+ //| Acquire an instance pointer from the pool | //| Returns NULL when pool is exhausted; caller must check. | //| No heap allocation occurs on this path after construction. | //+------------------------------------------------------------------+ template<typename T> T *CObjectPool::Acquire(void) { //--- O(1) pool path: return next available object from free stack if(m_free_count > 0) { m_free_count--; T *obj = m_objects[m_free_indices[m_free_count]]; obj.SetInUse(true); return(obj); } //--- Pool exhausted: caller must handle null //--- Do NOT fall back to new T() here: doing so reintroduces heap //--- allocation into the hot path, defeating the entire purpose of //--- the pool. Size the pool correctly in OnInit() instead. Print("[CObjectPool] WARNING: Pool exhausted. NULL returned. Increase pool capacity."); return(NULL); }
Release()
Метод Release() обрабатывает четыре случая: указатель NULL игнорируется без вывода сообщений; повторный возврат (IsInUse() возвращает false) отклоняется с выводом предупреждения; указатель на объект, не принадлежащий этому пулу (IsPooled() возвращает false), отклоняется с выводом предупреждения; у допустимого объекта, принадлежащего пулу, сбрасываются полезные данные и флаг занятости, а индекс его слота возвращается в стек свободных индексов за время O(1) с использованием сохраненного индекса.
//+------------------------------------------------------------------+ //| Release an active object pointer back to the pool | //| O(1): uses the slot index stored inside the object itself. | //+------------------------------------------------------------------+ template<typename T> void CObjectPool::Release(T *obj) { if(obj == NULL) return; //--- Guard against double-release if(!obj.IsInUse()) { Print("[CObjectPool] WARNING: Double-release detected. Object was already free."); return; } //--- Only accept objects that belong to this pool instance if(!obj.IsPooled()) { Print("[CObjectPool] WARNING: Released pointer does not belong to this pool. Ignored."); return; } //--- Validate the stored slot index is in range int slot = obj.GetPoolIndex(); if(slot < 0 || slot >= m_capacity || m_objects[slot] != obj) { Print("[CObjectPool] WARNING: Invalid slot index on released object. Ignored."); return; } //--- Reset business-state payload (ownership metadata is NOT touched by Reset) obj.Reset(); obj.SetInUse(false); //--- Return slot to free stack in O(1) m_free_indices[m_free_count] = slot; m_free_count++; }
Когда использовать пул объектов, а когда — более простые альтернативы
Пул объектов — это правильный выбор в следующих случаях: когда одновременно должно существовать много объектов одного и того же типа, их количество ограничено и предсказуемо, они создаются и освобождаются с высокой частотой, а профилирование подтвердило, что выделение памяти существенно влияет на время выполнения.
Для индикаторов, в которых на каждый тик используется только один или два объекта, пул — излишнее усложнение архитектуры. Более простое и столь же эффективное решение — заранее созданный глобальный объект или объект, являющийся полем класса, который повторно используется на каждом тике:
//--- Best option when only one object is needed per tick: CSignalEvent g_event; // global or class member - allocated once in OnInit() //+------------------------------------------------------------------+ //| Initialization | //+------------------------------------------------------------------+ int OnCalculate(...) { g_event.Reset(); g_event.SetDirection(...); g_event.SetPrice(...); //--- ... use g_event ... return(rates_total); }
Этот подход не создает накладных расходов на выделение памяти, исключает риск исчерпания пула и возможность повторного возврата, а также не требует реализации дополнительных методов контракта в классе полезных данных. Рассматривать использование пула следует только тогда, когда более простой подход с повторным использованием объектов перестанет соответствовать потребностям или профилирование покажет его недостаточность.
Практический порядок выбора подходов — от самого простого и эффективного до самого сложного:
- Локальные переменные без объекта — когда данные настолько просты, что объект не привносит никакой архитектурной ценности.
- Один постоянно существующий объект, повторно используемый на каждом тике, — подходит для подавляющего большинства схем работы индикаторов, использующих один объект на тик.
- Массив объектов с заранее выделенной памятью — когда одновременно должно существовать фиксированное небольшое число объектов.
- Пул объектов — когда большое, но ограниченное число однотипных короткоживущих объектов с высокой частотой получают из пула и возвращают в него.
- new/delete при каждом тике — избегайте этого в функциях OnCalculate() и OnTick().
Организация теста производительности
При каждом вызове функции OnCalculate(), соответствующем поступлению нового тика в режиме реального времени, индикатор выполняет два варианта вычислений. В первом варианте с помощью оператора new создаётся объект CSignalEvent, заполняются его поля, их значения считываются в локальные переменные, после чего объект немедленно удаляется. Во втором варианте объект CSignalEvent получают из пула, заполняют точно так же, считывают те же поля и возвращают объект в пул.
Оба пути выполняют одинаковую работу по обработке полезных данных. Единственное структурное отличие заключается в способе получения и освобождения объектов: через кучу либо через пул. Тест производительности измеряет именно эту разницу, а не затраты на вычисление сигнала.
Контекст теста производительности: это синтетический микротест производительности, который намеренно изолирует накладные расходы аллокатора, выполняя inp_iterations циклов выделения памяти на каждом тике. У реальных индикаторов, создающих один объект за каждый тик, абсолютная разница будет меньше. Ценность этого теста производительности в том, что он демонстрирует наличие и измеримость накладных расходов на выделение памяти, а не доказывает масштаб снижения производительности какого-либо конкретного индикатора в реальной эксплуатации. Прежде чем принимать решение об использовании пула, выполните профилирование своего индикатора.
При первом полном проходе пересчета (prev_calculated == 0) индикатор возвращает rates_total - 1, не выполняя ни один из двух вариантов теста производительности. Значения для исторических баров остаются равными EMPTY_VALUE. После поступления тика в реальном времени на текущем баре начинают отображаться результаты измерений, которые обновляются с каждым последующим тиком. При закрытом рынке или редком поступлении тиков по торговому инструменту подокно будет оставаться пустым до получения тика в реальном времени — это корректное и предусмотренное поведение.
Для измерения времени используется функция GetMicrosecondCount(). Перед записью в буферы время выполнения каждого варианта проверяется на соответствие диапазону допустимых значений. Значения ниже нуля (сброс счетчика) или выше одного миллиона микросекунд (прерывание на системном уровне) отбрасываются, а тик пропускается. Это предотвращает искажение данных в накопителях для расчета средних значений за сеанс из-за кратковременных аномалий.

Рисунок 4: Схема теста производительности с двумя вариантами выполнения в OnCalculate(). При первом проходе буферы заполняются значением EMPTY_VALUE, а тест производительности не запускается. На каждом тике в реальном времени оба варианта выполняют одинаковую работу с полезными данными объекта CSignalEvent: вариант без пула использует new и delete, а вариант с пулом — Acquire() и Release(). Перед записью в индикаторные буферы время выполнения в микросекундах, измеренное с помощью GetMicrosecondCount(), проверяется на соответствие диапазону допустимых значений.
Примечания по реализации индикаторных буферов
Три детали реализации в файле PoolBenchmark.mq5 влияют на правильное отображение графика, и их стоит явно зафиксировать в документации.
PLOT_EMPTY_VALUE должен быть равен EMPTY_VALUE, а не 0.0: если задать 0.0 в качестве маркера отсутствия данных, механизм построения графиков будет считать каждый слот буфера с нулевым значением — то есть всю инициализированную историю — не содержащим данных и не будет его отображать. Поскольку измеренное время обработки тиков при быстром выполнении также вполне может округляться до значений, близких к нулю, это приводило к тому, что подокно оставалось постоянно пустым. Правильный маркер отсутствия данных — EMPTY_VALUE (DBL_MAX): это значение никогда не может быть результатом реального измерения в микросекундах. Соответственно, буферы инициализируются значением EMPTY_VALUE с помощью ArrayInitialize(..., EMPTY_VALUE).
Функцию ArraySetAsSeries нельзя вызывать для буферов типа INDICATOR_DATA: терминал управляет индикаторными буферами, привязанными с помощью SetIndexBuffer(..., INDICATOR_DATA), в прямом порядке: индекс 0 соответствует самому старому бару, а индекс rates_total-1 — текущему бару. Вызов ArraySetAsSeries(true) для этих буферов обращает порядок индексации для вызывающего кода, но не меняет внутренний порядок индексации в терминале, из-за чего данные записываются не в те бары и график остается пустым. Для устранения этой проблемы необходимо удалить все вызовы ArraySetAsSeries для индикаторных буферов и обращаться к текущему бару по индексу rates_total - 1.
Для разреженных тиковых данных, поступающих в реальном времени, предпочтительнее использовать DRAW_SECTION, а не DRAW_LINE: DRAW_LINE соединяет каждую отображаемую точку со следующей прямым отрезком, в том числе перекрывая разрыв между последним историческим слотом со значением EMPTY_VALUE и первым измерением в реальном времени. На таймфреймах с редкими тиками, таких как H1, это приводит к появлению артефакта в виде почти вертикального скачка, который искажает данные. Режим DRAW_SECTION проводит линии только между последовательными непустыми барами, а в остальных местах оставляет разрывы, точно показывая, для каких именно баров были получены измерения в реальном времени.
Ожидаемая производительность и ограничения
Разница во времени выполнения двух вариантов обработки одного тика зависит от частоты поступления тиков и нагрузки на аллокатор. Для торгового инструмента с высокой частотой поступления тиков, например ETHUSD на таймфрейме M1, где тики поступают несколько раз в минуту, наблюдаемое время составляло примерно 25 мкс без пула объектов и 12 мкс с пулом объектов, что давало соотношение около 2,1. Линия времени выполнения без пула объектов также показывает выраженные колебания от тика к тику, зигзагообразно меняясь между измерениями, тогда как линия времени выполнения с пулом объектов проходит ниже и остается более ровной. Эти колебания и представляют собой джиттер времени выполнения, который устраняет пул и который зачастую важнее в реальной торговле, чем сама по себе разница в средних накладных расходах.
Для торгового инструмента с более низкой частотой поступления тиков, например EURUSD на таймфрейме H1, где тики поступают с интервалом в несколько минут, а нагрузка на кучу минимальна, наблюдаемое время составляло примерно 4–13 мкс без пула объектов и 2–7 мкс с пулом объектов. Это соотношение остается стабильным на уровне примерно 1,9–2,1, однако абсолютные значения меньше, поскольку аллокатор работает с "холодной", практически нефрагментированной кучей. Это прямо подтверждает вывод о том, что для торговых инструментов с редкими тиками зачастую достаточно одного повторно используемого объекта, а пул дает лишь незначительное дополнительное преимущество по сравнению с этой более простой альтернативой.
Наиболее наглядный диагностический показатель — линия Overhead Ratio ("Отношение накладных расходов") в подокне, которая для каждого тика показывает отношение времени выполнения без пула объектов ко времени выполнения с пулом объектов. Рост этого отношения в течение сеанса указывает на увеличение накладных расходов аллокатора, которые устраняются в варианте обработки с пулом. Постоянное соотношение в течение сеанса указывает на то, что аллокатор достаточно эффективно справляется с объектами данного размера при данной частоте тиков, поэтому приемлем любой из подходов.
Архитектурные ограничения
Потокобезопасность: пул не является потокобезопасным. В MQL5 каждый экземпляр индикатора или советника выполняется в одном потоке, что устраняет необходимость в мьютексе. Если в будущих обновлениях терминала будет реализовано параллельное выполнение, методам Acquire() и Release() потребуются атомарные операции над m_free_count.
Фиксированный размер: размер пула не изменяется динамически. Задайте размер пула в OnInit() исходя из максимального числа одновременно существующих объектов, определенного при профилировании. Предусмотрите небольшой запас. Если при эксплуатации пул часто исчерпывается (следите за предупреждением о NULL в журнале), увеличьте его вместимость и заново подключите индикатор.
Расходы памяти: пул хранит m_capacity экземпляров типа T на протяжении всего сеанса работы индикатора. Общий объем занимаемой памяти равен вместимости пула × sizeof(CSignalEvent). Для 64 объектов этот объем пренебрежимо мал, но при очень больших пулах или типах T очень большого размера следует учитывать фиксированный объем занимаемой памяти
Заключение
Многократное выделение памяти в куче в высокочастотных циклах индикатора приводит к накладным расходам и джиттеру времени выполнения в OnCalculate(), где стабильность времени выполнения важнее самой по себе средней скорости. Шаблон "пул объектов" решает эту проблему, ограничивая все операции с кучей этапом инициализации и используя операции с массивом индексов, выполняемые за время O(1), для последующего управления жизненным циклом объектов.
Реализация основана на двух ключевых архитектурных решениях. Во-первых, список свободных элементов на основе индексов вместо стека указателей позволяет сохранить проверку принадлежности пулу, при этом тип внутреннего массива остается независимым от параметра шаблона. Во-вторых, хранение индекса слота внутри каждого объекта с помощью SetPoolIndex() устраняет необходимость в переборе для проверки принадлежности при возврате объекта в пул, благодаря чему и Acquire(), и Release() выполняются за постоянное время независимо от вместимости пула.
Контракт пула обеспечивает четкое разделение между полезными данными, отражающими состояние прикладной логики, и метаданными жизненного цикла. Метод Reset() очищает только поля полезных данных, оставляя состояние принадлежности пулу исключительно под контролем самого пула. Флаг занятости каждого объекта защищает от повреждения данных при повторном возврате, а архитектура с фиксированной вместимостью без резервного выделения памяти в куче гарантирует, что на горячем пути память не выделяется даже при пиковой нагрузке.
Компонент теста производительности сохраняет диагностическую ценность и при эксплуатации. Измерение времени для двух вариантов работы позволяет количественно оценить, оправдывают ли конкретная частота тиков и характер использования объектов фиксированный объем занимаемой памяти пула, равный вместимости пула, умноженной на размер типа объектов в пуле. Стабильное соотношение накладных расходов на протяжении всей сессии указывает на то, что аллокатор эффективно справляется с рабочей нагрузкой. Рост этого соотношения или заметный джиттер времени выполнения на линии варианта без пула указывает на то, что гарантированная пулом стабильность оправдывает сопутствующие компромиссы.
Прежде чем внедрять пул, подумайте, не будет ли достаточно одного постоянно существующего объекта, состояние которого сбрасывается на каждом тике. Для многих индикаторов этого будет достаточно. Пул оправдывает себя, когда потребность в одновременно существующих объектах ограничена и предсказуема, а их создание и освобождение происходят достаточно часто, чтобы измеренные затраты на выделение памяти заметно влияли на время выполнения.
Программы, использованные в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | SignalEvent.mqh | Включаемый файл | Класс полезных данных CSignalEvent с поддержкой работы в пуле: содержит направление, цену, силу сигнала, временную метку и поля контракта пула (m_is_pooled, m_pool_index, m_in_use); метаданные строго отделены от полезных данных. |
| 2 | ObjectPool.mqh | Включаемый файл | Обобщенный шаблон CObjectPool<T>: операции Acquire и Release со сложностью O(1) благодаря сохраненному индексу слота, защита от повторного возврата, фиксированная вместимость без резервного выделения памяти в куче при исчерпании пула. |
| 3 | PoolBenchmark.mq5 | Пользовательский индикатор | Тест производительности, сравнивающий два варианта работы с объектами CSignalEvent — с выделением памяти в куче и с получением объектов из пула — путем измерения времени выполнения на каждом тике с помощью GetMicrosecondCount(). Использует DRAW_SECTION для корректного отображения разрывов, EMPTY_VALUE в качестве маркера отсутствия данных на графике и индексацию баров rates_total-1 без ArraySetAsSeries. |
| 4 | Generic_Object_Pool_in_MQL5.zip | Архив в формате ZIP | ZIP-архив, содержащий все приложенные файлы с сохранением их путей относительно корневой папки терминала. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22947
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.