English Русский 中文 Español Deutsch Português
preview
Linux上のMetaTrader 5のC++マルチスレッドサポートを備えた概念実証DLLを開発する

Linux上のMetaTrader 5のC++マルチスレッドサポートを備えた概念実証DLLを開発する

MetaTrader 5 | 9 2月 2023, 16:07
533 0
Wasin Thonkaew
Wasin Thonkaew

はじめに

Linuxには、力強い開発エコシステムと、ソフトウェア開発のための優れた人間工学があります。

これは、コマンドラインでの作業を楽しむ人にとって魅力的です。アプリケーションのインストールはパッケージマネージャーを使用して容易におこなわれ、OSはブラックボックスではないが内部について詳しく学びたくさせるようなものです。ほとんどすべてのサブシステム用に構成可能で、すぐに使用できる不可欠な開発ツールを備えるという、ソフトウェア開発に適した柔軟で合理化された環境です。

エンドユーザーのPCデスクトップから、VPSなどのクラウドソリューションやAWS、GoogleCloudなどのクラウドサービスプロバイダーまで、幅広く利用できます。

私は、自分が好きなOSに固執したいが、製品を開発してWindowsユーザーに提供したいと考えている開発者がいると強く信じています。もちろん、製品はプラットフォーム間でシームレスに同じように機能する必要があります。

通常、MetaTrader 5開発者は、MQL5プログラミング言語を使用して指標/エキスパートアドバイザー(EA)や関連製品を開発し、どのOSをベースにするかを気にせずにマーケットでエンドユーザーに公開します。配布前には、MT5のIDEを利用して.EX5実行可能ファイルのコンパイルとビルドをおこなうことができます(LinuxでMetaTrader 5を起動する方法を知っている場合)。
ただし、開発者が、MQL5プログラミング言語だけでは提供できない追加サービスをさらに拡張して提供するために、共有ライブラリ(DLL)としてカスタムソリューションを開発する必要がある場合、クロスコンパイルソリューションを探し、落とし穴やベストプラクティスを発見したりツールに慣れたりするのには、より多くの時間と労力を費やさなければなりません。

これが、この記事が登場した理由です。クロスコンパイルソリューションと、C++マルチスレッド対応のDLLを構築する機能を含めることで、これら2つを組み合わせることで、少なくとも、開発者がさらに拡張するためのベースとして使用できる基盤となります。
この記事が、読者が愛してやまないOSであるLinuxでMetaTrader 5関連の製品を開発し続けるのに役立つことを願っています。

この記事の対象者

まず、読者がコマンドラインからLinuxを使用した経験を持ち、LinuxでのC++ソースコードのコンパイルとビルドに関する一般的な概念をすでに理解していると想定しています。

とにかく、この記事は、Linuxで、マルチスレッド機能を備えてWindowsでも動作するDLLを開発するための手順とワークフローを調べたい方向けです。スレッド化プログラミングオプションを手元で拡張します。組み込みのOpenCLだけでなく、マルチスレッド機能を備えた、より柔軟で基本に戻る移植可能な C++ コードで、それにしっかりと基づいている他のいくつかのシステムと統合します。 

使用するシステムとソフトウェア

  • Ubuntu 20.04.3 LTSカーネルバージョン5.16.0、AMD Ryzen 5 3600 6コアプロセッサ(コアあたり2スレッド)、32GBのRAM
  • Wine(winehq-develパッケージ)バージョン8.0-rc3(この記事の執筆時点)(この記事がstableパッケージの代わりにdevelを使用することを決定した理由については、「winehq-stableパッケージで起動するとMT5ビルド3550がすぐにクラッシュした」も参照してください。)
  • MinGW(mingw-w64パッケージ)バージョン7.0.0-2
  • Windowsシステムでのテスト用のVirtualboxバージョン6.1

作戦

以下の計画を基本とします。

  1. Wineを学ぶ
  2. MinGWを学ぶ
  3. MinGWのスレッド化を実装する
    1. POSIX (pthread)
    2. Win32(mingw-std-threads使用)
  4. Linux開発マシンを準備する
    1. Wineをインストールする
    2. MetaTrader 5をインストールする
    3. MinGWをインストールする
    4. (オプション)mingw-std-threadsをインストールする
  5. 概念実証、開発フェーズI - DLL(C++マルチスレッドサポート)
  6. 概念実証、開発フェーズII - DLLを使用するMQL5コード
  7. Windowsシステムでテストする
  8. MinGWスレッド実装の簡単なベンチマーク


Wine

Wineは「Wine is Not an Emulator」の略(厳密には再帰的頭字語バクロニム)です。Wineはプロセッサやターゲットハードウェアをエミュレートするエミュレータではありません。代わりに、Windows以外のOSで動作するwin32APIのラッパーです。

Wineは、Windows以外のシステムのユーザーからのwin32APIへの呼び出しをインターセプトする別の抽象レイヤーを導入し、Wineの内部にルーティングしてから、Windowsで動作する必要がある場合と同じように(またはほぼ同じ方法で)要求を処理して動作させます。 

つまり、WineはPOSIX APIを使用してそれらのwin32 APIを動作させます。読者は、それと知らずにWineソフトウェアを使ってLinuxでそのようなWindowsソフトウェアを使用したり、Protonと呼ばれるWineのバリアントをランタイムベースとしてLinuxでストリームゲームをプレイしたりしたことがあるかもしれません。

これにより、Linuxには代替手段がないWindowsソフトウェアを柔軟にテストしたり使用したりできます。

通常、Wine経由でWindowsベースのアプリケーションを実行する場合は、次のコマンドを実行します。

wine windows_app.exe

または、特定のWine環境プレフィックスに関連付けながらアプリケーションを実行する場合は、次のようにします。

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe


MinGW

MinGWは「Minimalist GNU for Windows」の略です。これはGNUコンパイラコレクション(GCC)とそのツールチェーンの移植であり、Linuxで、WindowsをターゲットしたC/C++およびその他のプログラミング言語をコンパイルするために使用します。

機能、コンパイルフラグ/オプションは、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

違いが最小限であることにお気付きでしょう。コンパイルフラグは非常に似ており、ほとんど同じです。必要なものすべてをコンパイルしてビルドするために、異なるコンパイラバイナリを使用するだけです。

次のセクションで説明するスレッド実装のトピックにつながりますが、使用するバリアントは3つがあります。

  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
    win32APIスレッドモデルで動作することを目的としたバイナリ実行可能ファイル(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ベースOS用のパフォーマンス分析ツール
同様に、x86_64-w64-mingw32-gcc-nm-posixx86_64-w64-mingw32-gcc-nm-win32のバリアントもありますが、一部のものです。

MinGWスレッド実装

前のセクションから、MinGWが提供するスレッド実装には2つのバリアントがあることがわかりました。
  1. POSIX (pthread)
  2. Win32

なぜこれについてそれほど心配する必要があるのでしょうか。考えられる理由は2つあります。

  1. 安全性と互換性
    コードでC++マルチスレッド機能(例:std::threadstd::promiseなど)とOSのネイティブマルチスレッドサポート  CreateThread() (win32 API)またはpthread_create() (POSIX API)の両方を一緒に使用する可能性がある場合、他のAPIよりも1つのAPIを使用することに固執することが推奨されします。

    いずれにせよ、OSサポートAPIがC++にはない機能を備えているという非常に特殊な状況が発生しない限り、.C++のマルチスレッド機能を使用するコードとOSサポートを一緒に使用する可能性はほとんどありません。そのため、一貫性を維持し、両方に同じスレッドモデルを使用することをお勧めします。
    MinGWのpthread実装を使用する場合は、win32 APIのスレッド機能を使用しないようにしてください。同様に、MinGWのwin32スレッド実装(以降、簡単に「win32スレッド」と呼びます)を使用する場合は、OSのpthread APIを使用しない方がよいでしょう。

  2. パフォーマンス(後のMinGWスレッド実装の簡単なベンチマークセクションを参照)
    もちろん、ユーザーは低遅延のマルチスレッドソリューションを望んでいます。特定の状況でユーザーが使用することを選択する可能性が高いのは、より高速に実行できるソリューションです。

まず、両方のスレッド実装のベンチマークを実施する前に、概念実証のためのDLLとテストプログラムを開発します。

このプロジェクトでは、ビルドシステムが相互に簡単に切り替えることができるpthreadまたはwin32スレッドを使用する移植可能なコードを提供します。
win32スレッドを使用する場合、mingw-std-threadsプロジェクトからヘッダーをインストールする必要があります。


Linux開発マシンの準備

コーディング部分に飛び込む前に、まず必要なソフトウェアをインストールする必要があります。

Wineをインストールする

次のコマンドを実行して、Wine develパッケージをインストールします。

sudo apt install winehq-devel

次に、次のコマンドで正しく動作するかどうかを確認します。

wine --version

その出力は次のようになります。

wine-8.0-rc3


MetaTrader 5をインストールする

ほとんどのユーザーは、クラッシュの問題があるビルドであるビルド3550よりずっと前にMetaTrader 5をインストールしています。問題を解決するためにwinehq-develパッケージを使用するように切り替え、MetaTrader 5を起動できるようにするには、「Linuxでのプラットフォームのインストール」にある公式インストールスクリプトを直接使用することはできません。
公式のインストールスクリプトを直接実行すると、Wine stableパッケージに上書きされるため、自分でコマンドを実行することをお勧めします。

MT5 Build 3550 Broken Launching On Linux Through Wine.How To Solve?」でガイドラインを書きました。この記事では、Wine stableパッケージを既にインストールしているユーザー、またはdevelパッケージで新たに開始したいユーザーのすべてのケースをカバーします。

MetaTrader 5をWineからもう一度起動してみて、問題がないか確認してください。

メモ

公式のインストールスクリプトは~/.mt5にWine環境(プレフィックスと呼ばれます)を作成します。MetaTrader 5を簡単に起動するには、~/.bash_aliasesに

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

の行を含めて、

source ~/.bash_aliases

でsourceして、最終的に

mt5trader

コマンドを実行すると便利かもしれません。デバッグ出力がターミナルに表示されます。この方法でMetaTrader 5を起動すると、後でコードを複雑にすることなく、概念実証アプリケーションからデバッグログを簡単に確認できます。

MinGWをインストールする

次のコマンドを実行して、MinGWをインストールします。

sudo apt install mingw-w64

これにより、一連のツールがコレクションとしてシステムにインストールされ、これらのツールには接頭辞「x86_64-w64-mingw32-.」が付いています。ほとんどの場合、x86_64-w64-mingw32-g++-posix(または、win32スレッドを使用する場合はx86_64-w64-mingw32-win32)を使用します。

mingw-std-threadsをインストールする

mingw-std-threadsは、MinGWのwin32スレッドをLinuxで動作するように接着するプロジェクトです。ドロップインソリューションとしてのヘッダーのみです。そのため、インストールは簡単で、ヘッダーファイルをシステムのインクルードパスに配置するだけで済みます。

以下の手順に従ってインストールしてください。

まず、gitリポジトリをシステムにクローンします。

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

次に、システムのインクルードパスにヘッダーを保持するディレクトリを作成します。

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++マルチスレッドサポート)

それでは、コードに取り掛かりましょう。

ここでの目標は、C++11標準ライブラリのマルチスレッド機能を使用できる概念実証のDLLソリューションを実装して、読者がアイデアを得てさらに拡張できるようにすることです。

以下は、ライブラリとアプリケーションの構造です。

プロジェクトの構造

  • DLL
    • example.cpp
    • example.h
  • 消費者
    • main.cpp
  • ビルドシステム
    • Makefile -MinGWのpthreadを使用したクロスコンパイルビルドファイル
    • Makefile-th_win32 -MinGWのwin32スレッドを使用したクロスコンパイルビルドファイル 
    • Makefile-g++ - ネイティブLinuxでテストするためのビルドファイル(プロジェクトの開発中の迅速な反復とデバッグ用)

使用されるC++標準

主にC++11の機能を使用しますが、C++17標準を使用します。ただし、[[nodiscard]]のようなコード注釈の属性など、いくつかは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);
};

#pragmaonceはC++標準の一部ではありませんが、GCCでサポートされているため、MinGWもサポートしています。これは、重複したヘッダーのインクルードを防ぐための柔軟でより簡潔な方法です。
このようなディレクティブを使用しない場合、ユーザーは#ifdefと#defineの両方を使用し、各定義が各ヘッダーファイルに対して一意の名前を持っていることを確認する必要があるので、時間がかかる場合があります。

#ifdef WINDOWSを使用してEXAMPLE_API.定義宣言を保護します。これにより、MinGWとネイティブLinuxシステムでコンパイルできるようになります。したがって、共有ライブラリをクロスコンパイルする場合は常に、-DWINDOWS-DEXAMPLE_EXPORTの両方をコンパイルフラグに追加します。それ以外の場合は、テスト用のメインプログラムのみをコンパイルするので、-DEXAMPLE_EXPORTを削除できます。

__declspec(dllexport)は、DLLから関数をエクスポートするためのディレクティブです。

__declspec(dllimport)は、DLLから関数をインポートするためのディレクティブです。

WindowsでDLLを使用するには、上記の2つがコンパイルに必要です。Windows以外のシステムでは必要ありませんが、クロスコンパイルが必要です。したがって、Linux用にコンパイルするためのWINDOWSが定義されていない場合、EXAMPLE_APIは空になります。

次に、関数シグネチャについての非常に重要な部分です。関数シグネチャは、C(プログラミング言語)の呼び出し規約と互換性がある必要があります。
このextern "C"は、関数シグネチャがC++の呼び出し規約にマングル化されるのを防ぎます。

関数シグネチャをnamespace内にラップしたり、フリー関数として宣言したりすることはできません。これは、後でDLLを使用するときにMQL5コードがこれらのシグネチャを見つけることができないためです。

num_hardware_concurrency()の場合、実装でサポートされている同時スレッドの数を返します。
たとえば、私の場合、コアあたり2スレッドの6コアプロセッサを使用しているために同時に動作できるスレッドは事実上12で、12が返されます。

single_threaded_sum()multi_threaded_sum_v2()はどちらも、マルチスレッドを使用する利点を示し、2つの間のパフォーマンスを比較する概念実証アプリケーションの代表的な例です。

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スレッドの使用とリンクを切り替えることができます。クロスコンパイル時にwin32スレッドを使用するためには、コンパイルフラグに-DUSE_MINGW_STD_THREADを追加します。

シンプルなインターフェイスを簡単に実装します。

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()の場合、std::thread::hardware_concurrency()を使用するには、ヘッダー<thread>を含める必要があります。

次は、デバッグログユーティリティ関数です。

#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コードを使用した実際の使用法をシミュレートします。DLLが結果をMQL5コードに返す前に、MQL5がデータを配列としてDLL関数に送信して何かを計算する状況を想像してください。
それがこれです。ただし、この関数では、num_elemで指定された要素の総数に対して、指定された入力配列のすべての要素を1つずつ線形に反復します。

このコードは、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は、そのようなキャッシュラインを複数のスレッド間で共有するようにマークし、シグナリングと応答の待機のためにCPUサイクルを浪費しないためです。
以前のv1実装では、各スレッドにフィードするデータセットのコピーを作成するために全力を尽くしましたが、最近のプロセッサがすでにMESIを使用しているという前述の理由によると、そのような試みをソースコードに含める必要はありません。v1はv2よりも2~5倍遅くなります。

データの元の配列と操作するデータの範囲(開始インデックスと終了インデックスのペア)で機能するラムダ関数であるworker_funcに注意してください。ループ内のすべての要素をローカル変数に合計して、偽共有の問題を回避します。これにより、パフォーマンスが大幅に低下する可能性があり、最終的にすべてのスレッドで共有合計変数がアトミックに追加されます。std::atomicを使用して、スレッドセーフにするのに役立ちます。このような共有合計変数を変更する必要がある回数は最小限であるため、パフォーマンスに大きな影響はありません。実装の実践とスピードの向上のバランスをとることが、進むべき道です。

作業を分割するために必要なスレッドの数を計算するため、後で各スレッドの作業範囲がわかります。std::hardware_concurrency()は0を返す可能性があることに注意してください。これは、スレッド数を特定できない可能性があることを意味します。したがって、そのような場合も処理し、2にフォールバックします。

次に、スレッドのベクトルを作成します。その容量をnum_max_threadsまで予約します。次に、処理する各スレッドのデータセットの範囲を繰り返し計算します。最後のスレッドでは、残りのすべてのデータが必要になることに注意してください。これは、ほとんどの場合、実行される作業の要素の数が、使用する計算スレッドの数で割り切れない可能性があるためです。

重要なのは、すべてのスレッドを結合することです。より複雑な状況では、MQL5コードが結果を待つのをブロックしない非同期環境が必要になる場合があります。そのため、通常std::async、std::promise、std::packaged_taskのすべてのベースであるstd::futureを使用します。 そのため、通常、少なくとも2つのインターフェイスがあります。1つはMQL5コードからデータを送信してブロックせずにDLLで計算するためのリクエストを作成し、もう1つはそのようなリクエストからの結果をオンデマンドで受け取り、その時点で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()を使用して、結果が正しいことを検証します。

次に、共有ライブラリ(ネイティブLinuxのlibexample.soとして)とメインのテストプログラム(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++17標準のC++構文に基づくようにコンパイラに指示する
-Wall コンパイル時にすべての警告を出力するように指示する
-Wextra コンパイル時に追加の警告を出力するように指示する
-fno-rtti 最適化の一環としてRTTI(ランタイムタイプ情報)を無効にする
RTTIを使用すると、パフォーマンスコストが発生するため必要のないオブジェクトのタイプをプログラムの実行中に決定できる
-O2 レベル1に加えてより積極的な最適化を含む最適化レベル2を有効にする
-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には3つのバリアントがあります。

  1. Makefile
    LinuxとWindowsの両方のクロスコンパイル用です。pthreadを使用します。これを使用して、Wine

    経由で起動できるメインのテストプログラムに加えて、MetaTrader 5で動作するDLLをビルドします。
  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

上記のMakefileの3つのバリアントから、ほとんどすべてのコードを共有していますが、いくつかの最小限の違いがあります。

特に以下の違いについて少し時間をかけて確認してください。

  • コンパイラ バイナリの名前
  • -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-fMakefile-th_win32  make -f Makefile-th_win32 clean
ネイティブLinux make-fMakefile-g++  make -f Makefile-g++ clean

MetaTrader 5とWineで使用するDLLをビルドしましょう。両方をテストできます。

次を実行します。

$ 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
    WindowsシステムでネイティブにサポートされていないC++例外処理およびその他の低レベル機能のサポートを提供するために使用される

  • libstdc++6.dll
    C++プログラムを提供する基盤。入出力、数学演算、メモリ管理などのさまざまな操作を実行するために使用される関数とクラスが含まれている

  • libwinpthread-1.dll
    Windows用のpthreadAPIの実装。
    ターミナル出力に表示されない場合があるが、前述の2つのDLLからのDLL依存関係

MinGWをインストールしたので、これらのDLLは既にLinuxシステムに存在します。見つけるだけです。
それには次を使用します。

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

このコマンドはlibgcc_s_seh-1.dllを探します。ルートディレクトリから検索を開始して(/)、ディレクトリを無視します(-typef)。エラーが発生した場合は/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でビルドする場合は、win32ベースのディレクトリからDLLをコピーします。

主に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 - DLLを使用するMQL5コード

MQL5コード開発のフェーズIIに到達するまでの道のりは長いです。

以下は、スクリプトとしての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コードをEAまたは指標として作成できますが、この概念実証作業では、すべてのステップとワークフローをテストするためにヒットアンドランを進めます。スクリプトはここでのニーズに適しています。
とにかく、現実の状況では、通常、ターミナルからデータを取得するためにEAまたは指標が必要です(OnTick()、OnTrade()、OnCalculate())。MTプラットフォームの各タイプのプログラムでサポートされている関数の詳細については、「プログラムの実行」をご覧ください。

それでは、上記の完全なコードをブロックごとに分析してみましょう。

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コードにはありません。
    MQL5で使用されるC++構文と実際のC++自体は100%同じではないことに注意してください。つまり、MQL5で配列を渡すたびに、&を追加する必要があります。

大規模なデータセットの配列を作成します。

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

これにより、1000M要素の整数の配列が作成され、各要素に値1が設定されます。配列は動的になり、ヒープ上に存在します。スタックには、そのような膨大な量のデータサイズを保持するのに十分なスペースがありません。
配列を動的にするには、intarr[]の宣言構文を使用します。

その後、必要に応じて、宣言されたシグネチャから各DLL関数を呼び出すだけです。また、結果を確認して出力を検証し、正しくない場合はAlert()でユーザーに通知することにも注意してください。すぐには終了しません。

ArraySize()を使用して、配列の要素数を取得します。配列を関数に渡すには、その変数を関数に直接渡します。

スクリプトをコンパイルすれば、実装は完了です。


必要なすべてのDLLをMetaTrader 5にコピーする

MQL5スクリプトを起動しようとする前に、必要なすべてのDLLを<ターミナルl>/Librariesディレクトリにコピーする必要があります。通常、フルパスは~/.mt5/drive_c/ProgramFiles/MetaTrader 5/MQL5/Librariesです。
MetaTrader 5はここで、MetaTrader 5用に構築したプログラムによって必要とされる必要なDLLを探します。クロスコンパイルされた実行可能ファイルの実行をテストするセクションに戻り、コピー先のDLLのリストを確認します。

デフォルトでは、MetaTrader 5の公式インストールスクリプトは、Wineをプレフィックス~/.mt5に自動的にインストールします。これは、公式のインストールスクリプトを使用するユーザーにのみ適用されます。


テスト

コンパイル済みのTestConsumeDLLをチャートにドラッグアンドドロップする

コンパイルされたTestConsumeDLLをグラフにドラッグアンドドロップして、実行を開始する

最初にLinux上のWineを介してMetaTrader 5を起動してテストします。
コンパイルされたTestConsume DLLをチャートにドラッグアンドドロップします。次に、DLLからインポートする許可を求めるダイアログと、構築したMQL5プログラムのDLL依存関係のリストが表示されます。

DLLの依存関係のリストとともに、DLLのインポート許可を求めるダイアログ

DLLの依存関係のリストとともに、DLLのインポート許可を求めるダイアログ

libwinpthread-1.dllはコンパイルされたMQL5スクリプトの直接の依存関係ではないので表示されていませんが、libgcc_s_seh-1.dlllibstdc++6.dll両方の依存関係です。objdumpを使用して、次のようにターゲットDLLファイルのDLL依存関係を確認できます。

$ 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フラグは、すべてのヘッダーの内容を表示することを意味します。

[エキスパート]タブで結果出力を確認します。

TestConsume DLLの実行から[エキスパート]タブに表示される出力

TestConsume DLLの実行から[エキスパート]タブに表示される出力


次は、LinuxでMetaTrader 5を起動するために使用された元のターミナルウィンドウでの各関数の実行経過時間の結果です。

関数ごとのコンソール出力の実行経過時間

MetaTrader 5の起動に使用された同じターミナルウィンドウでDLLからの実行出力の経過時間を確認できる

Alerts()からのアラートが表示されない限り、実行の経過時間はかなり適切です。その後、すべて問題なく、この概念実証プログラムのすべてをほぼ完了します。


Windowsシステムでテストする

以下が必要です。

  • GuestAdditionsがインストールされたVirtualbox
    Linuxシステムにインストールする方法については、インターネットで調べてください。よりよい情報が他の複数の情報源ですでに包括的に提供されています。ここにそのような情報を網羅的に含めて記事を無駄に長くしないことにします。
    重要:ホストとゲストマシン間でデータを共有する機能を使用するには、Guest Additionsが必要なので、他のDLLとまとめてexample.dllをゲストマシ(Windows)にコピーしてください。

  • Windows 7+64ビットISOイメージ
    Virtualboxを介してハードドライブにロードおよびインストールされます。

Virtualboxのメインインターフェイス

Virtualboxのメインインターフェイス - ハードウェアリソースの可用性に依存(DLLの実行速度をテストする必要がある場合は多いほどいい)


また、DLLからの実行速度をテストする場合に、Virtualboxを介して起動するWindowsシステムに割り当てることができるマシンリソースの多さと可用性にも依存します。私の構成は次の通りです。

  • [System]->[Motherboard]->[Base Memory]を20480 MBまたは20 GBに設定(ホストマシンは32G B)
  • [System]->[Processor]->[Processor(s)]を6に設定し、[Execution Cap]を100%に設定(6が最大許容値であるため、12に設定できない)
  • [Display]->[Screen]->[Video Memory]を最大に設定(必ずしも必要ではありませんが、すべてのモニターを利用したい場合に備えます。モニターが多いほど、より多くのビデオメモリが必要です)
  • [Display]->[Screen]->[Monitor Count]を1に設定

いよいよテストです。コンパイルされたMQL5コードをLinuxマシンからコピーするか、すべてのコードをコピーしてから、MetaEditorを使用してWindowsマシンで再度コンパイルすることができます。
後者のオプションでまったく問題ないことがわかりました。もう一度コピーして貼り付けるだけです。私は後者にしました。

テスト結果

Windowsの[エキスパート]タブでのTestConsumeDLL出力結果

Windowsテストで[エキスパート]タブに表示される結果出力


問題は、実行の経過時間が標準出力(stdout)を介して出力されるようにコード化されており、WindowsでMetaTrader 5を起動してそのような出力をキャプチャする方法が見つからないことです。私が試した1つの方法は、構成ファイルを使用して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 ---");
}

「//*」というコメントのある行に注目してください。これは、注目すべき主な追加の行です。理解するのは簡単です。

では、再テストしてみましょう。

[エキスパート]タブに表示されるWindowsでのTestConsumeDLL再テスト結果]タブに表示されるWindowsでのTestConsumeDLL再テスト結果

Windowsでテストされた経過実行時間を測定するようにMQL5コードを更新


マルチスレッド対応のDLLを作成して概念実証アプリケーション全体を完成させ、それをMQL5コードで使用し、LinuxとWindowsシステムの両方でテストし、すべてLinuxで開始および開発しました。すべてが期待どおりに機能し、期待どおりの結果が得られます。


両方のMinGWスレッド実装の単純なベンチマーク

概念実証プログラムに基づいて、具体的に簡単な方法でベンチマークを実施します。プラットフォーム全体でC++マルチスレッド機能の完全なベンチマークを実施するには、特に複数の同期プリミティブ、thread_local、問題始域など、考慮すべき要素が複数あるためです。

ベンチマークの実施方法は次のとおりです。

  • Linux
    • Makefileを使用して5回ビルドして結果を平均化します。Makefile-th_win32についても同じことをおこないます。
    • WINEPREFIX=~/.mt5 wine main.exeでバイナリファイルを実行します。
    • 12個のスレッドをフルに使用し、32 GBの使用可能なすべてのRAMを使用します。
  • Windows
    • Makefileを使用して5回ビルドして結果を平均化します。Makefile-th_win32についても同じことをおこないます。
    • Virtualboxを使用して、必要なDLLと実行可能ファイルをゲストマシン(Windows)にコピーします。
    • コマンドプロンプトでmain.exeバイナリファイルを実行します。
    • 6スレッドの上限と20 GBのRAM(どちらもVirtualboxの有効な構成に準拠するため)

結果の数値は、小数点以下2桁で四捨五入されます。

結果を次の表に示します。

関数  Linux+pthread(ミリ秒) Linux+win32スレッド(ミリ秒) Windows+pthread(ミリ秒) Windows+win32スレッド(ミリ秒)
 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のどちらでもシームレスに動作するクロスプラットフォームアプリケーションを開発するためのクロスプラットフォームツールです。MTプラットフォーム向けに開発する場合も同様です。C++マルチスレッド対応のDLLを開発するための概念実証アプリケーションをLinuxとWindowsの両方でテストし、開発者からエコシステムへのリーチを拡大するための代替オプションを提供します。



MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/12042

添付されたファイル |
ExampleLib.zip (5.37 KB)
行列ユーティリティ - 行列とベクトルの標準ライブラリの機能を拡張する 行列ユーティリティ - 行列とベクトルの標準ライブラリの機能を拡張する
行列は大規模な数学的演算を効率的に処理できるため、機械学習アルゴリズムや一般的なコンピュータの基盤となっています。標準ライブラリは必要なものをすべて備えていますが、ユーティリティファイルでライブラリにはまだないいくつかの関数を導入して、拡張する方法を見てみましょう。
知っておくべきMQL5ウィザードのテクニック(第05回):マルコフ連鎖 知っておくべきMQL5ウィザードのテクニック(第05回):マルコフ連鎖
マルコフ連鎖は、金融をはじめとする様々な分野で、時系列データのモデル化や予測に利用できる強力な数学的ツールです。金融の時系列モデル化や予測では、株価や為替レートなど、金融資産の時間的変化をモデル化するためにマルコフ連鎖がよく使われます。マルコフ連鎖モデルの大きな利点の1つは、そのシンプルさと使いやすさにあります。
DoEasy - コントロール(第30部):ScrollBarコントロールのアニメーション化 DoEasy - コントロール(第30部):ScrollBarコントロールのアニメーション化
今回は、ScrollBarコントロールの開発の続きと、マウスインタラクション機能の実装を開始します。さらに、マウスの状態フラグやイベントのリストも充実させる予定です。
MQL5の圏論(第2回) MQL5の圏論(第2回)
圏論は数学の一分野であり、多様な広がりを見せていますが、MQL5コミュニティではまだ比較的知られていません。この連載では、その概念のいくつかを紹介し、考察することで、コメントや議論を呼び起こし、トレーダーの戦略開発におけるこの注目すべき分野の利用を促進することを目的としたオープンなライブラリを確立することを目指しています。