English Русский 中文 Español 日本語 Português
preview
Entwicklung einer DLL für eine Machbarkeitsstudie mit C++ Multi-Threading-Unterstützung für MetaTrader 5 unter Linux

Entwicklung einer DLL für eine Machbarkeitsstudie mit C++ Multi-Threading-Unterstützung für MetaTrader 5 unter Linux

MetaTrader 5Beispiele | 10 März 2023, 12:00
357 0
Wasin Thonkaew
Wasin Thonkaew

Einführung

Linux hat ein lebendiges Entwicklungsökosystem und eine gute Ergonomie für die Softwareentwicklung.

Es ist attraktiv für diejenigen, die gerne mit der Kommandozeile arbeiten, die einfache Installation von Anwendungen über den Paketmanager, ein Betriebssystem, das keine Blackbox ist, aber dennoch dazu verleitet, sich mit seinen Interna zu beschäftigen, das für fast alle Untersysteme konfigurierbar ist, wichtige Entwicklungswerkzeuge, die sofort einsatzbereit sind, eine flexible und schlanke Umgebung für die Softwareentwicklung usw.

Das Angebot reicht vom PC-Desktop für Endnutzer bis hin zu Cloud-Lösungen wie VPS oder Cloud-Service-Anbietern wie AWS und Google Cloud.

Ich glaube also fest daran, dass es hier einige Entwickler gibt, die an ihrem bevorzugten Betriebssystem festhalten, aber dennoch in der Lage sein wollen, Produkte für Windows-Nutzer zu entwickeln und zu liefern. Natürlich müssen die Produkte auf allen Plattformen nahtlos und sofort funktionieren.

Normalerweise verwenden MetaTrader 5-Entwickler nur die Programmiersprache MQL5, um ihre Indikatoren/Experten oder verwandte Produkte zu entwickeln und sie dann auf dem Markt zu veröffentlichen, ohne sich Gedanken darüber zu machen, auf welchem Betriebssystem sie basieren. Sie können sich einfach darauf verlassen, dass die IDE von MT5 die Kompilierung und Erstellung der ausführbaren .EX5-Datei vor der Auslieferung übernimmt (vorausgesetzt, sie wissen, wie man MetaTrader 5 unter Linux startet).
Wenn Entwickler jedoch eine nutzerdefinierte Lösung als gemeinsam genutzte Bibliothek (DLL) entwickeln müssen, um zusätzliche Dienste anzubieten, die die MQL5-Programmiersprache allein nicht bieten kann, dann müssen sie mehr Zeit und Mühe aufwenden, um eine Lösung für die Kreuzkompilierung zu finden, Probleme und bewährte Verfahren zu entdecken, sich mit den Tools vertraut zu machen usw.

Das sind die Gründe, die in diesem Artikel genannt werden. Durch die Einbeziehung von Kreuzkompilierungs-Lösung und die Fähigkeit, DLL mit C + + Multi-Threading-fähig zu bauen, sind diese beiden kombiniert zumindest eine Grundlage, die Entwickler als Basis verwenden können, um weiter zu erweitern.
Ich hoffe, dass es Ihnen helfen wird, die Entwicklung von MetaTrader 5 bezogenen Produkten auf Ihrem geliebten Betriebssystem Ihrer Wahl, Linux, fortzusetzen.

Für wen ist dieser Artikel bestimmt?

Ich gehe davon aus, dass die Leser, die diesen Artikel lesen wollen, bereits einige Erfahrung im Umgang mit Linux über die Kommandozeile haben und ein allgemeines Konzept für die Kompilierung und Erstellung von C++-Quellcode unter Linux besitzen.

Wie auch immer, dieser Artikel ist für diejenigen, die Schritte und Arbeitsabläufe zu entwickeln DLL fähig Multi-Threading-Fähigkeit auf Linux zu erkunden, sondern funktioniert auch auf Windows. Erweitern Sie die Möglichkeiten der Threading-Programmierung nicht nur durch das integrierte OpenCL, sondern auch durch flexibleren, portablen C++-Code mit seiner Multithreading-Fähigkeit, der sich in einige andere Systeme integrieren lässt, die eng darauf basieren. 

Verwendetes System und Software

  • Ubuntu 20.04.3 LTS mit Kernel-Version 5.16.0 auf AMD Ryzen 5 3600 6-Kern-Prozessor (2 Threads pro Kern), 32 GB RAM
  • Wine (winehq-devel-Paket) Version 8.0-rc3 (zum Zeitpunkt der Erstellung dieses Artikels) - (siehe auch Build 3550 stürzte sofort ab, wenn es mit dem winehq-stable-Paket gestartet wurde, weshalb in diesem Artikel das devel-Paket und nicht das stable-Paket verwendet wurde)
  • Mingw (Paketmingw-w64) Version 7.0.0-2
  • Virtualbox Version 6.1 zum Testen auf einem Windows-System

Schlachtplan

Wir werden wie folgt vorgehen

  1. Kennenlernen von Wine
  2. Kennenlernen von Mingw
  3. Mingws Threading-Implementierungen
    1. POSIX (pthread)
    2. Win32 (über die Github-Seite von mingw-std-threads, als reines Header-Drop-In, das mit mingwmingw-std-threads arbeitet)
  4. Vorbereiten einer Linux-Entwicklungsmaschine
    1. Installation von Wine
    2. Installation von MetaTrader 5
    3. Installation von Mingw
    4. Installation (optional) von mingw-std-threads
  5. Machbarkeitsnachweis, Entwicklungsphase I - DLL (C++ Multi-Threading-Unterstützung)
  6. Machbarkeitsnachweis, Entwicklungsphase II - MQL5 Code to Consume DLL
  7. Tests auf einem Windows-System
  8. Einfacher Benchmark der Threading-Implementierungen von Mingw


Wine

Wine ist die Abkürzung für (technisch gesehen ein Wikipedia für Rekursives Akronym oder Backronym) „Wine ist keine Emulator“. Es handelt sich nicht um einen Emulator, der einen Prozessor oder eine Zielhardware emuliert. Stattdessen ist es ein Wrapper für die Win32-API, der auf Nicht-Windows-Betriebssystemen funktioniert.

Wine führt eine weitere abstrakte Schicht ein, die den Aufruf der Win32-API von Nutzern auf Nicht-Windows-Systemen abfängt, ihn dann an die Wine-Interna weiterleitet und die Anfrage genauso (oder fast genauso) wie unter Windows verarbeitet. 

Das bedeutet, dass Wine auf der win32 API arbeitet, indem es die POSIX API verwendet. Die Leser können Wine-Software verwenden, ohne zu wissen, wann sie solche Windows-Software unter Linux starten, oder sogar Steam-Spiele unter Linux spielen, da die Laufzeitumgebung auf einer Variante von Wine namens Proton basiert.

Dies ermöglicht Flexibilität beim Testen oder bei der Verwendung von Windows-Software, während ihre Alternativen unter Linux nicht verfügbar sind.

Wenn Sie eine Windows-basierte Anwendung über Wine ausführen möchten, führen Sie normalerweise den folgenden Befehl aus

wine windows_app.exe

oder wenn Sie die Anwendung mit einem bestimmten Wine-Umgebungspräfix verknüpfen wollen, lautet der Befehl

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe


Mingw

Mingw steht für „Minimalist GNU for Windows“. Es ist eine Portierung der GNU Compiler Collection (GCC) und ihrer Toolchains für die Kompilierung von C/C++ und einigen anderen Programmiersprachen unter Linux, aber für Windows.

Funktionen, Kompilierflags und -optionen sind konsistent und ähnlich von GCC und Mingw verfügbar, sodass Nutzer ihr vorhandenes Wissen von GCC zu Mingw leicht übertragen können. Bitte beachten Sie auch, dass GCC sehr ähnlich in der Kompilierung Flags/Optionen zu Clang als gut hat. Sie sehen also, dass die Nutzung problemlos ist und die Nutzer ihr Wissen verwenden können, aber mit dem zusätzlichen Vorteil, dass sie ihre Nutzerbasis auf das Windows-System erweitern können.

In der folgenden Vergleichstabelle finden Sie einen Überblick über die Unterschiede.

  • Kompilieren von C++-Quellcode und Erstellen einer gemeinsamen Bibliothek
Compiler Befehlszeile
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

  • Kompilieren von C++-Quellcode und Erstellen einer ausführbaren Binärdatei
Compiler Befehlszeile
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

Die Leser werden feststellen, dass die Unterschiede minimal sind. Die Kompilierungsflags sind sehr ähnlich, meist gleich. Nur, dass wir unterschiedliche Compiler-Binärdateien verwenden, um alles zu kompilieren und zu bauen, was wir brauchen.

Es gibt 3 Varianten, in denen es zum Thema Threading-Implementierung führt, die wir im nächsten Abschnitt erläutern werden.

  1. x86_64-w64-mingw32-g++
    Es ist ein Alias für x86_64-w64-mingw32-g++-win32.

  2. x86_64-w64-mingw32-g++-posix
    Binäre ausführbare Datei, die mit pthread arbeiten soll.

  3. x86_64-w64-mingw32-g++-win32
    Binäre ausführbare Datei, die mit dem Win32-API-Threading-Modell arbeiten soll. Es wird durch 86_64-w64-mingw32-g++ ersetzt.

Darüber hinaus gibt es mehrere andere Tools mit dem Präfix 

x86_64-w64-mingw32-...

Hier einige Beispiele

  • x86_64-w64-mingw32-gcc-nm - Werkzeug zur Namensänderung
  • x86_64-w64-mingw32-gcc-ar - Werkzeug zur Archivverwaltung
  • x86_64-w64-mingw32-gcc-gprof - ein Werkzeug zur Leistungsanalyse für Unix-ähnliche Betriebssysteme
außerdem gibt es die Varianten x86_64-w64-mingw32-gcc-nm-posix und x86_64-w64-mingw32-gcc-nm-win32 .

Mingw Threading-Implementierungen

Aus dem vorigen Abschnitt wissen wir nun, dass es 2 Varianten der Threading-Implementierung von Mingw gibt.
  1. POSIX (pthread)
  2. Win32

Warum müssen wir uns darüber überhaupt Gedanken machen? Es gibt 2 Gründe, die mir einfallen

  1. Aus Sicherheits- und Kompatibilitätsgründen
    Wenn Ihr Code möglicherweise sowohl die C++-Multi-Threading-Fähigkeit (z. B. std::thread, std::promise, usw.) als auch die systemeigene Multi-Threading-Unterstützung verwendet (z. B. CreateThread() für die Win32-API und pthread_create() für die POSIX-API) ist es besser, sich an die Verwendung einer API zu halten, als an die der anderen.

    Jedenfalls ist es sehr unwahrscheinlich, dass wir den Code, der die Multi-Threading-Fähigkeit von C++ nutzt, und die OS-Unterstützung zusammen mischen werden, es sei denn, es ergibt sich eine sehr spezifische Situation, in der die OS-Unterstützungs-API mehr Funktionen bietet, die C++ nicht bieten kann. Es ist also besser, die Konsistenz zu wahren und für beide das gleiche Threading-Modell zu verwenden.
    Wenn wir die pthread-Implementierung von Mingw verwenden, dann versuchen Sie, die Threading-Fähigkeit der Win32-API zu vermeiden. Ebenso sollten wir, wenn wir die Win32-Threading-Implementierung von Mingw verwenden (von nun an kurz als „Win32-Thread“ bezeichnet), auf die Verwendung der pthread-API des Betriebssystems verzichten.

  2. Leistung (siehe später im Abschnitt: Einfacher Benchmark der Mingw-Threading-Implementierungen)
    Natürlich wollen die Nutzer eine Multi-Threading-Lösung mit geringer Latenz. Die schnellere Lösung zur Ausführung in bestimmten Situationen ist wahrscheinlich diejenige, für die sich der Nutzer entscheiden würde.

Wir werden zunächst unsere Proof-of-Concept-DLL und ein Testprogramm entwickeln, bevor wir einen Benchmark für beide Threading-Implementierungen durchführen werden.

Für das Projekt werden wir portablen Code zur Verfügung stellen, um entweder pthread oder win32 thread zu verwenden, wobei unser Build-System in der Lage ist, leicht zwischen den beiden zu wechseln.
Im Falle der Verwendung von Win32-Threads müssen wir Header von der mingw-std-threads installieren, in dem wir die Leser anleiten werden, wie es weitergeht.


Linux-Entwicklungsmaschine vorbereiten

Bevor wir mit der Programmierung beginnen, müssen wir zunächst die erforderliche Software installieren.

Installation von Wine

Führen Sie den folgenden Befehl aus, um das Wine-Development-Paket zu installieren.

sudo apt install winehq-devel

und prüfen Sie dann mit dem folgenden Befehl, ob er ordnungsgemäß funktioniert,

wine --version

Die Ausgabe sieht dann etwa so aus

wine-8.0-rc3


Installation von MetaTrader 5

Die meisten Nutzer haben MetaTrader 5 bereits lange vor der Build 3550 installiert, das das Absturzproblem verursachte. Um auf das Paket winehq-devel umzusteigen, um das Problem zu lösen und MetaTrader 5 starten zu können, können wir nicht direkt ein offizielles Installationsskript verwenden, wie es in „Die Plattform unter Linux installieren“ zu sehen ist.
Es ist besser, die Befehle selbst auszuführen, da die direkte Ausführung des offiziellen Installationsskripts unser Wine-Paket wieder mit dem Stable-Paket überschreiben würde.

Ich habe den Leitfaden auf „MT5 Build 3550 Broken Launching On Linux Through Wine. Wie kann man das lösen?“ geschrieben. Dieser Artikel sollte alle Fälle abdecken, entweder für Nutzer, die bereits ein stabiles Wine-Paket installiert haben, oder für Nutzer, die neu mit dem devel-Paket beginnen wollen.

Versuchen Sie schließlich, MetaTrader 5 erneut über Wine zu starten. Sehen Sie nach, ob es ein Problem gibt.

Hinweis

Das offizielle Installationsskript erstellt eine Wine-Umgebung (genannt Präfix) unter ~/.mt5. Es könnte praktisch sein, die folgende Zeile in Ihrer ~/.bash_aliases zu haben, damit Sie MetaTrader 5 mit Leichtigkeit starten können.

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

dann die Quelle mit

source ~/.bash_aliases

Führen Sie schließlich den folgenden Befehl aus, um MT5 zu starten, wobei die Debug-Ausgabe im Terminal angezeigt wird.

mt5trader

Wenn wir MetaTrader 5 auf diese Weise starten, können wir das Debug-Protokoll unserer Proof-of-Concept-Anwendung später problemlos einsehen, ohne unseren Code unnötig zu komplizieren.

Installation von Mingw

Führen Sie den folgenden Befehl aus, um Mingw zu installieren.

sudo apt install mingw-w64

Dies installiert eine Reihe von Tools als Kollektion in Ihr System, wobei diesen Tools x86_64-w64-mingw32- vorangestellt wird. Meistens werden wir entweder mit x86_64-w64-mingw32-g++-posix (oder x86_64-w64-mingw32-win32 im Falle der Verwendung von win32 thread) arbeiten.

Installation von mingw-std-threads

mingw-std-threads ist ein Projekt, das die Win32-Threads von Mingw unter Linux zum Laufen bringt. Es handelt sich um eine reine Kopfzeile als Drop-in-Lösung. Die Installation ist also einfach und es muss nur die Header-Datei in den Include-Pfad des Systems aufgenommen werden.

Folgen Sie den nachstehenden Schritten zur Installation.

Klonen Sie zunächst das Git-Repository in Ihr System.

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

Legen Sie dann ein Verzeichnis an, das den Header im Include-Pfad des Systems enthält.

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

Kopieren Sie schließlich alle Header-Dateien (.h) aus dem Verzeichnis des geklonten Projekts in das neu erstellte Verzeichnis.

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

Das war's. Dann im Code, wenn wir entscheiden, win32-Thread zu verwenden, für einige Header-Dateien im Zusammenhang mit Multi-Threading-Fähigkeit (z. B. Threads, Synchronisation Primitive, etc.), müssen wir es mit dem richtigen Pfad mit einer Namenssubstitution einbinden. In der nachstehenden Tabelle finden Sie eine vollständige Liste.

Einbinden der C++11 Multi-threading Header-Datei Änderung der Einbindung von mingw-std-threads Header-Datei
#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>


Machbarkeitsnachweis, Entwicklungsphase I - DLL (C++ Multithreading-Unterstützung)

Jetzt ist es an der Zeit, mit der Programmierung zu beginnen.

Unser Ziel ist es, eine Proof-of-Concept-DLL-Lösung zu implementieren, die in der Lage ist, die Multithreading-Fähigkeit der C++11-Standardbibliothek zu nutzen, damit die Leser die Idee verstehen und weiter ausbauen können.

Im Folgenden finden Sie unsere Bibliotheks- und Anwendungsstruktur.

Projektstruktur

  • DLL
    • example.cpp
    • example.h
  • Consumer
    • main.cpp
  • Build system
    • Makefile - eine kompilationsübergreifende Build-Datei unter Verwendung von pthread von Mingw
    • Makefile-th_win32 - eine kompilationsübergreifende Build-Datei, die den win32-Thread von Mingw verwendet 
    • Makefile-g++ - Build-Datei zum Testen auf nativem Linux. Dies dient der schnellen Iteration und Fehlersuche während der Entwicklung des Projekts.

Verwendeter C++-Standard

Wir werden den C++17-Standard verwenden, obwohl wir größtenteils Funktionen von C++11 nutzen werden, aber einige wenige, z. B. Attribute von Code-Anmerkungen wie [[nodiscard]], erfordern C++17.

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 ist zwar nicht Teil des C++-Standards, wird aber vom GCC und damit auch von Mingw unterstützt. Es ist ein flexibler und kürzerer Weg, um die doppelte Aufnahme von Kopfzeilen zu verhindern.
Ohne eine solche Direktive würde der Nutzer sowohl #ifdef als auch #define verwenden und müsste sicherstellen, dass jede Definition einen eindeutigen Namen für jede Header-Datei hat. Das kann zeitaufwendig sein.

Wir haben #ifdef WINDOWS , um die Definitionserklärung von EXAMPLE_API zu schützen. Dies ermöglicht es uns, die Kompilierung mit Mingw und dem nativen Linux-System durchzuführen. Wenn wir also eine Kreuzkompilierung für eine gemeinsam genutzte Bibliothek durchführen wollen, dann fügen wir sowohl -DWINDOWS als auch -DEXAMPLE_EXPORT zum Kompilierungsflag hinzu, andernfalls können wir -DEXAMPLE_EXPORT weglassen, wenn wir nur für ein Test-Hauptprogramm kompilieren.

__declspec(dllexport) ist eine Direktive zum Exportieren einer Funktion aus der DLL

__declspec(dllimport) ist eine Direktive zum Importieren einer Funktion aus einer DLL.

Für die Kompilierung der DLL unter Windows sind die oben genannten zwei Dateien erforderlich. Für Nicht-Windows-Systeme brauchen wir sie nicht, aber wir brauchen sie für die Kreuzkompilierung. Daher ist es leer für EXAMPLE_API , wenn keine Definition von WINDOWS für die Kompilierung für Linux vorliegt.

Außerdem ist es ein wichtiger Teil der Funktionssignaturen. Funktionssignaturen müssen mit der Aufrufkonvention von C (der Programmiersprache) kompatibel sein.
Dieses externe „C“ wird verhindern, dass Funktionssignaturen in C++-Aufrufkonventionen umgewandelt werden.

Wir können Funktionssignaturen nicht in Namespaces einschließen oder sie als freie Funktionen deklarieren, weil MQL5-Code nicht in der Lage sein wird, diese Signaturen zu finden, wenn wir die DLL später verwenden.

Für num_hardware_concurrency() wird die Anzahl der von der Implementierung unterstützten gleichzeitigen Threads zurückgegeben.
Ich verwende z. B. einen 6-Kern-Prozessor mit 2 Threads pro Kern, d. h. es gibt praktisch 12 Threads, die gleichzeitig arbeiten können. In meinem Fall wird die Zahl 12 angezeigt.

Sowohl single_threaded_sum() als auch multi_threaded_sum_v2() sind Paradebeispiele für unsere Proof-of-Concept-Anwendung, um die Vorteile der Verwendung von Multi-Threading zu zeigen und die Leistung der beiden zu vergleichen.

beispiel.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;
}

Oben ist der vollständige Code zu sehen, aber zum besseren Verständnis wollen wir jeden Teil einzeln aufschlüsseln. Die einzelnen Codeabschnitte werden im Folgenden näher erläutert.

Umschaltbar zwischen der Verwendung von pthread und win32 thread.

#include "example.h"

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

Mit diesem Setup können wir unser Build-System gut integrieren, um zwischen der Verwendung und Verknüpfung mit pthread oder win32 thread zu wechseln. Durch Hinzufügen von -DUSE_MINGW_STD_THREAD in der Kompilierungsflags, um win32-Thread zu verwenden, wenn wir Kreuzkompilierung durchführen.

Implementieren wir einfache, klare Schnittstellen.

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() und sub() sind einfach und leicht zu verstehen. Für num_hardware_concurrency() müssen wir den Header <thread> einbinden, um std::thread::hardware_concurrency() verwenden zu können.

Hilfsfunktion für das Debug-Protokoll.

#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

Durch Hinzufügen von -DENABLE_DEBUG in der Kompilierungsflags aktivieren wir die Ausgabe des Debug-Logs auf der Konsole. Deshalb schlage ich vor, MetaTrader 5 über die Kommandozeile zu starten, damit wir unser Programm entsprechend debuggen können.
Wenn wir eine solche Definition nicht definiert haben, bedeutet DLOG() nichts und hat keine Auswirkungen auf unseren Code, weder in Bezug auf die Ausführungsgeschwindigkeit noch in Bezug auf die Binärgröße der Shared Library oder der ausführbaren Binärdatei. Es ist sehr schön.

DLOG() wurde in Anlehnung an die Android-Entwicklung entwickelt, da es normalerweise einen Kontext-String gibt (von welcher Komponente auch immer das Debug-Log kommt), der in diesem Fall ctx ist, und dann folgt der Debug-Log-String.

Implementierung einer „single threaded“ Summierungsfunktion .

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;
}

Sie simuliert die reale Nutzung der Arbeit mit MQL5-Code. Stellen Sie sich die Situation vor, dass MQL5 einige Daten als Array an die DLL-Funktion sendet, um etwas zu berechnen, bevor die DLL das Ergebnis an den MQL5-Code zurückgibt.
Das ist es. Bei dieser Funktion werden jedoch alle Elemente aus dem angegebenen Eingabefeld linear durchlaufen, und zwar eines nach dem anderen für die Gesamtzahl der Elemente, die durch num_elem angegeben wird .

Der Code misst auch die Gesamtzeit der Ausführung, indem er die Bibliothek std::chrono zur Berechnung der verstrichenen Zeit verwendet. Bitte beachten Sie, dass wir std::chrono::steady_clock verwenden, eine monotone Uhr, die sich vorwärts bewegt, ohne den Einfluss der Anpassungen der Systemuhr. Er ist für die Messung von Zeitintervallen bestens geeignet.

Implementierung einer „multi-threaded“ Summierungsfunktion.

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;
}

Beachten Sie, dass es als v2 markiert ist, ich belasse dies so aus historischen Gründen. Kurz gesagt, für moderne Prozessoren, die MESI verwenden, ist es nicht notwendig, eine Kopie des Datensatzes zu erstellen, der in jeden Thread eingespeist wird, da MESI eine solche Cache-Zeile für die gemeinsame Nutzung durch mehrere Threads markiert und keinen CPU-Zyklus für die Signalisierung und das Warten auf die Rückantwort verschwendet.
Meine frühere v1-Implementierung gab sich alle Mühe, für jeden Thread eine Kopie des Datensatzes zu erstellen. Aber wie bereits erwähnt, verwenden moderne Prozessoren bereits MESI, sodass es nicht notwendig ist, einen solchen Versuch in den Quellcode aufzunehmen. v1 ist viel langsamer als v2, ca.~2-5 Mal.

Beachten Sie, dass worker_func eine Lambda-Funktion ist, die mit dem ursprünglichen Datenarray und dem Datenbereich arbeitet, mit dem gearbeitet werden soll (Paar von Anfangs- und Endindizes). Sie summiert alle Elemente innerhalb der Schleife in eine lokale Variable, um das False Sharing zu vermeiden, das die Leistung erheblich verlangsamt, bevor sie sie schließlich „atomweise“ zu einer gemeinsamen Summenvariablen für alle Threads addiert. Sie verwendet std::atomic, um es thread-sicher zu machen. Die Häufigkeit, mit der eine solche gemeinsame Summenvariable geändert werden muss, ist so gering, dass sie die Leistung nicht wesentlich beeinträchtigt. Ein Gleichgewicht zwischen der praktischen Umsetzung und dem Geschwindigkeitsgewinn ist ein guter Weg.

Wir berechnen, wie viele Threads benötigt werden, um die Arbeit aufzuteilen, sodass wir später den Umfang der Arbeit für jeden Thread kennen. Beachten Sie, dass std::hardware_concurrency() 0 zurückgeben kann, was bedeutet, dass es möglicherweise nicht in der Lage ist, die Anzahl der Threads zu bestimmen, daher behandeln wir auch diesen Fall und greifen auf 2 zurück.

Als Nächstes erstellen wir einen Vektor von Threads. Wir reservieren seine Kapazität für num_max_threads. Dann berechnen wir iterativ den Bereich des Datensatzes, an dem jeder Thread arbeiten soll. Beachten Sie, dass der letzte Thread alle verbleibenden Daten übernimmt, da die Anzahl der zu bearbeitenden Elemente meist nicht durch die Anzahl der zu verwendenden Threads teilbar ist.

Wichtig ist, dass wir alle Threads zusammenführen. Für kompliziertere Umstände benötigen wir möglicherweise eine asynchrone Umgebung, die den MQL5-Code nicht beim Warten auf das Ergebnis blockiert. Dazu verwenden wir in der Regel std::future, das die Basis für alle std::async, std::promise und std::packaged_task ist. So haben wir in der Regel mindestens 2 Schnittstellen, eine, um eine Anforderung zu machen, die Daten von MQL5-Code zu berechnen, indem DLL ohne Blockierung, und eine andere, um das Ergebnis einer solchen Anforderung zurück on-demand zu erhalten, an welchem Punkt, wird es den Aufruf auf MQL5-Code blockieren. Vielleicht schreibe ich darüber in einem zukünftigen Artikel.

Außerdem können wir DLOG() verwenden, um einige Debugging-Zustände auszugeben. Das ist hilfreich bei der Fehlersuche.

Als Nächstes wollen wir das portable Haupttestprogramm implementieren, das unter nativem Linux und in einer Kreuzkompilierungs-Umgebung mit Wine läuft.

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;
}

Normalerweise binden wir die Header-Datei example.h ein, um diese implementierten Schnittstellen aufrufen zu können. Wir überprüfen auch, ob die Ergebnisse korrekt sind, indem wir assert() verwenden.

Als Nächstes erstellen wir sowohl die gemeinsam genutzte Bibliothek (als libexample.so für Linux) als auch das Haupttestprogramm, nämlich main.out. Nicht zunächst mit einem „build system“, sondern über die Befehlszeile. Wir werden später ein „build system“ über das Makefile richtig implementieren.
Wir testen es zunächst lokal unter Linux, bevor wir eine Kreuzkompilierung durchführen.

Führen Sie den folgenden Befehl aus, um eine Shared Library als libexample.so zu erstellen.

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

Die Erklärungen zu den einzelnen Flags lauten wie folgt

Flag Beschreibung
-shared Compiler anweisen, eine gemeinsame Bibliothek zu erstellen
-std=c++17 Anweisung an den Compiler, die C++-Syntax auf den C++17-Standard zu stützen
-Wall Anweisung, alle Warnungen beim Kompilieren auszugeben
-Wextra Anweisung, beim Kompilieren noch mehr Warnungen auszugeben
-fno-rtti Dies geschieht im Rahmen der Optimierung. Es deaktiviert RTTI (Runtime Type Information).
RTTI ermöglicht die Bestimmung des Objekttyps während der Programmausführung, was wir nicht brauchen, da es zu Leistungseinbußen führt.
-O2 Aktivieren Sie die Optimierungsstufe 2, die zusätzlich zu Stufe 1 noch aggressivere Optimierungen beinhaltet.
-I. Legen Sie den Include-Pfad auf das aktuelle Verzeichnis fest, sodass der Compiler unsere Header-Datei example.h finden kann, die sich im selben Verzeichnis befindet.
-fPIC Wird in der Regel benötigt, wenn eine gemeinsam genutzte Bibliothek erstellt wird, da es einen Compiler anweist, positionsunabhängigen Code (PIC) zu erzeugen, der für eine gemeinsam genutzte Bibliothek geeignet ist
und mit dem Hauptprogramm zusammenarbeitet, mit dem er verknüpft werden soll. Da es keine feste Speicheradresse gibt, um eine bestimmte Funktion aus einer gemeinsam genutzten Bibliothek zu laden, wird auch die Sicherheit erhöht.
-lpthread  Anweisung zum Verlinken zur pthread-Bibliothek

Führen Sie den folgenden Befehl aus, um ein Haupttestprogramm zu erstellen, das mit libexample.so verknüpft und als main.out ausgegeben wird .

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

Die Erklärungen für die einzelnen Flags, die sich von den oben genannten unterscheiden, lauten wie folgt

Flag Beschreibung
-L. Setzen Sie den Include-Pfad für die Shared Library auf dasselbe Verzeichnis
 -Lexample Verknüpfung mit der gemeinsam genutzten Bibliothek libexample.so.

Schließlich führen wir die ausführbare Datei aus.

$ ./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 ---

Das Ergebnis zeigt, dass die Multi-Thread-Funktion im Vergleich zur Single-Thread-Funktion deutlich schneller arbeitet (~4,33x schneller).

Da wir damit vertraut sind, wie man sowohl die gemeinsam genutzte Bibliothek als auch das Hauptprogramm mit der Kommandozeile kompiliert und baut, bauen wir das weiter aus, um ein richtiges Build-System über Makefile zu erstellen.
Zwar gibt es dafür CMake, aber da wir hauptsächlich unter Linux entwickeln und bauen, scheint mir CMake zu viel des Guten zu sein. Wir brauchen eine solche Kompatibilität nicht, um auf Windows bauen zu können. Makefile ist also die richtige Wahl.

Wir werden drei Varianten von Makefile haben.

  1. Makefile
    Es ist für die Kreuzkompilierung für Linux und Windows gedacht. Es verwendet pthread. Wir verwenden dies, um eine DLL zu erstellen, die mit MetaTrader 5 arbeitet, zusätzlich zum Haupttestprogramm, das über Wine gestartet werden kann.

  2. Makefile-th_win32
    Wie Makefile, aber es verwendet Win32-Thread.

  3. Makefile-g++
    Es ist für die Kompilierung auf einem nativen Linux-System gedacht. Das sind die Schritte, die wir gerade oben gemacht haben.

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

Von den 3 Varianten der obigen Makefiles teilt es fast den gesamten Code, mit einigen minimalen Unterschieden.

Bitte nehmen Sie sich etwas Zeit, um zu sehen, welche Unterschiede es zwischen ihnen gibt, insbesondere

  • der Name der Compiler-Binärdatei
  • -DUSE_MINGW_STD_THREAD
  • mit oder ohne -lpthread
  • Binärname der Ausgabe, z. B. libexample.so oder example.dll, und main.out oder main.exe; je nach Zielsystem, für das gebaut werden soll


MORE_FLAGS wird deklariert als

MORE_FLAGS ?=

was bedeutet, dass es dem Nutzer erlaubt, zusätzliche Kompilierungsflags von der Kommandozeile aus zu übergeben, sodass der Nutzer bei Bedarf weitere Flags hinzufügen kann. Wenn keine Flags von außen übergeben werden, wird das verwendet, was bereits im Makefile-Code definiert ist.

Als Nächstes machen Sie alle diese Makefiles ausführbar über

$ chmod 755 Makefile*

Um für eine bestimmte Variante des obigen Makefiles zu bauen, siehe die folgende Tabelle.

Zielsystem Befehl Build  Befehl Clean
Kreuzkompilierung mit pthread make  make clean
Kreuzkompilierung mit Win32-Thread make -f Makefile-th_win32  make -f Makefile-th_win32 clean
Native Linux make -f Makefile-g++  make -f Makefile-g++ clean

Wir erstellen eine DLL zur Verwendung mit MetaTrader 5 und Wine. Wir können also beides testen.

Das gilt auch für

$ make

Es werden die folgenden Dateien erzeugt

  1. example.dll
  2. main.exe


Test der Ausführung einer kreuzkompilierten, ausführbaren Datei.

$ 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

Ok, wir haben ein Problem. main.exe kann die benötigten DLLs nicht finden.
Die Lösung besteht darin, sie alle in das gleiche Verzeichnis zu bringen wie unsere ausführbare Datei.

Die erforderlichen DLLs sind wie folgt

  • libgcc_s_seh-1.dll
    Wird verwendet, um Unterstützung für die C++-Ausnahmebehandlung und andere Low-Level-Funktionen zu bieten, die vom Windows-System nicht nativ unterstützt werden.

  • libstdc++6.dll
    Grundlage für das C++-Programm. Sie enthält Funktionen und Klassen, mit denen verschiedene Operationen wie Ein- und Ausgabe, mathematische Operationen und Speicherverwaltung durchgeführt werden können.

  • libwinpthread-1.dll
    Es ist die Implementierung der pthread API für Windows.
    Diese DLL wird zwar nicht in der Terminalausgabe angezeigt, aber sie ist eine DLL-Abhängigkeit von den beiden zuvor genannten DLLs.

Da wir Mingw installiert haben, sind diese DLLs bereits auf unserem Linux-System vorhanden. Wir müssen sie nur finden.
Verwenden Sie die folgende Technik, um sie zu finden.

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

Dieser Befehl findet libgcc_s_seh-1.dll und ignoriert Verzeichnisse (wegen -type f), indem er die Suche im Stammverzeichnis (wegen /) beginnt. Wenn ein Fehler auftritt, wird er über /dev/null (wegen 2>/dev/null) ausgegeben.

Wir sehen die entsprechende Ausgabe ab 

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

Achten Sie sorgfältig auf win32 und posix als Teil des Verzeichnisnamens. Wenn Sie über Makefile bauen, dann sollten Sie diese DLL aus einem Posix-basierten Verzeichnis kopieren. Wenn Sie aber mit Makefile-th_win32 bauen, dann kopieren Sie die DLLs aus dem win32-basierten Verzeichnis.

Da wir uns hauptsächlich für pthread entschieden haben, schlage ich folgendes vor

  • Kopieren Sie die DLLs aus dem Posix-Verzeichnis in das gleiche Verzeichnis wie unser Projekt, genau wie die ausführbare Binärdatei
  • Von Zeit zu Zeit möchten wir vielleicht mit einem Win32-Thread testen, also erstellen wir ein Win32- und ein Posix-Verzeichnis und kopieren dann die entsprechenden DLLs in jedes Verzeichnis.
    Wann immer Sie das eine oder andere testen möchten, kopieren Sie die erstellte DLL und die ausführbare Datei in das neu erstellte Win32- oder Posix-Verzeichnis und starten Sie das Programm von dort aus über Wine. Oder andersherum.

Schließlich können wir das Programm wie folgt testen

$ 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 ---

Ignoriere nicht relevante Ausgabezeilen, die Warnungen und kleinere Fehler von Wine selbst sind.

Wir sehen, dass eine Multi-Thread-Funktion um das 3,4-fache schneller ist als eine Single-Thread-Funktion. Immer noch etwas langsamer als der native Linux-Build, was verständlich ist.
Wir werden später noch einmal auf einen einfachen Benchmark zurückkommen, nachdem wir den MQL5-Code fertiggestellt haben, um ihn zu nutzen.

Fantastisch! Als Nächstes können wir nun den MQL5-Code implementieren.


Machbarkeitsnachweis, Entwicklungsphase II - MQL5 Code zum Verbrauch von DLL

Es ist eine lange Reise, bis wir hier in Phase II der MQL5-Codeentwicklung ankommen.

Implementierung von TestConsumeDLL.mq5 als Skript.

//+------------------------------------------------------------------+
//|                                               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 ---");
}

Sicherlich können Sie einen MQL5-Code als Experten oder Indikatoren erstellen, aber für diese Proof-of-Concept-Arbeit gehen wir mit Hit and Run vor, um alle Schritte und den Workflow zu testen. Das Skript ist also für unseren Bedarf geeignet.
Wie auch immer, in der realen Welt benötigen Sie normalerweise Experten oder Indikatoren, um die Möglichkeit zu haben, Daten vom Terminal zu erhalten, z.B. OnTick(), OnTrade(), OnCalculate(). Weitere Informationen darüber, welche Funktionen von den einzelnen Programmtypen auf der MT-Plattform unterstützt werden, finden Sie im offiziellen MQL5-Dokument für Programmausführung.

Lassen Sie uns nun den gesamten obigen Code Stück für Stück zerlegen.

Funktionssignaturen aus DLL importieren.

#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

Um die von der DLL ausgestellten Funktionen aufrufen zu können, müssen wir diese Signaturen erneut im MQL5-Code deklarieren.

Zu beachtende Dinge

  • Wir können die Benennung der Funktionsparameter überspringen, z. B. add(int, int) und sub(int, int).
  • Array wird nur in MQL5 als Referenz übergeben. Beachten Sie den Unterschied zwischen den Signaturen, wie sie im DLL-Code deklariert sind, und MQL5. Im MQL5-Code gibt es & (das kaufmännische Und-Zeichen), aber nicht im DLL-Code.
    Bitte beachten Sie, dass die C++-Syntax, wie sie in MQL5 verwendet wird, und das eigentliche C++ nicht zu 100 % identisch sind. Kurz gesagt, wann immer wir in MQL5 ein Array übergeben, müssen wir & hinzufügen.

Erstellen eines Arrays mit einem großen Datensatz

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

Dadurch wird ein Array von Integer für 1000M Elemente erstellt und jedes Element auf den Wert 1 gesetzt. Damit ist das Array dynamisch und lebt „on heap“. Der Stapel hat nicht genug Platz, um eine so große Datenmenge zu speichern.
Um also ein Array dynamisch zu machen, verwenden Sie die Deklarationssyntax von int arr[].

Danach rufen wir einfach jede DLL-Funktion aus den deklarierten Signaturen nach Bedarf auf. Beachten Sie auch, dass wir die Ausgabe validieren, indem wir das Ergebnis überprüfen und, falls es nicht korrekt ist, die Nutzer mit Alert() benachrichtigen. Allerdings steigen wir nicht sofort aus.

Wir verwenden ArraySize() , um die Anzahl der Elemente des Arrays zu ermitteln. Um das Array an die Funktion zu übergeben, übergeben wir einfach seine Variable direkt an die Funktion.

Kompilieren Sie das Skript. Damit sind wir bereit für die Implementierung.


Kopieren Sie alle erforderlichen DLLs in den MetaTrader 5

Bevor wir versuchen, das MQL5-Skript zu starten. Wir müssen alle erforderlichen DLLs in das Verzeichnis <Terminal>/Libraries kopieren. Normalerweise lautet der vollständige Pfad ~/.mt5/drive_c/Program Files/MetaTrader 5/MQL5/Libraries.
Dort wird MetaTrader 5 nach allen erforderlichen DLLs suchen, die von den Programmen benötigt werden, die wir für MetaTrader 5 entwickelt haben. Gehen Sie zurück zum Abschnitt Test der Ausführung einer kreuzkompilierten, ausführbaren Datei, um die Liste der zu kopierenden DLLs zu sehen.

Standardmäßig installiert das offizielle MetaTrader 5-Installationsskript Wine automatisch mit dem Präfix ~/.mt5 . Dies gilt nur für Nutzer, die das offizielle Installationsskript verwenden.


Tests

Ziehen Sie die kompilierte TestConsumeDLL per Drag and Drop auf das Chart

Ziehen Sie die kompilierte TestConsumeDLL per Drag and Drop auf das Chart, um die Ausführung zu starten.

Testen Sie zunächst MetaTrader 5, der über Wine unter Linux gestartet wird.
Ziehen Sie die kompilierte TestConsumeDLL per Drag & Drop in das Chart. Sie werden sehen, dass ein Dialog erscheint, in dem Sie um die Erlaubnis gebeten werden, den Import von DLL zu erlauben, zusammen mit einer Auflistung der DLL-Abhängigkeiten für ein solches MQL5-Programm, das wir gebaut haben.

Dialog, der nach der Erlaubnis zum DLL-Import fragt, zusammen mit einer Liste der DLL-Abhängigkeiten.

Dialog, der nach der Erlaubnis zum DLL-Import fragt, zusammen mit einer Liste der DLL-Abhängigkeiten.

Auch wenn wir libwinpthread-1.dll nicht gesehen haben, weil es keine unmittelbare Abhängigkeit des kompilierten MQL5-Skripts ist, aber es ist eine Abhängigkeit für libgcc_s_seh-1.dll und libstdc++6.dll. Wir können die DLL-Abhängigkeit der Ziel-DLL-Datei mit objdump wie folgt überprüfen.

$ 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 kann Binärdateien (gemeinsam genutzte Bibliotheken oder ausführbare Dateien) lesen, die von Windows und Linux erstellt wurden. Es ist vielseitig, um verfügbare Informationen nach Bedarf auszugeben. Das Flag -x bedeutet, dass der Inhalt aller Kopfzeilen angezeigt werden soll.

Sehen Sie sich die Ergebnisausgabe auf der Registerkarte Experten an

Ausgabe auf der Registerkarte „Experten“ nach Ausführung von TestConsumeDLL

Ausgabe auf der Registerkarte „Experten“ nach Ausführung von TestConsumeDLL


sowie das Ergebnis der verstrichenen Ausführungszeit für jede Funktion im ursprünglichen Terminalfenster, das zum Starten von MetaTrader 5 unter Linux verwendet wurde.

Verstrichene Zeit bei der Ausführung Ausgabe in der Konsole für jede Funktion

Im gleichen Terminal-Fenster, das zum Starten von MetaTrader 5 verwendet wird, sehen die Nutzer die verstrichene Zeit in der Ausgabe der DLL.

Solange Sie keine Warnmeldungen von Alerts() erhalten haben und die verstrichene Ausführungszeit in Ordnung ist. Dann ist alles in Ordnung und wir haben fast alles in diesem Proof-of-Concept-Programm erledigt.


Tests auf einem Windows-System

Wir brauchen Folgendes

  • Virtualbox mit installierten Gasterweiterungen
    Bitte recherchieren Sie im Internet, wie Sie es auf Ihrem Linux-System installieren können. Andere Quellen haben bereits umfassende Informationen darüber geliefert, wie man es besser machen kann, als dass ich solche Informationen hier erschöpfend aufführe, was den Artikel unnötig in die Länge zieht.
    Wichtiger Hinweis: Sie benötigen Guest Additions, um die Funktionen zur gemeinsamen Nutzung von Daten zwischen Host- und Gastcomputer nutzen zu können. Sie können also example.dll zusammen mit anderen DLLs auf den Gastcomputer (Windows-Computer) kopieren.

  • Windows 7+ 64 Bit ISO-Abbild
    Diese soll über Virtualbox auf die Festplatte geladen und installiert werden.

Virtualbox-Hauptschnittstelle

Virtualbox Hauptschnittstelle. Hängt von der Verfügbarkeit von Hardware-Ressourcen ab, die Sie dafür erübrigen können; mehr ist besser, wenn Sie die Ausführungsgeschwindigkeit der DLL testen müssen.


Hängt auch davon ab, wie großzügig und Verfügbarkeit Ihrer Maschine Ressourcen, die auf Windows-System starten über Virtualbox für den Fall der Prüfung der Geschwindigkeit der Ausführung von DLL verschont werden kann. In meinem Fall habe ich folgenden Konfigurationen

  • System -> Motherboard -> Base Memory auf 20480 MB oder 20 GB eingestellt (ich habe 32 GB auf dem Hostrechner)
  • System -> Processor -> Processor(s) auf 6 und Ausführungskapazität auf 100 % gesetzt (kann nicht vollständig auf 12 gesetzt werden, da 6 hier der maximal zulässige Wert ist)
  • Display -> Screen -> Video Memory auf Maximum eingestellt (nicht unbedingt notwendig, aber für den Fall, dass Sie alle Monitore nutzen möchten. Mehr Monitore, mehr Videospeicher erforderlich)
  • Display -> Screen -> Monitor Count auf 1 gesetzt

Jetzt ist es an der Zeit zu testen. Wir können entweder den kompilierten MQL5-Code von der Linux-Maschine kopieren oder einfach den gesamten Code kopieren und dann MetaEditor verwenden, um ihn auf der Windows-Maschine erneut zu kompilieren.
Ich habe festgestellt, dass die letztere Option völlig in Ordnung ist, und es ist nur ein weiteres Kopieren und Einfügen entfernt. Also habe ich genau das getan.

Testergebnis

TestConsumeDLL-Ausgabeergebnis auf der Registerkarte „Experten“ unter Windows

Das Ergebnis wird auf der Registerkarte Experten angezeigt, wie unter Windows getestet.


Das Problem ist, dass die verstrichene Zeit in der Ausführung kodiert ist, um über die Standardausgabe (stdout) auszugeben, und ich kann keine Möglichkeit finden, diese Ausgabe beim Start von MetaTrader 5 unter Windows zu erfassen. Eine Möglichkeit, die ich versucht habe, ist, MetaTrader 5 mit einer Konfigurationsdatei zu starten, um ein Skript beim Start auszuführen und dann die Ausgabe in eine Datei umzuleiten, aber der Versuch schlug fehl, da MetaTrader 5 es nicht erlaubt, eine DLL beim Start von der Befehlszeile zu laden. Um dies zu ändern, ohne den DLL-Hauptcode zu beeinträchtigen, werden wir eine kleine Anpassung des MQL5-Codes vornehmen, um die verstrichene Ausführungszeit von dort aus mit GetTickCount() zu berechnen.

   ...
   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 ---");
}

Beachten Sie die Zeile mit dem Kommentar „// *“. Das sind die wichtigsten zusätzlichen Zeilen, auf die Sie achten sollten. Es ist ganz einfach zu verstehen.

Testen wir es noch einmal.

TestConsumeDLL Windows Wiederholungstest Ergebnis wie auf der Registerkarte „Experten“ angezeigt

Aktualisierter MQL5-Code zur Messung der verstrichenen Ausführungszeit, wie unter Windows getestet.


Wir haben die gesamte Proof-of-Concept-Anwendung in Multi-Threading-fähige DLL, dann verbrauchen sie in MQL5-Code, getestet auf beiden Linux-und Windows-System, alle gestartet und entwickelt auf Linux. Alles funktioniert wie erwartet mit dem erwarteten Ergebnis.


Einfacher Benchmark der beiden Mingw-Threading-Implementierungen

Wir werden den Benchmark auf einfache Art und Weise durchführen, und zwar auf der Grundlage unseres Proof-of-Concept-Programms. Um einen vollständigen Benchmark über die plattformübergreifende Multithreading-Fähigkeit von C++ durchzuführen, sind mehrere Faktoren zu berücksichtigen, insbesondere mehrere Synchronisationsprimitive, thread_local, Problemdomäne usw.

Den Benchmark führen wir folgendermaßen durch:

  • Linux
    • Bauen Sie per Makefile und führen Sie das Ergebnis 5 Mal durch, bevor Sie den Mittelwert bilden, und machen Sie dasselbe für Makefile-th_win32
    • Führen Sie die Binärdatei mit WINEPREFIX=~/.mt5 wine main.exe aus
    • Nutzung aller 12 Threads und des gesamten verfügbaren RAM ab 32 GB
  • Windows
    • Erzeugen Sie mit Makefile und führen Sie das Ergebnis 5 Mal durch, bevor Sie den Durchschnitt ermitteln, und machen Sie dasselbe für Makefile-th_win32
    • Kopieren der erforderlichen DLLs und ausführbaren Dateien in den Gastcomputer (Windows) über Virtualbox
    • Führen Sie die Binärdatei über die Eingabeaufforderung von main.exe aus
    • Begrenzt auf 6 Threads und mit 20 GB RAM (beides aufgrund der Einhaltung gültiger Konfigurationen in Virtualbox)

Die Ergebniszahlen wurden auf 2 Dezimalstellen gerundet.

Das Ergebnis ist in der folgenden Tabelle dargestellt.

Funktion  Linux + pthread (ms) Linux + win32-Thread (ms) Windows + pthread (ms) Windows + win32-Thread (ms)
 single_threaded_sum 417.53
417.20
467.77
475.00
 multi_threaded_sum_v2  120.91  122.51  121.98  125.00


Schlussfolgerung

Mingw und Wine sind die plattformübergreifenden Tools, die es Entwicklern ermöglichen, mit Linux plattformübergreifende Anwendungen zu entwickeln, die sowohl unter Linux als auch unter Windows problemlos funktionieren. Dies gilt auch für den Fall, dass für die MT-Plattform entwickelt wird. Mit unserer Proof-of-Concept-Anwendung zur Entwicklung einer C++-Multithreading-fähigen DLL, die sowohl unter Linux als auch unter Windows getestet wurde, bietet sie alternative Optionen zur Erweiterung der Reichweite von Entwicklern im Ökosystem.



Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/12042

Beigefügte Dateien |
ExampleLib.zip (5.37 KB)
Algorithmen zur Optimierung mit Populationen Optimierung gemäß einer bakteriellen Nahrungssuche (BFO) Algorithmen zur Optimierung mit Populationen Optimierung gemäß einer bakteriellen Nahrungssuche (BFO)
Die Strategie der Nahrungssuche des Bakteriums E. coli inspirierte die Wissenschaftler zur Entwicklung des BFO-Optimierungsalgorithmus. Der Algorithmus enthält originelle Ideen und vielversprechende Optimierungsansätze und ist es wert, weiter untersucht zu werden.
Das Murray-System neu überdenken Das Murray-System neu überdenken
Grafische Preisanalysesysteme sind bei den Händlern zu Recht sehr beliebt. In diesem Artikel beschreibe ich das komplette Murray-System, einschließlich seiner berühmten Level, sowie einige andere nützliche Techniken, um die aktuelle Kurslage zu bewerten und eine Handelsentscheidung zu treffen.
Neuronale Netze leicht gemacht (Teil 33): Quantilsregression im verteilten Q-Learning Neuronale Netze leicht gemacht (Teil 33): Quantilsregression im verteilten Q-Learning
Wir setzen die Untersuchung des verteilten Q-Learnings fort. Heute wollen wir diesen Ansatz von der anderen Seite her betrachten. Wir werden die Möglichkeit prüfen, die Quantilsregression zur Lösung von Preisvorhersageaufgaben einzusetzen.
Kategorientheorie in MQL5 (Teil 2) Kategorientheorie in MQL5 (Teil 2)
Die Kategorientheorie ist ein vielfältiger und expandierender Zweig der Mathematik, der in der MQL-Gemeinschaft noch relativ unentdeckt ist. In dieser Artikelserie sollen einige der Konzepte vorgestellt und untersucht werden, mit dem übergeordneten Ziel, eine offene Bibliothek einzurichten, die zu Kommentaren und Diskussionen anregt und hoffentlich die Nutzung dieses bemerkenswerten Bereichs für die Strategieentwicklung der Händler fördert.