English Русский 中文 Español Deutsch 日本語
preview
Desenvolvimento de uma DLL experimental com suporte a multithreading em C++ para MetaTrader 5 no Linux

Desenvolvimento de uma DLL experimental com suporte a multithreading em C++ para MetaTrader 5 no Linux

MetaTrader 5Exemplos | 10 maio 2023, 09:36
291 0
Wasin Thonkaew
Wasin Thonkaew

Introdução

O Linux oferece um ecossistema dinâmico e ergonomia excelente para o desenvolvimento de software.

É especialmente adequado para aqueles que gostam de trabalhar com a linha de comando e desejam instalar aplicativos facilmente por meio do gerenciador de pacotes. Além disso, o sistema operacional não é uma caixa preta, mas é interessante dominá-lo, já que é altamente configurável para quase todos os subsistemas, e inclui ferramentas integradas e um ambiente de desenvolvimento de software flexível e simplificado.

É possível encontrar o Linux em diferentes soluções, seja na área de trabalho ou como uma opção em nuvem, como um servidor virtual privado (VPS) ou provedores de serviços em nuvem, como AWS e Google Cloud.

Muitos desenvolvedores se apegam a um determinado sistema operacional, mas ainda desejam criar produtos que possam ser utilizados por usuários do Windows. Nesse sentido, é importante garantir que os produtos funcionem igualmente bem em diferentes plataformas.

Normalmente, os desenvolvedores criam seus indicadores, robôs e produtos relacionados em MQL5 e os publicam no Market, sem se preocupar com o sistema operacional. Eles podem confiar no MetaTrader 5 Interactive Development Environment para compilar e construir o executável .EX5, desde que saibam como executar o MetaTrader 5 no Linux.
No entanto, quando os desenvolvedores precisam criar uma solução de biblioteca compartilhada (DLL) para ampliar e criar opções adicionais, eles podem gastar mais tempo e esforço para encontrar soluções de compilação cruzada, descobrir armadilhas, aprender as melhores práticas e familiarizar-se com as ferramentas.

Por essa razão, escrevi este artigo. A compilação cruzada e a capacidade de criar DLLs thread-safe em C++ são pontos de partida para uma exploração mais aprofundada do assunto.
Espero que este artigo ajude você a continuar desenvolvendo produtos relacionados ao MetaTrader 5 no Linux.

Para quem é este artigo?

Presumo que os leitores já possuam alguma experiência com o Linux por meio da linha de comando e um entendimento básico de compilação e criação de código-fonte C++ no Linux.

Este artigo é para aqueles que querem aprender como desenvolver uma DLL multi-threaded que funcione tanto no Linux quanto no Windows. Você ampliará seus recursos de multithreading usando não apenas o OpenCL integrado, mas também o código C++ móvel flexível básico com capacidade de multithreading para integração com outros sistemas intimamente relacionados. 

SO e software necessários

  • Ubuntu 20.04.3 LTS com kernel versão 5.16.0 em um processador AMD Ryzen 5 3600 de 6 núcleos (2 threads por núcleo) e 32 GB de RAM
  • Wine (pacote winehq-devel) 8.0-rc3, que é recomendado em vez do pacote estável Veja também o post do blog "O MT5 build 3550 trava imediatamente na inicialização com o pacote winehq-stable" (em inglês) para entender por que escolhemos essa opção
  • Mingw (pacote mingw-w64) 7.0.0-2
  • VirtualBox 6.1 para testes no Windows

Esboço do artigo

Seguiremos o seguinte plano:

  1. Introdução ao Wine
  2. Introdução ao Mingw
  3. Threads Mingw
    1. POSIX (pthread)
    2. Win32 (via mingw-std-threads)
  4. Preparando o ambiente de desenvolvimento no Linux
    1. Instalando o Wine
    2. Instalando o MetaTrader 5
    3. Instalando o Mingw
    4. (Opcional) Instalando o mingw-std-threads
  5. Experimento, fase de desenvolvimento I - DLL (suporte a multithreading C++)
  6. Experimento, fase de desenvolvimento II - código MQL5 para usar a DLL
  7. Testando no Windows
  8. Teste simples de implementação de threads de Mingw


Wine

O nome Wine é um acrônimo recursivo retroacrônimo, que significa Wine is Not an Emulator (Wine não é um emulador). Na realidade, ele não emula nenhum processo ou equipamento, mas sim age como um wrapper de API win32 para sistemas operacionais não Windows.

O Wine adiciona uma camada abstrata adicional que intercepta chamadas de API win32 de usuários em sistemas não Windows. Essas chamadas são redirecionadas para componentes internos do Wine e são manipuladas de forma semelhante (ou quase semelhante) ao que ocorre no Windows. 

Para usar a API win32, o Wine trabalha com POSIX. Isso permite que os usuários executem aplicativos do Windows no Linux sem saberem que estão fazendo isso e até mesmo joguem jogos de sua biblioteca Steam no Linux, pois o ambiente de execução é baseado em uma variante do Wine chamada Proton.

Essa flexibilidade oferece aos usuários a capacidade de testar ou utilizar aplicativos do Windows que não têm equivalentes no Linux.

Normalmente, ao executar um aplicativo do Windows pelo Wine, usamos o seguinte comando:

wine windows_app.exe

Se quisermos executar um aplicativo associado a um prefixo de ambiente Wine específico, digitamos:

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe


Mingw

Mingw é uma sigla para "Minimalist GNU for Windows". Trata-se de uma porta da GNU Compiler Collection (GCC) e de suas ferramentas, que são utilizadas para compilar C/C++ e outras linguagens de programação específicas do Windows no Linux.

É importante destacar que o GCC e o Mingw disponibilizam funções e opções de compilação similares, o que facilita a adaptação de usuários que já conhecem o GCC para o Mingw. Além disso, o GCC e o Clang possuem sinalizadores/opções de compilação bastante parecidos, possibilitando que usuários migrem para um novo ambiente com facilidade e expandam sua base de usuários, incluindo os do Windows.

Para exemplificar as diferenças entre essas ferramentas, apresentamos a seguir uma tabela comparativa.

  • Compilação do código-fonte C++ e criação de uma biblioteca compartilhada
Compilador Linha de comando
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

  • Compilação de código-fonte C++ e criação de um arquivo binário executável
Compilador Linha de comando
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

Conforme a tabela, é possível perceber que as diferenças são mínimas. Os sinalizadores de compilação são muito semelhantes, praticamente iguais. A única discrepância está no binário do compilador para a construção do que é necessário.

No que diz respeito aos casos de uso, abordaremos mais detalhes na seção sobre a implementação de multithreading.

  1. x86_64-w64-mingw32-g++
    Nome x86_64-w64-mingw32-g++-win32.

  2. x86_64-w64-mingw32-g++-posix
    Executável binário projetado para funcionar com pthread.

  3. x86_64-w64-mingw32-g++-win32
    Arquivo executável binário projetado para funcionar com o modelo de threading da API win32. Nome 86_64-w64-mingw32-g++.

Além disso, existem várias outras ferramentas prefixadas com 

x86_64-w64-mingw32-...

Alguns exemplos:

  • x86_64-w64-mingw32-gcc-nm - mudança de nome
  • x86_64-w64-mingw32-gcc-ar - gerenciamento de arquivo
  • x86_64-w64-mingw32-gcc-gprof - análise de desempenho de sistemas operacionais do tipo Unix
Há também x86_64-w64-mingw32-gcc-nm-posix x86_64-w64-mingw32-gcc-nm-win32 .

Implementando threads do Mingw

Sabemos da seção anterior que existem duas maneiras para implementar threads oferecidos pelo Mingw.
  1. POSIX (pthread)
  2. Win32

Por que devemos nos importar? Posso pensar em dois motivos:

  1. Segurança e compatibilidade
    Se o seu código potencialmente usa os recursos multithreading do C++ (por exemplo, std::thread, std::promise, etc.), bem como suporte integrado para multithreading do sistema operacional, por exemplo CreateThread() para API win32 e pthread_create() para APIs POSIX, é melhor usar apenas uma API específica.

    De qualquer forma, é improvável que misturemos código usando multithreading C++ e suporte a SO, exceto em situações muito específicas em que a API de suporte a SO oferece mais recursos do que C++. Portanto, é melhor ser consistente e usar um modelo de threading.
    Ao usar a implementação pthread, tente não usar os recursos de encadeamento da API win32 e vice-versa.

  2. Performance (mais detalhes em "A Simple Test of Mingw Threads Implementation")
    Obviamente, os usuários desejam uma solução multithread e de baixa latência. Via de regra, os usuários escolhem aplicativos com execução mais rápida.

Primeiro desenvolveremos nosso exemplo de DLL e programa de teste e, em seguida, testaremos ambas as implementações de encadeamento.

Para o projeto, estamos usando código móvel para usar pthread ou thread win32. Nosso sistema de construção pode facilmente mudar de um sistema para outro.
Ao usar um thread win32, você precisa instalar os cabeçalhos damingw-std-threads project.


Preparando o ambiente de desenvolvimento no Linux

Antes de ir diretamente para o código, precisamos instalar o software necessário.

Instalando o Wine

Execute o seguinte comando para instalar o pacote de desenvolvimento do Wine.

sudo apt install winehq-devel

Em seguida, verifique se funciona corretamente com o seguinte comando:

wine --version

A resposta deve ser algo assim

wine-8.0-rc3


Instalando o MetaTrader 5

A maioria dos usuários instalou o MetaTrader 5 muito antes da falha do build 3550. Não podemos implementar o script de instalação oficial para usar o pacote winehq-devel e executar o MetaTrader 5 conforme descrito na no"Instalação no Linux".
É melhor que os comandos sejam executados por conta própria, pois a execução direta do script de instalação oficial substituirá nosso Wine de volta ao pacote estável.

Eu escrevi o guia MT5 Build 3550 Broken Launching On Linux Through Wine. How To Solve? (em inglês) em meu blog. Este artigo é destinado a usuários que já instalaram o pacote estável do Wine, bem como para aqueles que desejam recomeçar com o pacote devel.

Depois de tudo feito, tente iniciar o MetaTrader 5 através do Wine novamente e certifique-se de que está tudo em ordem.

Observação

O script de instalação oficial cria o ambiente Wine (chamado usando o prefixo) na rota ~/.mt5. Eu recomendo adicionar a seguinte linha a ~/.bash_aliases para iniciar facilmente o MetaTrader 5.

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

Em seguida, deve ser usada com

source ~/.bash_aliases

Por fim, execute o seguinte comando para iniciar o MetaTrader 5. Sua depuração também será mostrada no terminal.

mt5trader

Esta inicialização do MetaTrader 5 nos permitirá posteriormente ver o log de depuração de nosso aplicativo experimental sem a necessidade de complicar o código.

Instalando o Mingw

Execute o seguinte comando para instalar o Mingw.

sudo apt instalar mingw-w64

Como resultado, instalaremos no sistema um pacote de ferramentas com o prefixo x86_64-w64-mingw32-. Basicamente, trabalharemos com x86_64-w64-mingw32-g++-posix ou x86_64-w64-mingw32-win32 no caso de usar um thread win32.

Instalando mingw-std-threads

mingw-std-threads é um projeto que agrupa um thread win32 para rodar no Linux. A solução é usada para trabalhar com cabeçalhos. A instalação é bastante simples e requer apenas colocar o arquivo de cabeçalho no caminho include.

Siga os passos abaixo para instalar.

Primeiro, clone o repositório git em seu sistema.

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

Em seguida, crie um diretório para armazenar seu cabeçalho no caminho include.

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

Por fim, copie todos os arquivos de cabeçalho (.h) do diretório do projeto clonado para o diretório recém-criado.

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

Isso é tudo. Então, no código, se decidirmos usar um threat win32, para alguns dos arquivos de cabeçalho relacionados a encadeamento (como threads, primitivas de sincronização etc.), precisaremos incluí-lo no caminho apropriado com substituição de nome. A lista completa é mostrada na tabela abaixo.

Inclusão de arquivo de cabeçalho C++11 multithread Inclusão de arquivo de cabeçalho 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>


Experimento, fase de desenvolvimento I - DLL (suporte a multithreading C++)

Agora é hora de passar para o código.

Nosso objetivo é implementar uma solução de DLL experimental que possa aproveitar os recursos multithreading da biblioteca padrão C++11 para entender a ideia e continuar nossa exploração.

Abaixo está nossa biblioteca e estrutura de implementação.

Estrutura do Projeto

  • DLL
    • example.cpp
    • example.h
  • Usuário
    • main.cpp
  • Compilação
    • Makefile - arquivo de compilação cruzada usando pthread
    • Makefile-th_win32 - arquivo de compilação cruzada usando o thred win32 
    • Makefile-g++ - arquivo de compilação para teste no Linux nativo. Projetado para rápida iteração e depuração durante o desenvolvimento do projeto.

Padrão C++ aplicado

Embora usemos principalmente o padrão C++11, às vezes precisaremos usar elementos do padrão C++17, como o atributo de anotação de código [[nodiscard]].

DLL

example.h

#pragma once

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

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

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

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

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

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

Embora #pragma once não faça parte do padrão C++, ele é suportado pelo GCC e, portanto, pelo Mingw. Essa é uma maneira flexível e mais curta de evitar a inclusão de cabeçalhos duplicados.
Sem essa diretiva, os usuários usariam #ifdef e #define e precisariam garantir que cada definição tivesse um nome exclusivo para cada arquivo de cabeçalho. Isso levaria muito tempo.

Temos #ifdef WINDOWS para manter a declaração de definição EXAMPLE_API. Isso nos permite compilar com Mingw e Linux nativo. Assim, sempre que quisermos fazer uma compilação cruzada em uma biblioteca compartilhada, adicionamos -DWINDOWS e-DEXAMPLE_EXPORT ao sinalizador de compilação. Se estivermos compilando apenas para testar o programa principal, podemos omitir -DEXAMPLE_EXPORT.

__declspec(dllexport) - diretiva para exportar uma função a partir de uma DLL

__declspec(dllimport) - diretiva para importar uma função a partir de uma DLL.

As diretivas especificadas são necessárias para que a compilação funcione com DLLs no Windows. Elas não são necessárias para sistemas não Windows, mas ainda são necessárias para compilação cruzada. Portanto, o valor é deixado em branco para EXAMPLE_API na ausência de uma definição do WINDOWS para compilação no Linux.

As assinaturas de função devem ser compatíveis com a convenção de chamada C.
Esse "C" superficial impedirá que as assinaturas de função sejam adulteradas nas convenções de chamada do C++.

Não podemos agrupar assinaturas de função dentro de um namespace ou declará-las como funções livres porque o código MQL5 não será capaz de encontrar essas assinaturas quando usarmos a DLL posteriormente.

Para num_hardware_concurrency() ele retornará o número de threads simultâneos suportados pela implementação.
Por exemplo, estou usando um processador de 6 núcleos com 2 threads por núcleo, portanto, na verdade, ele possui 12 threads que podem ser executados ao mesmo tempo. No meu caso, 12 é retornado.

single_threaded_sum() e multi_threaded_sum_v2() oferecem uma ideia visual dos benefícios do multithreading e permitem que você compare o desempenho.

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

Acima está todo o código. Analisaremos cada parte separadamente para facilitar as coisas.

É possível alternar entre pthread e win32.

#include "example.h"

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

Essa configuração permite uma boa integração em nosso sistema de compilação para alternar entre pthread e win32. Adicionar -DUSE_MINGW_STD_THREAD ao sinalizador de compilação permite que o thread win32 seja usado durante a compilação cruzada.

Apresentamos interfaces simples.

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() e sub() são fáceis de entender. Para num_hardware_concurrency() precisamos incluir o cabeçalho <thread> para usar std::thread::hardware_concurrency().

Função de utilitário de log de depuração.

#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

Uma vez adicionado -DENABLE_DEBUG ao sinalizador de compilação, habilitamos o log de depuração no console. É por isso que sugiro iniciar o MetaTrader 5 via linha de comando para que possamos depurar nosso programa adequadamente.
Caso não definamos, DLOG() não afeta nosso código em termos de velocidade de execução ou tamanho binário da biblioteca compartilhada ou binário executável. Isso é muito bom.

O desenvolvimento de DLOG() foi inspirado no sistema operacional Android. Geralmente, há uma string de contexto (independentemente do componente ao qual a entrada de log pertence). No nosso caso, é ctx seguido por uma linha do log de depuração.

Implementação de uma função de soma de thread único.

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

Isso simula o uso real ao trabalhar com código MQL5. Imagine uma situação em que MQL5 envia alguns dados como uma matriz para uma função DLL para calcular algo antes que a DLL retorne o resultado para o código MQL5.
Mas para esta função, ela itera linearmente por todos os elementos da matriz de entrada especificada, um por um, até atingir o número total de elementos conforme especificado por num_elem.

O código também avalia o tempo total de execução usando a biblioteca std::chrono para calcular o tempo decorrido. Observe que estamos usando std::chrono::steady_clock. É um relógio monotônico que avança independentemente da configuração do relógio do sistema. Serve para medição de intervalo de tempo.

Implementação da função de soma multithread.

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

Note que o código v2 foi marcado para referência histórica. Em resumo, para um processador moderno que usa o Protocolo de Consistência de Cache MESI, não há necessidade de fazer uma cópia do conjunto de dados alimentado em cada thread, porque o MESI marca essa linha de cache como compartilhada por vários threads, evitando o desperdício de recursos de computação do processador para sinalizar e aguardar uma resposta.
Na minha implementação anterior (v1), foram feitos esforços para criar uma cópia do conjunto de dados em cada thread, mas, como mencionado, não há necessidade de incluir essa tentativa no código-fonte. A v1 é cerca de 2 a 5 vezes mais lenta que a v2.

A função lambda "worker_func" é responsável por operar em uma matriz inicial de dados e um intervalo de dados para ser processado (um par de índices iniciais e finais). Ela soma todos os elementos dentro do loop em uma variável local para evitar falsa partição, o que pode prejudicar significativamente o desempenho, e então adiciona uma variável de soma comum a todos os threads, utilizando std::atomic para torná-la thread-safe. O número de vezes que a variável de soma precisa ser alterada é muito pequeno e não tem um impacto significativo no desempenho. Foi alcançado um equilíbrio entre a implementação prática e o ganho de velocidade.

Realizamos o cálculo do número necessário de threads para dividir o trabalho, determinando, assim, o intervalo de trabalho de cada thread. É possível que std::hardware_concurrency() retorne o valor 0, o que significa que não é possível determinar o número de threads. Nesse caso, tratamos essa situação e retornamos ao passo 2.

Em seguida, criamos um vetor de fluxos, limitando sua performance ao valor num_max_threads. Calculamos iterativamente o intervalo do conjunto de dados para cada thread no qual queremos trabalhar. É importante observar que o último thread precisará processar todos os dados restantes, uma vez que o número de itens de trabalho pode não ser divisível pelo número de threads de computação utilizados.

O mais importante é que agregamos todos os fluxos. Para situações mais complexas, pode ser necessário um ambiente assíncrono que não impeça o código MQL5 de aguardar um resultado. Nesse caso, normalmente utilizamos std::future, que serve como base para std::async, std::promise e std::packaged_task. Geralmente, temos pelo menos duas interfaces: uma para solicitar o envio de dados do código MQL5 para serem calculados pela DLL sem bloqueio, e outra para obter o resultado dessa solicitação de volta na solicitação, após o que bloqueia a chamada para o código MQL5. Talvez eu aborde esse tema em um próximo artigo.

Além disso, ao longo do caminho, podemos usar DLOG() para imprimir alguns estados de depuração. Isso é útil para depuração.

Agora vamos implementar um testador de núcleo móvel que será executado no Linux nativo e em ambientes de compilação cruzada usando o 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;
}

Incluímos um arquivo de cabeçalho example.h, via de regra, para poder chamar interfaces prontas. Também validamos os resultados com assert().

Vamos criar ambas as bibliotecas compartilhadas (como libexample.so para Linux nativo) e o programa de teste principal, ou seja, main.out. Primeiro, faremos isso não usando o sistema de compilação via linha de comando. Sistema de compilação via Makefile será implementado posteriormente.
Vamos testá-lo localmente no Linux antes da compilação cruzada.

Vamos executar o seguinte comando para criar uma biblioteca compartilhada para saída na forma de libexample.so.

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

A seguir, há uma descrição de cada sinalizador.

Sinalizador Descrição
-shared Cria uma biblioteca compartilhada
-std=c++17 Usa a sintaxe C++ padrão do C++17.
-Wall Exibe avisos durante a compilação
-Wextra Exibe avisos adicionais durante a compilação
-fno-rtti Ele faz parte da otimização. Desativa RTTI (Informações de tipo de tempo de execução).
O RTTI permite determinar o tipo de objeto em tempo de execução. Nós não precisamos disso. Além disso, reduz o desempenho.
-O2 Ativa o nível de otimização 2, que permite otimizações mais agressivas sobre o nível 1.
-I. Define o caminho de inclusão para o diretório atual para que o compilador possa encontrar nosso arquivo de cabeçalho example.h, que está no mesmo diretório.
-fPIC Gera código independente de posição (PIC) adequado para construir uma biblioteca compartilhada,
e trabalha com o programa principal de vinculação. Não ter um endereço de memória fixo para carregar uma função específica de uma biblioteca compartilhada também melhora a segurança.
-lpthread  Vincula com a biblioteca pthread

Execute o seguinte comando para criar o programa de teste principal associado a libexample.so e para ter uma saída como main.out.

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

Esta é uma descrição de cada sinalizador diferente do mencionado acima.

Sinalizador Descrição
-L. Configura o caminho de inclusão para a biblioteca compartilhada no mesmo diretório.
 -lexample Vinculação para a biblioteca compartilhada libexample.so.

Finalmente, rodamos o executável.

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

Como você pode ver, a função multithread funciona muito mais rápido que a função single-threaded (cerca de 4,33 vezes mais rápido).

Bem, estamos familiarizados com a compilação e criação da biblioteca compartilhada e do programa principal usando a linha de comando. Agora vamos criar o sistema de compilação necessário usando o Makefile.
Para isso, é possível usar o CMake, mas como desenvolvemos principalmente no Linux, o CMake parece redundante para mim. Não precisamos desse tipo de compatibilidade para desenvolver com base no Windows. Então escolhemos Makefile.

Teremos três opções de Makefile.

  1. Makefile
    Projetado para compilação cruzada em Linux e Windows. Implementa pthread. Usamos para construir uma DLL que funciona com o MetaTrader 5, além do programa de teste principal que pode ser executado através do Wine.

  2. Makefile-th_win32
    Igual ao Makefile, mas usa thread win32.

  3. Makefile-g++
    Projetado para compilação em sistema Linux nativo. Estas são as etapas que acabamos de fazer acima.

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

Todos os três Makefiles são quase o mesmo código, com algumas pequenas diferenças.

As diferenças são:

  • nome binário do compilador
  • -DUSE_MINGW_STD_THREAD
  • presença/ausência de -lpthread
  • nome do binário de saída, como libexample.so ou example.dll, e main.out ou main.exe, dependendo do sistema de destino da compilação


MORE_FLAGS é declarado como

MORE_FLAGS ?=

o que significa que ele permite que os usuários passem sinalizadores de compilação adicionais da linha de comando, para que o usuário possa, sob demanda, adicionar sinalizadores adicionais conforme necessário. Se os sinalizadores não forem passados externamente pelos usuários, será usado o que já está definido no código do Makefile.

Tornamos todos os Makefiles executáveis

$ chmod 755 Makefile*

As informações para construir a variante Makefile são apresentadas na tabela abaixo.

Sistema alvo Comando de compilação  Comando de limpeza
Compilação cruzada usando pthread make  make clean
Compilação cruzada usando thread win32 make -f Makefile-th_win32  make -f Makefile-th_win32 clean
Linux nativo make -f Makefile-g++  make -f Makefile-g++ clean

Vamos criar uma DLL para usar com MetaTrader 5 e Wine, assim podemos testar os dois.

Executamos

$ make

Geramos os seguintes arquivos

  1. example.dll
  2. main.exe


Teste de um executável de compilação cruzada.

$ 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

Então, temos um problema, já que main.exe não consegue encontrar as DLLs necessárias.
A solução é colocá-las no mesmo diretório do nosso executável.

As seguintes DLLs são necessárias:

  • libgcc_s_seh-1.dll
    Usada para oferecer suporte à manipulação de exceção C++ e outros recursos de baixo nível não suportados nativamente pelo Windows.

  • libstdc++6.dll
    Ela é a base para a manutenção de um programa C++. Contém funções e classes usadas para executar várias operações, como entrada e saída, operações matemáticas e gerenciamento de memória.

  • libwinpthread-1.dll
    Implementação da API pthread para Windows.
    Esta DLL pode não aparecer na saída do terminal, mas depende das duas DLLs anteriores mencionadas.

Desde que instalamos o Mingw, essas DLLs já estão em nosso sistema Linux. Só precisamos encontrá-las.

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

Este comando localiza libgcc_s_seh-1.dll, ignorando os diretórios (use -type f), pois a pesquisa começa no diretório raiz (use /). Se ocorrer um erro, redefina para /dev/null (usando 2>/dev/null).

Veremos a saída correspondente para 

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

Observe win32 e posix como parte do nome do diretório. Ao compilar com um Makefile, copie essa DLL a partir do diretório baseado em posix. Ao compilar Makefile-th_win32, copie a DLL a partir do diretório baseado em win32.

Como decidimos confiar principalmente no pthread, sugiro o seguinte:

  • Copie a DLL do diretório posix para o diretório do nosso projeto (o mesmo que para o binário executável).
  • Podemos precisar testar um thread win32, para que possamos criar o diretório win32 e posix e, em seguida, copiar as DLLs apropriadas em cada diretório.
    Sempre que você precisar copiar um thread específico, copie a DLL gerada e o executável para um diretório win32 ou posix e, em seguida, execute o programa a partir daí através do Wine ou vice-versa.

Finalmente, podemos testar o programa da seguinte maneira.

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

Ignore as saídas irrelevantes, que são avisos e pequenos bugs do próprio Wine.

Vemos que a função multithread é cerca de 3,4 vezes mais rápida que a função single-threaded, o que é um pouco mais lento do que com a compilação nativa do Linux.
Voltaremos ao tópico do teste mais tarde, quando terminarmos de implementar o código MQL5 necessário.

Tudo está pronto para a implementação do código MQL5.


Experimento, fase de desenvolvimento II - código MQL5 para usar a DLL

Tivemos que percorrer um longo caminho até o segundo estágio do desenvolvimento do código MQL5.

Implementação do script 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 ---");
}

Podemos executar o código MQL5 como um EA ou um indicador, mas para o propósito de nosso experimento, precisamos testar todas as etapas e todo o fluxo de trabalho. E para isso usaremos um script.
Em uma situação real, geralmente precisamos de Expert Advisors ou indicadores para obter dados do terminal, por exemplo via OnTick(), OnTrade(), OnCalculate(). Informações adicionais sobre quais funções são suportadas por cada tipo de programa na plataforma MetaTrader 5 podem ser encontradas na seção "Execução de programa".

Agora vamos analisar o código acima peça por peça.

Importação de assinaturas de função a partir da 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

Para poder chamar funções derivadas de uma DLL, precisamos declarar essas assinaturas novamente no código MQL5.

O que você deve prestar atenção:

  • Podemos omitir nomes de parâmetros de funções como add(int, int) e sub(int, int).
  • Matrizes são passadas como referência apenas no MQL5. Preste atenção na diferença entre as assinaturas declaradas no código da DLL e no MQL5. O código MQL5 tem um & (ampersand), mas o código DLL não tem.
    Observe que a sintaxe C++ usada em MQL5 e a sintaxe C++ padrão não correspondem completamente. Sempre que passamos uma matriz para MQL5, precisamos adicionar &.

Vamos criar uma matriz com um grande conjunto de dados

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

Desse modo, é criada uma matriz de números inteiros para 1000 milhões de elementos e cada elemento é definido como 1. A matriz é dinâmica e é armazenada na memória dinâmica. Não há espaço suficiente na pilha para armazenar uma quantidade tão grande de dados.
Portanto, para tornar a matriz dinâmica, use a sintaxe int arr[].

Depois disso, você precisa chamar cada função DLL das assinaturas declaradas conforme necessário. E verificamos o resultado. Se estiver incorreto, um Alert() é enviado ao usuário. Mas não nós vamos embora imediatamente.

Usamos ArraySize() para obter o número de elementos na matriz. Para passar uma matriz para uma função, simplesmente passamos sua variável diretamente para a função.

Vamos compilar o script e finalizar a implementação.


Copiamos todas as DLLs necessárias para o MetaTrader 5

Antes de executar o script MQL5, precisamos copiar todas as DLLs necessárias para o diretório <terminal>/Libraries. O caminho completo geralmente é assim: ~/.mt5/drive_c/Program Files/MetaTrader 5/MQL5/Libraries.
É aqui que o MetaTrader 5 procurará as DLLs necessárias para os programas que criamos para ele. Vamos voltar para a seção "Teste de executável de compilação cruzada" para ver a lista de DLLs a serem copiadas.

Por padrão, o script de instalação oficial do MetaTrader 5 instalará automaticamente o Wine com o prefixo ~/.mt5. Isso se aplica apenas a usuários que usam o script de instalação oficial.


Teste

Arraste o TestConsumeDLL compilado para o gráfico

Arraste o TestConsumeDLL compilado para o gráfico para iniciar a execução

Primeiro, vamos testar a execução do MetaTrader 5 via Wine no Linux.
Vamos arrastar o TestConsumeDLL compilado para o gráfico. Em seguida, veremos uma caixa de diálogo solicitando permissão para importar de uma DLL, bem como uma lista de dependências de DLL para o programa MQL5 que criamos.

Caixa de diálogo solicitando permissão para importar uma DLL junto com uma lista de dependências de DLL

Caixa de diálogo solicitando permissão para importar uma DLL junto com uma lista de dependências de DLL

Embora não tenhamos visto libwinpthread-1.dll, porque não é uma dependência direta do script MQL5 compilado, mas uma dependência para ambos - libgcc_s_seh-1.dll e libstdc++6.dll. Podemos verificar a dependência DLL do arquivo DLL de destino com objdump da seguinte maneira.

$ 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


O objdump pode ler um arquivo binário (biblioteca compartilhada ou executável) criado no Windows e no Linux. É versátil o suficiente para carregar informações disponíveis conforme necessário. O sinalizador -x é para exibir o conteúdo de todos os cabeçalhos.

O resultado é exibido na guia "Experts"

Resultados da execução de TestConsumeDLL na guia Experts

Resultados da execução de TestConsumeDLL na guia "Experts"


Também é mostrado o tempo gasto em cada função na janela do terminal usada para iniciar o MetaTrader 5 no Linux.

O tempo decorrido é impresso no console para cada função

Na mesma janela de terminal que foi usada para iniciar o MetaTrader 5, os usuários verão o tempo necessário para a saída da DLL

Até agora, não vimos nenhum aviso de Alerts() e o tempo de execução necessário é exibido corretamente. Nosso programa experimental está quase pronto.


Testando no Windows

Precisamos do seguinte:

  • VirtualBox com as adições de convidados instaladas
    Para instalar as adições de convidados, você pode encontrar informações na Internet, portanto, não há necessidade de prolongar o artigo sobre isso.
    É importante lembrar que as adições de convidados são necessárias para copiar arquivos como example.dll para a máquina convidada (no caso, uma máquina Windows).

  • Windows 7+ ISO de 64 bits
    Além disso, é importante baixar e instalar essa imagem ISO do Windows no disco rígido via VirtualBox.

Interface principal do VirtualBox

Essa é a interface principal do VirtualBox. E depende dos recursos de hardware que podem ser alocados. Quanto mais, melhor ao verificar a velocidade de execução em uma DLL


A velocidade de execução de uma DLL ao ser testada no Virtualbox, depende dos recursos alocados para o Windows. Abaixo, apresento a configuração que uso:

  • Sistema -> Placa-mãe -> RAM definido como 20480 MB ou 20 GB (tenho 32 GB no host)
  • Sistema -> Processor -> Processador(es) é 6 com um limite de execução de 100% (6 sendo o máximo permitido aqui)
  • Display -> Tela -> RAM de vídeo definida no máximo (isso é opcional, mas útil se você estiver usando vários monitores, quanto mais monitores, mais RAM de vídeo você precisa)
  • Display -> Tela -> Número de monitores definido como 1

Após configurar a máquina, é hora de testar. Existem duas opções: copiar o código MQL5 compilado da máquina Linux ou copiar todo o código e usá-lo no MetaEditor para compilá-lo novamente na máquina Windows.
Na minha experiência, a segunda opção foi a mais adequada e simples, apenas um processo de copiar e colar. Por isso o escolhi.

Resultado do teste

Resultado TestConsumeDLL na guia Experts no Windows

Resultados na guia "Experts" no Windows


O problema é que o tempo de execução é exibido via saída padrão (stdout) e não consigo encontrar uma maneira de salvar essa saída ao executar o MetaTrader 5 no Windows. Tentei executar o MetaTrader 5 com um arquivo de configuração para executar um script desde o início e, em seguida, redirecionar a saída para um arquivo, mas a tentativa falhou porque o MetaTrader 5 não permite que nenhuma DLL seja carregada quando executado a partir da linha de comando. Para corrigir isso sem mexer no código DLL principal, vamos fazer um pequeno ajuste no código MQL5 para calcular o tempo decorrido a partir daí usando 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 ---");
}

Observe o comentário "// *". Vale a pena prestar atenção a isso.

Vamos testar novamente.

Resultados do novo teste do Windows TestConsumeDLL na guia Experts

Código MQL5 atualizado testado no Windows para medir o tempo de execução


Concluímos o aplicativo experimental criando uma DLL com suporte a multithreading e, em seguida, usando-a no código MQL5 e testando-a no Linux e no Windows. Tudo funciona bem.


Testando ambas as implementações de threads Mingw

Faremos um teste simples usando nosso programa experimental, porque há muitos fatores a serem considerados para testar na íntegra os recursos multithreading do C++ em diferentes plataformas, incluindo várias primitivas de sincronização, thread_local, domínio problemático e assim por diante.

O teste é realizado da seguinte forma:

  • Linux
    • Compilação com Makefile. Em seguida, realização do teste cinco vezes antes da média e o mesmo para Makefile-th_win32
    • Execução do binário com WINEPREFIX=~/.mt5 wine main.exe
    • Uso dos 12 threads e todos os 32 GB de RAM disponíveis.
  • Windows
    • Compilação com Makefile. Em seguida, realização do teste cinco vezes antes da média e o mesmo para Makefile-th_win32
    • Copie das DLLs e executáveis necessários na máquina convidada (Windows) usando o Virtualbox
    • Execução do arquivo binário usando a linha de comando main.exe
    • Limite de 6 threads e 20 GB de RAM (respeitando as configurações permitidas no VirtualBox)

Os resultados serão arredondados para duas casas decimais.

Os resultados são mostrados na tabela a seguir.

Função  Linux + pthread (ms) Linux + thread win32 (ms) Windows + pthread (ms) Windows + thread win32 (ms)
 single_threaded_sum 417,53
417,20
467,77
475,00
 multi_threaded_sum_v2  120,91  122,51  121,98  125,00


Considerações finais

O Mingw e o Wine são ferramentas de plataforma cruzada que permitem aos desenvolvedores usar o Linux para criar aplicativos de plataforma cruzada que são executados perfeitamente no Linux e no Windows. Isso também se aplica ao desenvolvimento de aplicativos para MetaTrader 5. Nosso aplicativo experimental de desenvolvimento de DLL multithread em C++, testado no Linux e no Windows, oferece opções alternativas para aumentar o acesso do desenvolvedor ao ecossistema.



Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/12042

Arquivos anexados |
ExampleLib.zip (5.37 KB)
Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 10): Usando apenas dados reais na replay Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 10): Usando apenas dados reais na replay
Aqui vamos ver como você pode utilizar dados mais fieis ( tickets negociados ) no sistema de replay, sem necessariamente ter que se preocupar se eles estão ou não ajustados.
Mais sobre o sistema Murray Mais sobre o sistema Murray
Os sistemas gráficos de análise de preços são amplamente reconhecidos e apreciados pelos traders. Neste artigo, irei abordar o sistema Murray em sua totalidade, que engloba não apenas os renomados níveis, mas também outras técnicas úteis para avaliar a posição atual do preço e tomar decisões de negociação.
Receitas MQL5 — Banco de dados de eventos macroeconômicos Receitas MQL5 — Banco de dados de eventos macroeconômicos
Este artigo explora como trabalhar com bancos de dados baseados no mecanismo SQLite. Com o objetivo de oferecer conveniência e utilizar eficientemente os princípios da OOP, foi criada a classe CDatabase. Essa classe é responsável pela criação e gerenciamento de um banco de dados de eventos macroeconômicos. Além disso, são apresentados exemplos de como utilizar diferentes métodos da classe CDatabase.
Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 09): Eventos Customizados Desenvolvendo um sistema de Replay - Simulação de mercado (Parte 09): Eventos Customizados
Aqui vamos ver como disparar eventos customizados e melhorar a questão sobre como o indicador informa o status do serviço de replay/simulação.