English 中文 Español Deutsch 日本語 Português
preview
Разработка экспериментальной DLL с поддержкой многопоточности в C++ для MetaTrader 5 на Linux

Разработка экспериментальной DLL с поддержкой многопоточности в C++ для MetaTrader 5 на Linux

MetaTrader 5Примеры | 13 марта 2023, 08:58
1 665 5
Wasin Thonkaew
Wasin Thonkaew

Введение

Linux располагает динамичной экосистемой и хорошей эргономикой для разработки программного обеспечения.

Она хорошо подходит тем, кто любит работать с командной строкой и стремится к простоте установки приложений через диспетчер пакетов. Операционная система не является черным ящиком, но ее интересно осваивать. Она настраивается почти для всех подсистем, располагает встроенными инструментами, а также гибкой и оптимизированной средой разработки ПО.

ОС доступна как на настольном ПК, так и в виде облачного решения, в частности на виртуальном выделенном сервере (VPS) или у поставщиков облачных услуг, таких как AWS и Google Cloud.

Думаю, многие разработчики придерживаются выбранной ими операционной системы, но при этом хотят разрабатывать продукты и для пользователей Windows. Конечно, продукты должны работать одинаково хорошо на разных платформах.

Обычно разработчики создают свои индикаторы, роботов и сопутствующие продукты на MQL5, а затем публикуют их на Маркете, не заботясь об операционной системе. В вопросе компиляции и сборки исполняемого файла .EX5 они могут просто положиться на интерактивную среду разработки MetaTrader 5 (при условии, что они знают, как запустить MetaTrader 5 в Linux).
Но когда разработчикам необходимо создать решение в виде разделяемой библиотеки (DLL) для дальнейшего расширения и создания дополнительных опций, им приходится тратить больше времени и усилий на поиск решений для кросс-компиляции, обнаружение подводных камней, изучение наиболее оптимальных методов, знакомство с инструментами и т. д.

Эта причина и побудила меня написать эту статью. Кросс-компиляция и возможность создания DLL с поддержкой многопоточности в C++ служат отправными точками для дальнейшего изучения темы.
Я надеюсь, что статья поможет вам продолжить разработку продуктов, связанных с MetaTrader 5, в Linux.

Для кого эта статья

Я предполагаю, что у читателей уже есть некоторый опыт взаимодействия с Linux через командную строку, а также общее представление о компиляции и построении исходного кода C++ в Linux.

Статья предназначена для тех, кто хочет изучить процесс разработки DLL, поддерживающей многопоточность в Linux, но также работающей и в Windows. Расширьте свои возможности многопоточного программирования, используя не только встроенный OpenCL, но и базовый гибкий мобильный код C++ с возможностью многопоточности для интеграции с некоторыми другими системами, которые тесно с ним связаны. 

ОС и программное обеспечение

  • Ubuntu 20.04.3 LTS с версией ядра 5.16.0 на 6-ядерном процессоре AMD Ryzen 5 3600 (2 потока на ядро), 32 ГБ ОЗУ
  • Wine (пакет winehq-devel) 8.0-rc3 (на момент написания этой статьи). Также смотрите тему в блоге MT5 build 3550 сразу же вылетает при запуске с пакетом winehq-stable (на английском), чтобы узнать, почему я решил использовать devel, а не стабильный пакет
  • Mingw (пакет mingw-w64) 7.0.0-2
  • VirtualBox 6.1 для тестирования в Windows

План статьи

Будем придерживаться следующего плана:

  1. Знакомство с Wine
  2. Знакомство с Mingw
  3. Потоки Mingw
    1. POSIX (pthread)
    2. Win32 (via mingw-std-threads)
  4. Подготовка среды разработки на Linux
    1. Установка Wine
    2. Установка MetaTrader 5
    3. Установка Mingw
    4. (Опционально) Установка mingw-std-threads
  5. Эксперимент, этап разработки I - DLL (поддержка многопоточности в C++)
  6. Эксперимент, этап разработки II - MQL5-код для использования DLL
  7. Тестирование в Windows
  8. Простой тест реализации потоков Mingw


Wine

Название Wine является рекурсивным бэкронимом и расшифровывается как Wine is Not an Emulator (Wine - не эмулятор). Действительно, это не эмулятор какого-либо процесса или оборудования. Wine - это оболочка win32 API, которая работает в ОС, отличных от Windows.

Wine представляет еще один абстрактный уровень, который перехватывает вызов win32 API пользователей в системе, отличной от Windows, перенаправляет его во внутренние компоненты Wine, а затем обрабатывает запрос так же (или почти так же), как это делается в Windows. 

Wine работает с win32 API с помощью POSIX API. Пользователи могут работать в Wine, не подозревая, что они запускают Windows-приложение в Linux, и даже играть в игры из своей Steam-библиотеки в Linux, поскольку его среда выполнения основана на варианте Wine под названием Proton.

Это обеспечивает гибкость при тестировании или использовании Windows-приложений, для которых нет аналогов в Linux.

Обычно при запуске Windows-приложения через Wine мы выполняем следующую команду:

wine windows_app.exe

Если мы хотим запустить приложение, связанное с определенным префиксом среды Wine, мы вводим:

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe


Mingw

Mingw расшифровывается как "Minimalist GNU for Windows" (минималистский GNU для Windows). Это порт коллекции компиляторов GNU (GCC) и ее инструментов, используемых при компиляции C/C++ и некоторых других языков программирования, предназначенных для Windows, в Linux.

Функции, флаги/параметры компиляции постоянно доступны как в GCC, так и в Mingw, поэтому пользователи, знающие GCC, легко освоятся и с Mingw. Кроме того, по флагам/параметрам компиляции GCC очень похож на Clang. Таким образом пользователи могут легко использовать свои наработки в новой среде, при этом имея возможность расширить базу пользователей, включив в нее работающих в ОС Windows.

Ниже приведена сравнительная таблица, чтобы вы могли увидеть различия.

  • Компиляция исходного кода на C++ и создание разделяемой библиотеки
Компилятор Командная строка
GCC
g++ -shared -std=c++17 -fPIC -o libexample.so example.cpp -lpthread
Mingw
x86_64-w64-mingw32-g++-posix -shared -std=c++17 -fPIC -o example.dll example.cpp -lpthread

  • Компиляция исходного кода на C++ и создание исполняемого двоичного файла
Компилятор Командная строка
GCC
g++ -std=c++17 -I. -o main.out main.cpp -L. -lexample
Mingw 
x86_64-w64-mingw32-g++-posix -std=c++17 -I. -o main.exe main.cpp -L. -lexample

Как видите, различия минимальны. Флаги компиляции очень похожи, почти одинаковы. Единственное различие - в двоичном файле компилятора для сборки всего необходимого.

Имеются три варианта использования. Подробнее об этом - в разделе о реализации многопоточности.

  1. x86_64-w64-mingw32-g++
    Наименование x86_64-w64-mingw32-g++-win32.

  2. x86_64-w64-mingw32-g++-posix
    Двоичный исполняемый файл, предназначенный для работы с pthread.

  3. x86_64-w64-mingw32-g++-win32
    Двоичный исполняемый файл, предназначенный для работы с потоковой моделью win32 API. Наименование 86_64-w64-mingw32-g++.

Кроме того, есть несколько других инструментов с префиксом 

x86_64-w64-mingw32-...

Некоторые примеры:

  • x86_64-w64-mingw32-gcc-nm - изменение имени
  • x86_64-w64-mingw32-gcc-ar - управление архивом
  • x86_64-w64-mingw32-gcc-gprof - анализ производительности Unix-подобных операционных систем
Также есть x86_64-w64-mingw32-gcc-nm-posix и x86_64-w64-mingw32-gcc-nm-win32 .

Реализация потоков Mingw

Из предыдущего раздела мы знаем, что существуют два варианта реализации потоков, предоставляемых Mingw.
  1. POSIX (pthread)
  2. Win32

Почему нас это должно интересовать? Я могу назвать две причины:

  1. Безопасность и совместимость
    Если ваш код потенциально использует возможности многопоточности C++ (например, std::thread, std::promise и т.д.), а также встроенную поддержку многопоточности ОС, например CreateThread() для win32 API и pthread_create() для POSIX API, лучше придерживаться использования какого-то одного API.

    В любом случае, маловероятно, что мы будем смешивать код, используя многопоточность C++ и поддержку ОС, за исключением очень специфических ситуаций, когда API поддержки ОС предлагает больше функций, чем C++. Поэтому лучше проявлять последовательность и использовать одну потоковую модель.
    При использовании реализации pthread, постарайтесь не применять возможности многопоточности win32 API, и наоборот.

  2. Производительность (подробнее - в разделе "Простой тест реализации потоков Mingw")
    Конечно, пользователям нужно многопоточное решение с малой задержкой. Как правило, пользователи выбирают приложения с более быстрым исполнением.

Сначала мы разработаем нашу пробную DLL и тестовую программу, а затем протестируем обе реализации потока.

Для проекта мы используем мобильный код для использования потока pthread или win32. Наша система сборки может легко переключаться с одной системы на другую.
При использовании потока win32 необходимо установить заголовки из проекта mingw-std-threads.


Подготовка среды разработки на Linux

Прежде чем перейти непосредственно к коду, нам нужно установить необходимое программное обеспечение.

Установка Wine

Выполните следующую команду, чтобы установить devel-пакет Wine.

sudo apt install winehq-devel

затем проверьте, правильно ли он работает, с помощью следующей команды:

wine --version

Ответ должен быть примерно таким

wine-8.0-rc3


Установка MetaTrader 5

Большинство пользователей установили MetaTrader 5 задолго до билда 3550, вызывающего сбой. Мы не можем применить официальный скрипт установки, чтобы использовать пакет winehq-devel и запустить MetaTrader 5, как описано в разделе "Установка в Linux".
Команды лучше выполнять самостоятельно, потому что непосредственное выполнение официального установочного скрипта перезапишет наш Wine обратно в стабильный пакет.

Я написал руководство MT5 Build 3550 Broken Launching On Linux Through Wine. How To Solve? (на английском) в своем блоге. Статья предназначена для пользователей, которые уже установили стабильный пакет Wine, а также длятех, кто хочет начать установку заново с помощью devel-пакета.

После того, как всё сделано, попробуйте еще раз запустить MetaTrader 5 через Wine и убедитесь, что всё в порядке.

Примечание

Официальный скрипт установки создаст среду Wine (называемую префиксом) по адресу ~/.mt5. Рекомендую добавить следующую строку в ~/.bash_aliases для легкого запуска MetaTrader 5.

alias mt5trader="WINEPREFIX=~/.mt5 wine '/home/haxpor/.mt5/drive_c/Program Files/MetaTrader 5/terminal64.exe'"

Затем используйте ее с

source ~/.bash_aliases

Наконец, выполните следующую команду, чтобы запустить MetaTrader 5, в котором его отладка будет показана в терминале.

mt5trader

Такой запуск MetaTrader 5 впоследствии позволит нам увидеть журнал отладки из нашего экспериментального приложения без необходимости усложнять код.

Установка Mingw

Выполните следующую команду, чтобы установить Mingw.

sudo apt install mingw-w64

В результате в вашу систему будет установлен набор инструментов с префиксом x86_64-w64-mingw32-. В основном мы будем работать либо с x86_64-w64-mingw32-g++-posix, либо с x86_64-w64-mingw32-win32 в случае использования потока win32.

Установка mingw-std-threads

mingw-std-threads - это проект, объединяющий поток win32 для работы в Linux. Решение используется для работы с заголовками. Установка довольно проста и требует лишь поместить заголовочный файл в путь include.

Для установки выполните следующие действия.

Во-первых, клонируйте репозиторий git в свою систему.

git clone git@github.com:Kitware/CMake.git

Затем создайте каталог для хранения его заголовка в пути include.

sudo mkdir /usr/x86_64-w64-mingw32/include/mingw-std-threads

Наконец скопируйте все заголовочные файлы (.h) из каталога клонированного проекта во вновь созданный каталог.

cp -av *.h /usr/x86_64-w64-mingw32/include/mingw-std-threads/

На этом всё. Затем в коде, если мы решим использовать поток win32, для некоторых файлов заголовков, связанных с возможностью многопоточности (например, потоки, примитивы синхронизации и т. д.), нам нужно будет включить его из соответствующего пути с заменой имени. Полный список приведен в таблице ниже.

Включение многопоточного заголовочного файла C++11 Включение файла заголовка mingw-std-threads
#include <mutex>
#include <mingw-std-threads/mingw.mutex.h>
#include <thread>
#include <mingw-std-threads/mingw.thread.h>
#include <shared_mutex>
#include <mingw-std-threads/mingw.shared_mutex.h>
#include <future>
#include <mingw-std-threads/mingw.future.h>

#include <condition_variable>
#include <mingw-std-threads/mingw.condition_variable.h>


Эксперимент, этап разработки I - DLL (поддержка многопоточности в C++)

Теперь пришло время перейти к коду.

Наша цель — реализовать экспериментальное DLL-решение, способное использовать возможности многопоточности из стандартной библиотеки C++11, чтобы понять идею и продолжить исследование.

Ниже представлены наша библиотека и структура приложения.

Структура проекта

  • DLL
    • example.cpp
    • example.h
  • Пользователь
    • main.cpp
  • Сборка
    • Makefile - файл сборки кросс-компиляции с использованием pthread
    • Makefile-th_win32 - файл сборки кросс-компиляции с использованием потока win32 
    • Makefile-g++ - файл сборки для тестирования на нативном Linux. Предназначен для быстрой итерации и отладки при разработке проекта.

Применяемый стандарт C++

Хотя в основном мы будем использовать стандарт C++11, однако иногда нам нужно будет применять и элементы стандарта C++17, например атрибут аннотации кода [[nodiscard]].

DLL

example.h

#pragma once

#ifdef WINDOWS
        #ifdef EXAMPLE_EXPORT
                #define EXAMPLE_API __declspec(dllexport)
        #else
                #define EXAMPLE_API __declspec(dllimport)
        #endif
#else
        #define EXAMPLE_API
#endif

// we have to use 'extern "C"' in order to export functions from DLL to be used
// in MQL5 code.
// Using 'namespace' or without such extern won't make it work for MQL5 code, it
// won't be able to find such functions.
extern "C" {
	/**
	 * Add two specified number together.
	 */
        EXAMPLE_API [[nodiscard]] int add(int a, int b) noexcept;

	/**
	 * Subtract two specified number.
	 */
        EXAMPLE_API [[nodiscard]] int sub(int a, int b) noexcept;

	/**
	 * Get the total number of hardware's concurrency.
	 */
	EXAMPLE_API [[nodiscard]] int num_hardware_concurrency() noexcept;

	/**
	 * Sum all elements from specified array for number of specified elements.
	 * The computation will be done in a single thread linearly manner.
	 */
	EXAMPLE_API [[nodiscard]] int single_threaded_sum(const int arr[], int num_elem);

	/**
	 * Sum all elements from specified array for number of specified elements.
	 * The computation will be done in a multi-thread.
	 *
	 * This version is suitable for processor that bases on MESI cache coherence
	 * protocol. It won't make a copy of input array of data, but instead share
	 * it among all threads for reading purpose. It still attempt to write both
	 * temporary and final result with minimal number of times thus minimally
	 * affect the performance.
	 */
	EXAMPLE_API [[nodiscard]] int multi_threaded_sum_v2(const int arr[], int num_elem);
};

Хотя #pragma once не является частью стандарта C++, но поддерживается GCC, а значит, и Mingw. Это гибкий и более короткий способ предотвратить дублирование включения заголовка.
Без этой директивы пользователи применяли бы #ifdef и #define и им необходимо было бы убедиться, что каждое определение имеет уникальное имя для каждого файла заголовка. Это занимало бы много времени.

У нас есть #ifdef WINDOWS, чтобы сохранить объявление определения EXAMPLE_API. Это позволяет нам выполнять компиляцию с помощью Mingw и нативной Linux. Таким образом, всякий раз, когда мы хотим выполнить кросс-компиляцию для разделяемой библиотеки, мы добавляем -DWINDOWS и -DEXAMPLE_EXPORT во флаг компиляции. Если же мы компилируем только для тестирования основной программы, мы можем опустить -DEXAMPLE_EXPORT.

__declspec(dllexport) - директива для экспорта функции из DLL

__declspec(dllimport) - директива для импорта функции из DLL.

Указанные директивы необходимы для компиляции, чтобы работать с DLL в Windows. Они не нужны для систем, отличных от Windows, но по-прежнему нужны для кросс-компиляции. Таким образом, значение остается пустым для EXAMPLE_API при отсутствии определения WINDOWS для компиляции под Linux.

Сигнатуры функций должны быть совместимы с соглашением о вызовах языка C.
Такой поверхностный "C" предотвратит искажение сигнатур функций, внедряемых в соглашение о вызовах в C++.

Мы не можем обернуть сигнатуры функций внутри namespace или объявить их свободными функциями, потому что MQL5-код не сможет найти эти сигнатуры, когда мы позже используем DLL.

Для num_hardware_concurrency() он вернет количество одновременных потоков, поддерживаемых реализацией.
Например, я использую 6-ядерный процессор с 2 потоками на ядро, поэтому у него фактически 12 потоков, которые могут работать одновременно. В моем случае возвращается 12.

single_threaded_sum() и multi_threaded_sum_v2() наглядно демонстрируют преимущества многопоточности и позволяют сравнить производительность.

example.cpp

#include "example.h"

#ifdef USE_MINGW_STD_THREAD
        #include <mingw-std-threads/mingw.thread.h>
#else
        #include <thread>
#endif

#include <iostream>
#include <vector>
#include <chrono>
#include <numeric>
#include <atomic>

#ifdef ENABLE_DEBUG
#include <cstdarg>
#endif

#ifdef ENABLE_DEBUG
const int LOG_BUFFER_SIZE = 2048;
char log_buffer[LOG_BUFFER_SIZE];

inline void DLOG(const char* ctx, const char* format, ...) {
        va_list args;
        va_start(args, format);
        std::vsnprintf(log_buffer, LOG_BUFFER_SIZE-1, format, args);
        va_end(args);

        std::cout << "[DEBUG] [" << ctx << "] " << log_buffer << std::endl;
}
#else
        #define DLOG(...)
#endif

EXAMPLE_API int add(int a, int b) noexcept {
        return a + b;
}

EXAMPLE_API int sub(int a, int b) noexcept {
        return a - b;
}

EXAMPLE_API int num_hardware_concurrency() noexcept {
        return std::thread::hardware_concurrency();
}

EXAMPLE_API int single_threaded_sum(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        int local_sum = 0;
        for (int i=0; i<num_elem; ++i) {
                local_sum += arr[i];
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;
        return local_sum;
}

EXAMPLE_API int multi_threaded_sum_v2(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        std::vector<std::pair<int, int>> arr_indexes;

        const int num_max_threads = std::thread::hardware_concurrency() == 0 ? 2 : std::thread::hardware_concurrency();
        const int chunk_work_size = num_elem / num_max_threads;

        std::atomic<int> shared_total_sum(0);

        // a lambda function that accepts input vector by reference
        auto worker_func = [&shared_total_sum](const int* arr, std::pair<int, int> indexes) {
                int local_sum = 0;
                for (int i=indexes.first; i<indexes.second; ++i) {
                        local_sum += arr[i];
                }
                shared_total_sum += local_sum;
        };

        DLOG("multi_threaded_sum_v2", "chunk_work_size=%d", chunk_work_size);
        DLOG("multi_threaded_sum_v2", "num_max_threads=%d", num_max_threads);

        std::vector<std::thread> threads;
        threads.reserve(num_max_threads);

        for (int i=0; i<num_max_threads; ++i) {
                int start = i * chunk_work_size;
                // also check if there's remaining to piggyback works into the last chunk
                int end = (i == num_max_threads-1) && (start + chunk_work_size < num_elem-1) ? num_elem : start+chunk_work_size;
                threads.emplace_back(worker_func, arr, std::make_pair(start, end));
        }

        DLOG("multi_threaded_sum_v2", "thread_size=%d", threads.size());

        for (auto& th : threads) {
                th.join();
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;

        return shared_total_sum;
}

Выше приведен полный код. Разберем каждую его часть по отдельности для простоты понимания.

Есть возможность переключения между pthread и win32.

#include "example.h"

#ifdef USE_MINGW_STD_THREAD
        #include <mingw-std-threads/mingw.thread.h>
#else
        #include <thread>
#endif

Эта настройка позволяет хорошо интегрироваться в нашу систему сборки, чтобы переключаться между pthread и win32. Добавление -DUSE_MINGW_STD_THREAD во флаг компиляции позволяет использовать поток win32 во время кросс-компиляции.

Внедряем простые интерфейсы.

EXAMPLE_API int add(int a, int b) noexcept {
        return a + b;
}

EXAMPLE_API int sub(int a, int b) noexcept {
        return a - b;
}

EXAMPLE_API int num_hardware_concurrency() noexcept {
        return std::thread::hardware_concurrency();
}

add() и sub() просты для понимания. Для num_hardware_concurrency() нам нужно включить заголовок <thread>, чтобы использовать std::thread::hardware_concurrency().

Функция утилиты журнала отладки.

#ifdef ENABLE_DEBUG
#include <cstdarg>
#endif

#ifdef ENABLE_DEBUG
const int LOG_BUFFER_SIZE = 2048;
char log_buffer[LOG_BUFFER_SIZE];

inline void DLOG(const char* ctx, const char* format, ...) {
        va_list args;
        va_start(args, format);
        std::vsnprintf(log_buffer, LOG_BUFFER_SIZE-1, format, args);
        va_end(args);

        std::cout << "[DEBUG] [" << ctx << "] " << log_buffer << std::endl;
}
#else
        #define DLOG(...)
#endif

Добавляя -DENABLE_DEBUG во флаге компиляции, мы разрешаем вывод журнала отладки на консоль. Вот почему я предлагаю запустить MetaTrader 5 через командную строку, чтобы мы могли соответствующим образом отладить нашу программу.
Каждый раз, когда мы не даем такого определения, DLOG() ничего не значит и не влияет на наш код ни с точки зрения скорости выполнения, ни с точки зрения двоичного размера разделяемой библиотеки или исполняемого двоичного файла. Это очень хорошо.

Разработка DLOG() была вдохновлена ОС Android. Обычно имеется контекстная строка (независимо от компонента, которому принадлежит запись журнала). В нашем случае это ctx, за которой следует строка журнала отладки.

Реализация однопоточной функции суммирования.

EXAMPLE_API int single_threaded_sum(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        int local_sum = 0;
        for (int i=0; i<num_elem; ++i) {
                local_sum += arr[i];
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;
        return local_sum;
}

Здесь имитируется реальное использование при работе с MQL5-кодом. Представьте себе ситуацию, когда MQL5 отправляет какие-то данные в виде массива в функцию DLL для вычисления чего-то до того, как DLL вернет результат в код MQL5.
Но для этой функции она линейно перебирает все элементы из указанного входного массива один за другим до достижения общего количества элементов, как указано num_elem.

Код также оценивает общее время выполнения с помощью библиотеки std::chrono для вычисления истекшего времени. Обратите внимание, что мы используем std::chrono::steady_clock. Это монотонические часы, движущиеся вперед независимо от настройки системных часов. Они полностью подходят для измерения интервала времени.

Реализация многопоточной функции суммирования.

EXAMPLE_API int multi_threaded_sum_v2(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        std::vector<std::pair<int, int>> arr_indexes;

        const int num_max_threads = std::thread::hardware_concurrency() == 0 ? 2 : std::thread::hardware_concurrency();
        const int chunk_work_size = num_elem / num_max_threads;

        std::atomic<int> shared_total_sum(0);

        // a lambda function that accepts input vector by reference
        auto worker_func = [&shared_total_sum](const int arr[], std::pair<int, int> indexes) {
                int local_sum = 0;
                for (int i=indexes.first; i<indexes.second; ++i) {
                        local_sum += arr[i];
                }
                shared_total_sum += local_sum;
        };

        DLOG("multi_threaded_sum_v2", "chunk_work_size=%d", chunk_work_size);
        DLOG("multi_threaded_sum_v2", "num_max_threads=%d", num_max_threads);

        std::vector<std::thread> threads;
        threads.reserve(num_max_threads);

        for (int i=0; i<num_max_threads; ++i) {
                int start = i * chunk_work_size;
                // also check if there's remaining to piggyback works into the last chunk
                int end = (i == num_max_threads-1) && (start + chunk_work_size < num_elem-1) ? num_elem : start+chunk_work_size;
                threads.emplace_back(worker_func, arr, std::make_pair(start, end));
        }

        DLOG("multi_threaded_sum_v2", "thread_size=%d", threads.size());

        for (auto& th : threads) {
                th.join();
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;

        return shared_total_sum;
}

Обратите внимание, что он помечен как v2. Я оставлю его для истории. Если коротко, для современного процессора, использующего протокол согласованности кэша MESI, нет необходимости делать копию набора данных, подаваемого в каждый поток, потому что MESI пометит такую линию кэша как совместно используемую несколькими потоками и не будет тратить вычислительные ресурсы процессора на сигнализацию и ожидание ответа.
В моей предыдущей реализации v1 приложены все усилия, чтобы сделать копию набора данных, подаваемого в каждый поток. Но по упомянутой причине включать такую попытку в исходный код не нужно. v1 медленнее v2 примерно в 2-5 раз.

Уведомление worker_func представляет собой лямбда-функцию, которая работает с исходным массивом данных и диапазоном данных, с которыми нужно работать (пара начальных и конечных индексов). Оно суммирует все элементы внутри цикла в локальную переменную, чтобы избежать ложного разделения, которое может значительно снизить производительность, а затем складывает в общую переменную суммирования для всех потоков. Используется std::atomic, чтобы сделать его потокобезопасным. Количество раз, когда такая общая переменная суммирования должна быть изменена, достаточно невелико и не оказывает существенного влияния на производительность. Достигается баланс между практической реализацией и приростом скорости.

Мы вычисляем, сколько потоков потребуется для разделения работы, таким образом узнавая диапазон работы для каждого потока. std::hardware_concurrency() может вернуть 0. Это означает, что он не сможет определить количество потоков, поэтому мы обрабатываем и такой случай и возвращаемся к 2.

Далее мы создаем вектор потоков. Ограничиваем его производительность значением num_max_threads. Затем итеративно вычисляем диапазон набора данных для каждого потока, над которым нужно работать. Обратите внимание, что для последнего потока потребуются все оставшиеся данные, поскольку количество элементов работы, которую необходимо выполнить, может не делиться на количество используемых вычисляемых потоков.

Самое важное, что мы присоединяемся ко всем потокам. Для более сложных случаев нам может понадобиться асинхронная среда, которая не блокирует код MQL5 от ожидания результата. При этом мы обычно используем std::future, являющейся базой для std::async, std::promise, и std::packaged_task. Таким образом, у нас обычно есть как минимум два интерфейса: один - для запроса отправки данных из кода MQL5 для вычисления с помощью DLL без блокировки, а другой - для получения результата такого запроса обратно по запросу, после чего он блокирует вызов MQL5-кода. Возможно, я напишу об этом в следующей статье.

Кроме того, попутно мы можем использовать DLOG() для печати некоторых состояний отладки. Это полезно для отладки.

Теперь реализуем мобильную основную программу тестирования, которая будет работать в родном Linux и кросс-компилируемой среде с помощью Wine.

main.cpp

#include "example.h"
#include <iostream>
#include <cassert>
#include <vector>
#include <memory>

int main() {
        int res = 0;

        std::cout << "--- misc ---\n";
        res = add(1,2);
        std::cout << "add(1,2): " << res << std::endl;
        assert(res == 3);

	res = 0;

        res = sub(2,1);
        std::cout << "sub(2,1): " << res << std::endl;
        assert(res == 1);

	res = 0;

        std::cout << "hardware concurrency: " << num_hardware_concurrency() << std::endl;
        std::cout << "--- end ---\n" << std::endl;

        std::vector<int> arr(1000000000, 1);

        std::cout << "--- single-threaded sum(1000M) ---\n";
        res = single_threaded_sum(arr.data(), arr.size());
        std::cout << "sum: " << res << std::endl;
        assert(res == 1000000000);
        std::cout << "--- end ---\n" << std::endl;
        
        res = 0;

        std::cout << "--- multi-threaded sum_v2(1000M) ---\n";
        res = multi_threaded_sum_v2(arr.data(), arr.size());
        std::cout << "sum: " << res << std::endl;
        assert(res == 1000000000);
        std::cout << "--- end ---" << std::endl;

        return 0;
}

Мы включаем заголовочный файл example.h, как правило, чтобы иметь возможность вызывать готовые интерфейсы. Мы также подтверждаем правильность результатов с помощью assert().

Создадим обе общие библиотеки (как libexample.so для нативного Linux) и основную программу тестирования, а именно main.out. Сначала сделаем это не с помощью системы сборки, а через выполнение командной строки. Система сборки через Makefile будет реализована позже.
Сначала протестируем его локально на Linux, прежде чем выполнять кросс-компиляцию.

Выполним следующую команду, чтобы создать общую библиотеку для вывода в виде libexample.so.

$ g++ -shared -std=c++17 -Wall -Wextra -fno-rtti -O2 -I. -fPIC -o libexample.so example.cpp -lpthread

Описание каждого флага представлено ниже

Флаг Описание
-shared Поручить компилятору построить разделяемую библиотеку
-std=c++17 Поручить компилятору использовать синтаксис C++ стандарта C++17.
-Wall Поручить выводить все предупреждения при компиляции
-Wextra Выводить в том числе и дополнительные предупреждения при компиляции
-fno-rtti Это часть оптимизации. Отключить RTTI (информация о типе времени выполнения).
RTTI позволяет определить тип объекта во время выполнения программы. Нам это не нужно. К тому же, это снижает производительность.
-O2 Активировать уровень оптимизации 2, который включает более агрессивные оптимизации поверх уровня 1.
-I. Установить путь включения в текущий каталог, чтобы компилятор смог найти наш заголовочный файл example.h, который находится в том же каталоге.
-fPIC Обычно требуется при создании общей библиотеки, поскольку указывает компилятору генерировать позиционно-независимый код (PIC), подходящий для создания общей библиотеки,
и работать с основной программой, с которой нужно линковаться. Отсутствие фиксированного адреса памяти для загрузки определенной функции из общей библиотеки также повышает безопасность.
-lpthread  Поручить связать с библиотекой pthread

Выполните следующую команду, чтобы создать основную программу тестирования, связанную с libexample.so, и вывести в виде main.out.

$ g++ -std=c++17 -Wall -Wextra -fno-rtti -O2 -I. -o main.out main.cpp -L. -lexample

Описание каждого флага, отличного от того, что мы упомянули выше, выглядит следующим образом.

Флаг Описание
-L. Установить путь включения для общей библиотеки в тот же каталог.
 -lexample Связать с разделяемой библиотекой libexample.so.

Наконец, запускаем исполняемый файл.

$ ./main.out 
--- misc ---
add(1,2): 3
sub(2,1): 1
hardware concurrency: 12
--- end ---

--- single-threaded sum(1000M) ---
elapsed time: 568.401ms
sum: 1000000000
--- end ---

--- multi-threaded sum_v2(1000M) ---
elapsed time: 131.697ms
sum: 1000000000
--- end ---

Как видим, многопоточная функция работает значительно быстрее однопоточной (примерно в 4.33 раза быстрее).

Мы знакомы с тем, как компилировать и собирать как разделяемую библиотеку, так и основную программу с помощью командной строки. Теперь создадим необходимую систему сборки с помощью Makefile.
Для этих целей можно использовать CMake, но поскольку мы в основном разрабатываем под Linux, CMake мне кажется излишним. Нам не нужна такая совместимость, чтобы разрабатывать под Windows. Так что выбираем Makefile.

У нас будет три варианта Makefile.

  1. Makefile
    Предназначен для кросс-компиляции как под Linux, так и под Windows. Применяет pthread. Используем для сборки DLL, работающей с MetaTrader 5 в дополнение к основной программе тестирования, которую можно запустить через Wine.

  2. Makefile-th_win32
    То же, что и Makefile, но использует поток win32.

  3. Makefile-g++
    Предназначен для компиляции в нативной системе Linux. Это шаги, которые мы только что сделали выше.

Makefile

# script to build project with mingw with posix thread
.PHONY: all clean example.dll main.exe

COMPILER := x86_64-w64-mingw32-g++-posix
FLAGS := -O2 -fno-rtti -std=c++17 -Wall -Wextra
MORE_FLAGS ?=

all: example.dll main.exe

example.dll: example.cpp example.h
        $(COMPILER) -shared $(FLAGS) $(MORE_FLAGS) -DEXAMPLE_EXPORT -DWINDOWS -I. -fPIC -o $@ $< -lpthread

main.exe: main.cpp example.dll
        $(COMPILER) $(FLAGS) $(MORE_FLAGS) -I. -DWINDOWS -o $@ $< -L. -lexample

clean:
        rm -f example.dll main.exe

Makefile-th_win32

# script to build project with mingw with win32 thread
.PHONY: all clean example.dll main.exe

COMPILER := x86_64-w64-mingw32-g++-win32
FLAGS := -O2 -fno-rtti -std=c++17 -Wall -Wextra
MORE_FLAGS ?=

all: example.dll main.exe

example.dll: example.cpp example.h
        $(COMPILER) -shared $(FLAGS) $(MORE_FLAGS) -DEXAMPLE_EXPORT -DWINDOWS -DUSE_MINGW_STD_THREAD -I. -fPIC -o $@ $<

main.exe: main.cpp example.dll
        $(COMPILER) $(FLAGS) $(MORE_FLAGS) -I. -DWINDOWS -DUSE_MINGW_STD_THREAD -o $@ $< -L. -lexample

clean:
        rm -f example.dll main.exe

Makefile-g++

# script to build project with mingw with posix thread, for native linux
.PHONY: all clean example.dll main.exe

COMPILER := g++
FLAGS := -O2 -fno-rtti -std=c++17 -Wall -Wextra
MORE_FLAGS ?=

all: libexample.so main.out

libexample.so: example.cpp example.h
        $(COMPILER) -shared $(FLAGS) $(MORE_FLAGS) -I. -fPIC -o $@ $< -lpthread

main.out: main.cpp libexample.so
        $(COMPILER) $(FLAGS) $(MORE_FLAGS) -I. -o $@ $< -L. -lexample

clean:
        rm -f libexample.so main.out

Код всех трех приведенных вариантов Makefiles почти одинаков с некоторыми минимальными отличиями.

Отличия следующие:

  • имя двоичного файла компилятора
  • -DUSE_MINGW_STD_THREAD
  • наличие/отсутствие -lpthread
  • имя двоичного файла на выходе, например libexample.so или example.dll, а также main.out или main.exe в зависимости от целевой системы для сборки


MORE_FLAGS объявляется в виде

MORE_FLAGS ?=

что означает, что он позволяет пользователям передавать дополнительные флаги компиляции из командной строки, поэтому пользователь по требованию может добавлять дополнительные флаги по мере необходимости. Если флаги не передаются извне от пользователей, используется то, что уже определено в коде Makefile.

Сделаем все файлы Makefile исполняемыми

$ chmod 755 Makefile*

Информация для сборки варианта Makefile представлена в таблице ниже.

Целевая система Команда сборки  Команда очистки
Кросс-компиляция с использованием pthread make  make clean
Кросс-компиляция с использованием потока win32 make -f Makefile-th_win32  make -f Makefile-th_win32 clean
Нативный Linux make -f Makefile-g++  make -f Makefile-g++ clean

Создадим DLL для использования с MetaTrader 5 и Wine. Таким образом, мы сможем протестировать оба.

Выполним

$ make

У нас будут сгенерированы следующие файлы

  1. example.dll
  2. main.exe


Тестовое выполнение кросс-компилируемого исполняемого файла.

$ wine main.exe
...
0118:err:module:import_dll Library libgcc_s_seh-1.dll (which is needed by L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\main.exe") not found
0118:err:module:import_dll Library libstdc++-6.dll (which is needed by L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\main.exe") not found
0118:err:module:import_dll Library libgcc_s_seh-1.dll (which is needed by L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\example.dll") not found
0118:err:module:import_dll Library libstdc++-6.dll (which is needed by L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\example.dll") not found
0118:err:module:import_dll Library example.dll (which is needed by L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\main.exe") not found
0118:err:module:LdrInitializeThunk Importing dlls for L"Z:\\mnt\\datadrive\\_extended\\home\\haxpor\\Data\\Projects\\ExampleLib\\t\\main.exe" failed, status c0000135

Итак, у нас возникла проблема. main.exe не может найти необходимые DLL.
Решение состоит в том, чтобы поместить их в тот же каталог, что и наш исполняемый файл.

Требуются следующие DLL:

  • libgcc_s_seh-1.dll
    Используется для поддержки обработки исключений C++ и других низкоуровневых функций, изначально не поддерживаемых системой Windows.

  • libstdc++6.dll
    Основа обслуживания C++-программы. Содержит функции и классы, используемые для выполнения различных операций, таких как ввод и вывод, математические операции и управление памятью.

  • libwinpthread-1.dll
    Реализация pthread API для Windows.
    Эта DLL может не отображаться в выводе терминала, но она зависит от двух предыдущих упомянутых DLL.

Поскольку мы установили Mingw, эти DLL уже есть в нашей системе Linux. Нам лишь нужно их найти.

sudo find / -type f -name libgcc_s_seh-1.dll 2>/dev/null

Эта команда обнаруживает libgcc_s_seh-1.dll, игнорируя каталоги (используйте -type f), так как поиск начинается с корневого каталога (используйте /). Если возникла ошибка, сбросьте на /dev/null (с помощью 2>/dev/null).

Мы увидим соответствующий вывод для 

  • /usr/lib/gcc/x86_64-w64-mingw32/9.3-win32/
  • /usr/lib/gcc/x86_64-w64-mingw32/9.3-posix/

Обратите внимание на win32 и posix как часть имени каталога. При сборке с помощью Makefile скопируйте такую DLL из каталога на основе posix. При сборке Makefile-th_win32 скопируйте DLL из каталога на базе win32.

Поскольку мы решили основываться главным образом на pthread, я предлагаю следующее:

  • Скопируйте DLL из posix-каталога в каталог нашего проекта (тот же, что и для исполняемого двоичного файла).
  • Нам может понадобиться протестировать поток win32, поэтому мы можем создать каталог win32 и posix, а затем скопировать соответствующие DLL в каждый каталог.
    Всякий раз, когда нужно скопировать тот или иной поток, скопируйте созданный DLL и исполняемый файл в такой каталог win32 или posix, а затем запустите программу оттуда через Wine или наоборот.

Наконец, мы можем протестировать программу следующим образом.

$ wine main.exe
0098:fixme:hid:handle_IRP_MN_QUERY_ID Unhandled type 00000005

        ... 
0098:fixme:xinput:pdo_pnp IRP_MN_QUERY_ID type 5, not implemented!

        ... 
--- misc ---
add(1,2): 3
sub(2,1): 1
hardware concurrency: 12
--- end ---

--- single-threaded sum(1000M) ---
elapsed time: 416.829ms
sum: 1000000000
--- end ---

--- multi-threaded sum_v2(1000M) ---
elapsed time: 121.164ms
sum: 1000000000
--- end ---

Не обращайте внимания на нерелевантные строки вывода, которые являются предупреждениями и незначительными ошибками самого Wine.

Мы видим, что многопоточная функция быстрее однопоточной примерно в 3,4 раза. Это всё еще немного медленнее, чем родная сборка Linux, что вполне понятно.
Мы вернемся к теме тестирования позже, когда закончим реализацию необходимого MQL5-кода.

Всё готово для реализации MQL5-кода.


Эксперимент, этап разработки II - MQL5-код для использования DLL

Нам пришлось пройти долгий путь до второго этапа разработки MQL5-кода.

Реализация скрипта TestConsumeDLL.mq5 .

//+------------------------------------------------------------------+
//|                                               TestConsumeDLL.mq5 |
//|                                          Copyright 2022, haxpor. |
//|                                                 https://wasin.io |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, haxpor."
#property link      "https://wasin.io"
#property version   "1.00"

#import "example.dll"
const int add(int, int);
const int sub(int, int);
const int num_hardware_concurrency();
const int single_threaded_sum(const int& arr[], int num_elem);
const int multi_threaded_sum_v2(const int& arr[], int num_elem);
#import

void OnStart()
{
   Print("add(1,2): ", example::add(1,2));
   Print("sub(2,1): ", example::sub(2,1));
   Print("Hardware concurrency: ", example::num_hardware_concurrency());

   int arr[];
   ArrayResize(arr, 1000000000);                // 1000M elements
   ArrayFill(arr, 0, ArraySize(arr), 1);

   // benchmark of execution time will be printed on terminal
   int sum = 0;
   Print("--- single_threaded_sum(1000M) ---");
   sum = single_threaded_sum(arr, ArraySize(arr));
   Print("sum: ", sum);
   if (sum != 1000000000) Alert("single_threaded_sum result not correct");
   Print("--- end ---");

   sum = 0;

   Print("--- multi_threaded_sum_v2(1000M) ---");
   sum = multi_threaded_sum_v2(arr, ArraySize(arr));
   Print("sum: ", sum);
   if (sum != 1000000000) Alert("multi_threaded_sum_v2 result not correct");
   Print("--- end ---");
}

Мы можем выполнить MQL5-код в виде советника или индикатора, но в целях нашего эксперимента нам необходимо проверить все шаги и весь рабочий процесс. Для этой цели лучше всего подходит скрипт.
В реальной ситуации нам обычно нужны советники или индикаторы, чтобы получать данные из терминала, например OnTick(), OnTrade(), OnCalculate(). Дополнительную информацию о том, какие функции поддерживаются каждым типом программы в платформе MetaTrader 5, можно найти в разделе "Выполнение программ".

Теперь давайте разберем приведенный выше код фрагмент за фрагментом.

Импорт сигнатур функций из DLL.

#import "example.dll"
const int add(int, int);
const int sub(int, int);
const int num_hardware_concurrency();
const int single_threaded_sum(const int& arr[], int num_elem);
const int multi_threaded_sum_v2(const int& arr[], int num_elem);
#import

Чтобы иметь возможность вызывать функции, выведенные из DLL, нам нужно снова объявить эти сигнатуры в MQL5-коде.

На что следует обратить внимание:

  • Мы можем пропустить имена параметров функции, например add(int, int) и sub(int, int).
  • Массив передается в виде ссылки только в MQL5. Обратите внимание на разницу сигнатур, объявленных в коде DLL и MQL5. В MQL5-коде присутствует & (амперсанд), а в DLL-коде его нет.
    Обратите внимание, что синтаксис C++, используемый в MQL5, и синтаксис стандартного C++ совпадают не полностью. Всякий раз, когда мы передаем массив в MQL5, нам нужно добавить &.

Создадим массив большого набора данных

   int arr[];
   ArrayResize(arr, 1000000000);                // 1000M elements
   ArrayFill(arr, 0, ArraySize(arr), 1);

Это создаст массив целых чисел для 1000 миллионов элементов и установит для каждого элемента значение 1. Массив является динамическим и хранится в динамической памяти. В стеке не хватит места для хранения такого огромного объема данных.
Итак, чтобы сделать массив динамическим, используйте синтаксис объявления int arr[].

После этого необходимо вызывать каждую функцию DLL из объявленных сигнатур по мере необходимости. Проверяем результат. Если он неверен, пользователю отправляется Alert(). Сразу не выходим.

Используем ArraySize(), чтобы получить количество элементов массива. Чтобы передать массив в функцию, просто передаем его переменную в функцию напрямую.

Скомпилируем скрипт и закончим реализацию.


Скопируем все необходимые DLL в MetaTrader 5

Перед запуском MQL5-скрипта нужно скопировать все необходимые DLL в каталог <terminal>/Libraries. Полный путь обычно выглядит так: ~/.mt5/drive_c/Program Files/MetaTrader 5/MQL5/Libraries.
Именно здесь MetaTrader 5 будет искать любые необходимые DLL по мере необходимости для программ, которые мы создали для MetaTrader 5. Вернемся к разделу "Тестовое выполнение кросс-компилируемого исполняемого файла", чтобы увидеть список DLL для копирования.

По умолчанию официальный скрипт установки MetaTrader 5 автоматически установит Wine с префиксом ~/.mt5 . Это относится только к пользователям, применяющим официальный скрипт установки.


Тестирование

Перетащим скомпилированный TestConsumeDLL на график

Перетащим скомпилированный TestConsumeDLL на график, чтобы начать выполнение

Сначала протестируем запуск MetaTrader 5 через Wine в Linux.
Перетащим скомпилированный TestConsumeDLL на график. Затем мы увидим диалоговое окно с запросом разрешения на импорт из DLL, а также список зависимостей DLL для созданной нами MQL5-программы.

Диалоговое окно с запросом разрешения на импорт DLL вместе со списком зависимостей DLL

Диалоговое окно с запросом разрешения на импорт DLL вместе со списком зависимостей DLL

Хотя мы и не увидели libwinpthread-1.dll, потому что это не непосредственная зависимость скомпилированного скрипта MQL5, а зависимость для обоих - libgcc_s_seh-1.dll и libstdc++6.dll. Мы можем проверить DLL-зависимость целевого DLL-файла с objdump следующим образом.

$ objdump -x libstdc++-6.dll  | grep DLL
        DLL
 vma:            Hint    Time      Forward  DLL       First
        DLL Name: libgcc_s_seh-1.dll
        DLL Name: KERNEL32.dll
        DLL Name: msvcrt.dll
        DLL Name: libwinpthread-1.dll

$ objdump -x libgcc_s_seh-1.dll  | grep DLL
        DLL
 vma:            Hint    Time      Forward  DLL       First
        DLL Name: KERNEL32.dll
        DLL Name: msvcrt.dll
        DLL Name: libwinpthread-1.dll


objdump может читать двоичный файл (разделяемая библиотека или исполняемый файл), созданный в Windows и Linux. Он достаточно универсален, чтобы выгружать доступную информацию по мере необходимости. Флаг -x означает отображение содержимого всех заголовков.

Результат отображается на вкладке "Эксперты"

Результаты выполнения TestConsumeDLL на вкладке "Эксперты"

Результаты выполнения TestConsumeDLL на вкладке "Эксперты"


Также отображается время, затраченное на выполнение каждой функции в исходном окне терминала, которое использовалось для запуска MetaTrader 5 в Linux.

Затраченное время выводится в консоль для каждой функции

В том же окне терминала, которое использовалось для запуска MetaTrader 5, пользователи увидят время, затраченное на вывод из DLL

Пока мы не видели никаких предупреждений Alerts() и время, затраченное на исполнение, отображается правильно. Наша экспериментальная программа почти готова.


Тестирование в Windows

Нам нужно следующее:

  • VirtualBox с установленными гостевыми дополнениями
    В Интернете можно найти информацию об установке, так что смысла удлинять статью нет.
    Важно: гостевые дополнения необходимы для использования функции обмена данными между хостом и гостевой машиной, чтобы можно было копировать example.dll вместе со многими другими DLL на гостевую машину (Windows-машину).

  • 64-битный ISO-образ Windows 7+
    Образ должен быть загружен и установлен на жесткий диск через Virtualbox.

Основной интерфейс VirtualBox

Основной интерфейс VirtualBox. Зависит от наличия у вас аппаратных ресурсов, которые можно выделить. Чем больше, тем лучше, если вам нужно проверить скорость выполнения на DLL


Многое зависит от ресурсов вашей машины, которые можно выделить для запуска Windows через Virtualbox для тестирования скорости выполнения из DLL. В моем случае конфигурация следующая:

  • Система -> Материнская плата -> Оперативная память установлена на 20480 МБ или 20 ГБ (у меня 32 ГБ на хосте)
  • Система -> Процессор -> Процессор(ы) равно 6 с лимитом исполнения 100% (6 является здесь максимально допустимым значением)
  • Дисплей -> Экран -> Видеопамять установлена на максимум (это необязательно, но полезно, если вы используете несколько мониторов, чем больше мониторов, тем больше нужно видеопамяти)
  • Дисплей -> Экран -> Количество мониторов установлено на 1

Пришло время тестировать. Мы можем либо скопировать скомпилированный код MQL5 с машины Linux, либо просто скопировать весь код, а затем использовать MetaEditor, чтобы снова скомпилировать его на машине Windows.
Я обнаружил, что последний вариант вполне подходит. Это просто еще одно копирование и вставка. Поэтому я и выбрал его.

Результат теста

Результат TestConsumeDLL на вкладке "Эксперты" в Windows

Результаты на вкладке "Эксперты" в Windows


Проблема в том, что время, затраченное на выполнение, закодировано для отображения через стандартный вывод (stdout), и я не могу найти способ сохранить такой вывод при запуске MetaTrader 5 в Windows. Я пробовал запустить MetaTrader 5 с конфигурационным файлом для выполнения скрипта с самого начала, а затем перенаправить вывод в файл, но попытка не удалась, поскольку MetaTrader 5 не позволяет загружать какие-либо DLL при запуске из командной строки. Чтобы исправить это, не вмешиваясь в основной код DLL, внесем небольшую корректировку в MQL5-код, чтобы вычислить затраченное время оттуда, используя GetTickCount().

   ...
   int sum = 0;
   uint start_time = 0;
   uint elapsed_time = 0; 

   Print("--- single_threaded_sum(1000M) ---");
   start_time = GetTickCount();                         // *
   sum = single_threaded_sum(arr, ArraySize(arr));
   elapsed_time = GetTickCount() - start_time;          // *
   Print("sum: ", sum);
   if (sum != 1000000000) Alert("single_threaded_sum result not correct");
   Print("elapsed time: ", elapsed_time, " ms");
   Print("--- end ---");

   sum = 0;
   start_time = 0;
   elapsed_time = 0;

   Print("--- multi_threaded_sum_v2(1000M) ---");
   start_time = GetTickCount();                         // *
   sum = multi_threaded_sum_v2(arr, ArraySize(arr));
   elapsed_time = GetTickCount() - start_time;          // *
   Print("sum: ", sum);
   if (sum != 1000000000) Alert("multi_threaded_sum_v2 result not correct");
   Print("elapsed time: ", elapsed_time, " ms");
   Print("--- end ---");
}

Обратите внимание на комментарий "// *". Это дополнительные строки, на которые стоит обратить внимание.

Проведем тестирование заново.

Результаты повторного тестирования TestConsumeDLL в Windows на вкладке "Эксперты"

Протестированный в Windows обновленный код MQL5 для измерения времени выполнения


Мы завершили работу над экспериментальным приложением, создав библиотеку DLL с поддержкой многопоточности, а затем использовав ее в MQL5-коде и протестировав в Linux и Windows. Всё работает нормально.


Простой тест обеих реализаций потоков Mingw

Мы проведем простой тест с использованием нашей экспериментальной программы, так как для проведения полноценного теста возможностей многопоточности в C++ на разных платформах необходимо учитывать много факторов, включая наличие множества примитивов синхронизации, thread_local, проблемный домент и т.д.

Тест проводится следующим образом:

  • Linux
    • Собрать с помощью Makefile. Затем провести тест пять раз перед усреднением, и сделать то же самое для Makefile-th_win32
    • Запустить бинарный файл с помощью WINEPREFIX=~/.mt5 wine main.exe
    • Использовать полные 12 потоков и всю доступную оперативную память в 32 ГБ.
  • Windows
    • Собрать с помощью Makefile. Затем провести тест пять раз перед усреднением, и сделать то же самое для Makefile-th_win32
    • Скопировать необходимые DLL и исполняемые файлы на гостевую машину (Windows) с помощью Virtualbox
    • Запустить бинарный файл с помощью командной строки main.exe
    • Ограничение в 6 потоков и 20 ГБ ОЗУ (оба из-за соблюдения допустимых настроек в VirtualBox)

Результаты будут округлены до двух знаков после запятой.

Результаты показаны в следующей таблице.

Функция  Linux + pthread (мс) Linux + поток win32 (мс) Windows + pthread (мс) Windows + win32 thread (мс)
 single_threaded_sum 417,53
417,20
467,77
475,00
 multi_threaded_sum_v2  120,91  122,51  121,98  125,00


Заключение

Mingw и Wine — это кроссплатформенные инструменты, которые позволяют разработчикам использовать Linux для создания кроссплатформенных приложений, без проблем работающих как в Linux, так и в Windows. Это относится и к разработке приложений для MetaTrader 5. Наше экспериментальное приложение для разработки DLL с поддержкой многопоточности на C++, протестированное как в Linux, так и в Windows, предлагает альтернативные варианты расширения доступа разработчиков к экосистеме.



Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/12042

Прикрепленные файлы |
ExampleLib.zip (5.37 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
BeeXXI Corporation
Nikolai Semko | 13 мар. 2023 в 09:11
Спасибо автору за интересный материал!
Было бы ещё интересно узнать и почитать об экспериментах по работе MT5 с Docker.
Maxim Kuznetsov
Maxim Kuznetsov | 13 мар. 2023 в 09:30

теперь у меня только один вопрос: "почему мою серию аналогичных (про С/С++/mingw) статей отклонили отклонили с формулировкой не соответствуют идеологии компании"

Aleksey Nikolayev
Aleksey Nikolayev | 13 мар. 2023 в 10:00
Было бы интересно что-то подобное в контексте создания быстрых кастомных функций на С/С++ для обработки питоновских массивов и таблиц (из numpy и pandas). Можно даже без линукса)
Wasin Thonkaew
Wasin Thonkaew | 13 мар. 2023 в 10:43
Nikolai Semko #:
Спасибо автору за интересный материал!
Было бы ещё интересно узнать и почитать об экспериментах по работе MT5 с Docker.
Thank you for your kind words. Sorry I didn't speak Russian.
Wasin Thonkaew
Wasin Thonkaew | 13 мар. 2023 в 10:46
Aleksey Nikolayev #:
Было бы интересно что-то подобное в контексте создания быстрых кастомных функций на С/С++ для обработки питоновских массивов и таблиц (из numpy и pandas). Можно даже без линукса)
Yes, exactly no need to be only Linux. It is just that I base on it solely, offer in perspective of cross platform developing on platform of your choice.

Ideally, it would be best to use compiler native to each platform. I might write something about it using CMake build system.

Thanks for your comment!

Популяционные алгоритмы оптимизации: Алгоритм растущих деревьев (Saplings Sowing and Growing up — SSG) Популяционные алгоритмы оптимизации: Алгоритм растущих деревьев (Saplings Sowing and Growing up — SSG)
Алгоритм растущих деревьев (Saplings Sowing and Growing up, SSG) вдохновлен одним из самых жизнестойких организмов на планете, который является замечательным образцом выживания в самых различных условиях.
Популяционные алгоритмы оптимизации: Алгоритм обезьян (Monkey algorithm, MA) Популяционные алгоритмы оптимизации: Алгоритм обезьян (Monkey algorithm, MA)
В этой статье рассмотрим алгоритм оптимизации "Алгоритм обезьян" (MA). Способность этих подвижных животных преодолевать сложные препятствия и добираться до самых труднодоступных вершин деревьев легли в основу идеи алгоритма MA.
Теория категорий в MQL5 (Часть 2) Теория категорий в MQL5 (Часть 2)
Теория категорий представляет собой разнообразный и расширяющийся раздел математики, который пока относительно не освещен в MQL5-сообществе. Эта серия статей призвана осветить некоторые из ее концепций для создания открытой библиотеки и дальнейшему использованию этого замечательного раздела в создании торговых стратегий.
Эксперименты с нейросетями (Часть 4): Шаблоны Эксперименты с нейросетями (Часть 4): Шаблоны
Нейросети наше все. Проверяем на практике, так ли это. MetaTrader 5 как самодостаточное средство для использования нейросетей в трейдинге. Простое объяснение.