English Русский 中文 Deutsch 日本語 Português
preview
Desarrollando una DLL experimental con soporte multihilo en C++ para MetaTrader 5 en Linux

Desarrollando una DLL experimental con soporte multihilo en C++ para MetaTrader 5 en Linux

MetaTrader 5Ejemplos | 9 mayo 2023, 10:35
424 0
Wasin Thonkaew
Wasin Thonkaew

Introducción

Linux dispone de un ecosistema dinámico y una buena ergonomía para el desarrollo de software.

Resulta muy cómodo para aquellos a quienes les gusta trabajar desde la línea de comandos y buscan una instalación sencilla de aplicaciones a través del gestor de paquetes. El sistema operativo no es una caja negra, pero resulta divertido dominarlo. Se puede personalizar para casi todos los subsistemas, cuenta con herramientas integradas y un entorno de desarrollo de software flexible y optimizado.

El sistema operativo está disponible tanto en el escritorio como en una solución en la nube, en un servidor privado virtual (VPS) especial o con proveedores de servicios en la nube como AWS y Google Cloud.

Creo que muchos desarrolladores se ciñen al sistema operativo que han elegido, pero también desean desarrollar productos para los usuarios de Windows. Obviamente, los productos tienen que funcionar igual de bien en las distintas plataformas.

En general, los desarrolladores crean sus indicadores, robots y productos relacionados en MQL5 y luego los publican en el Mercado sin preocuparse por el sistema operativo. En cuanto a la compilación y la construcción del archivo ejecutable .EX5, pueden simplemente confiar en el entorno de desarrollo en línea de MetaTrader 5 (siempre que sepan cómo ejecutar MetaTrader 5 en Linux).
Pero cuando los desarrolladores necesitan crear una solución como biblioteca compartida (DLL) para ampliarla y crear opciones adicionales, tienen que dedicar más tiempo y esfuerzo a encontrar soluciones de compilación cruzada, descubrir posibles obstáculos, aprender las mejores prácticas, familiarizarse con las herramientas, etc.

Esta es la razón que me ha impulsado a escribir este artículo. La compilación cruzada y la posibilidad de crear una DLL multihilo en C++ sirven como comienzo para profundizar en el tema.
Espero que este artículo le ayude a seguir desarrollando productos relacionados con MetaTrader 5 en Linux.

A quién va dirigido este artículo

Asumo que los lectores ya tienen cierta experiencia en la interacción con Linux a través de la línea de comandos, además de una comprensión general de la compilación de C++ y la construcción de código fuente en Linux.

Este artículo está dirigido a aquellos que quieran aprender a desarrollar una DLL que soporte miltihilo en Linux, pero que también funcione en Windows. Amplíe sus capacidades de programación multihilo utilizando no solo la OpenCL incorporada, sino también el código C++ móvil, flexible y básico con capacidad multihilo para la integración con algunos de los otros sistemas estrechamente relacionados con él. 

SO y software

  • Ubuntu 20.04.3 LTS con kernel versión 5.16.0 en un procesador AMD Ryzen 5 3600 de 6 núcleos (2 hilos por núcleo), 32 GB RAM.
  • Wine (paquete winehq-devel) 8.0-rc3 (al momento de escribirse este artículo). Eche también un vistazo también al hilo del blog MT5 build 3550 se bloquea inmediatamente al iniciar con el paquete winehq-stable (en inglés) para ver por qué hemos decidido usar devel en lugar de un paquete estable.
  • Mingw (paquete mingw-w64) 7.0.0-2
  • VirtualBox 6.1 para pruebas en Windows

Plan del artículo

Nos ceñiremos al siguiente plan:

  1. Conocimiento de Wine
  2. Conocimiento de Mingw
  3. Implementación de hilos de Mingw
    1. POSIX (pthread)
    2. Win32 (a través de mingw-std-threads)
  4. Preparación de un entorno de desarrollo para Linux
    1. Instalación de Wine
    2. Instalación de MetaTrader 5
    3. Instalación de Mingw
    4. (Opcional) Instalación de mingw-std-threads
  5. Experimento, etapa de desarrollo I - DLL (soporte multihilo en C++)
  6. Experimento, etapa de desarrollo II - Código MQL5 para usar una DLL
  7. Pruebas en Windows
  8. Una prueba sencilla de la aplicación de los hilos Mingw


Wine

El nombre Wine es un recursivo retroacrónimo y se descifra como Wine is Not an Emulator (Wine no es un emulador). De hecho, no supone un emulador de ningún proceso o equipo. Wine es una API win32 que funciona en sistemas operativos distintos a Windows.

Wine introduce otra capa de abstracción que intercepta la llamada a la API de los usuarios de win32 en un sistema distinto a Windows, la redirige a los componentes internos de Wine y, a continuación, procesa la petición de la misma forma (o casi la misma) que en Windows. 

Wine funciona con la API win32 usando la API . Los usuarios pueden trabajar en Wine sin sospechar que están ejecutando una aplicación de Windows en Linux, e incluso jugar a juegos de su biblioteca de Steam en Linux, ya que su tiempo de ejecución se basa en una variante de Wine llamada Proton.

Esto nos ofrece flexibilidad al probar o usar aplicaciones de Windows para las que no existe una contrapartida en Linux.

Normalmente ejecutaríamos el siguiente comando al iniciar una aplicación Windows a través de Wine:

wine windows_app.exe

Si queremos ejecutar una aplicación asociada a un determinado prefijo del entorno Wine, teclearemos:

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe


Mingw

Mingw se descifra como "Minimalist GNU for Windows" (GNU minimalista para Windows). Es una adaptación a Linux de la colección de compiladores de GNU (GCC) y sus herramientas para compilar C/C++ y otros lenguajes de programación orientados a Windows.

Las funciones y los parámetros de compilación están disponibles tanto en GCC como en Mingw, por lo que los usuarios que conozcan GCC se acostumbrarán a Mingw fácilmente. Además, GCC es muy similar a Clang en cuanto a los parámetros de compilación. De esta forma, los usuarios podrán usar fácilmente sus desarrollos en el nuevo entorno, al tiempo que se amplía la base de usuarios para incluir a los usuarios de Windows.

A continuación, le mostramos una tabla comparativa para que pueda ver las diferencias.

  • Compilación del código fuente C++ y creación de una biblioteca compartida
Compilador Línea de comandos
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

  • Compilación del código fuente C++ y creación de un archivo binario ejecutable
Compilador Línea de comandos
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

Como podemos ver, las diferencias son mínimas. Las banderas de compilación resultan muy similares, casi idénticas. La única diferencia reside en el archivo binario del compilador para construir todo lo necesario.

Existen tres opciones de uso. Para más información, podrá consultar el apartado sobre la implementación multihilo.

  1. x86_64-w64-mingw32-g++
    Denominación x86_64-w64-mingw32-g++-win32.

  2. x86_64-w64-mingw32-g++-posix
    Ejecutable binario diseñado para trabajar con pthread.

  3. x86_64-w64-mingw32-g++-win32
    Ejecutable binario diseñado para trabajar con el modelo de streaming de la API win32. Denominación 86_64-w64-mingw32-g++.

Además, existen otras herramientas con el prefijo 

x86_64-w64-mingw32-...

Aquí tenemos algunos ejemplos:

  • x86_64-w64-mingw32-gcc-nm - cambio de nombre
  • x86_64-w64-mingw32-gcc-ar - control de archivos
  • x86_64-w64-mingw32-gcc-gprof - análisis del rendimiento de sistemas operativos tipo Unix
También existe x86_64-w64-mingw32-gcc-nm-posix x86_64-w64-mingw32-gcc-nm-win32

Aplicación de los hilos Mingw

Por el apartado anterior, sabemos que hay dos opciones para implementar los hilos proporcionados por Mingw.
  1. POSIX (pthread)
  2. Win32

¿Por qué debería importarnos esto? Básicamente, se me ocurren dos razones:

  1. Seguridad y compatibilidad
    Si su código usa potencialmente la capacidad multihilo de C++ (por ejemplo, std::thread, std::promise, etc.), así como el soporte incorporado multihilo del SO, por ejemplo CreateThread() para win32 API y pthread_create() para POSIX API, mejor utilizar una sola API concreta.

    En cualquier caso, resulta poco probable mezclar un código que utilice las características multihilo de C++ y el soporte de SO, salvo en situaciones muy concretas en las que la API de soporte de SO ofrezca más funciones que C++. Por lo tanto, resultará mejor ser coherente y usar un único modelo de flujo.
    Al utilizar la implementación pthread, intente no aplicar las capacidades multihilo de la API win32, y viceversa.

  2. Rendimiento (para más detalles, consulte el apartado "Una prueba sencilla de la aplicación del hilo Mingw")
    Obviamente, los usuarios quieren una solución multihilo con baja latencia. Por norma general, los usuarios eligen aplicaciones con una ejecución más rápida.

Primero desarrollaremos nuestra DLL de prueba y el programa de prueba, y luego probaremos ambas implementaciones de hilos.

Para el proyecto, usaremos código móvil para utilizar el hilo pthread o win32. Podemos cambiar fácilmente nuestro sistema de montaje de un sistema a otro.
Al utilizar un hilo win32, deberá establecer los encabezados desde el proyecto mingw-std-threads


Preparación de un entorno de desarrollo para Linux

Antes de pasar directamente al código, deberemos instalar el software necesario.

Instalación de Wine

Ejecute el siguiente comando para instalar el paquete devel de Wine.

sudo apt install winehq-devel

a continuación, tendrá que comprobar que funciona correctamente usando el siguiente comando

wine --version

La respuesta debería ser algo así

wine-8.0-rc3


Instalación de MetaTrader 5

La mayoría de los usuarios ya habían instalado MetaTrader 5 mucho antes del build 3550, lo cual provocaba un fallo. No podemos aplicar el script de instalación oficial para utilizar el paquete winehq-devel y ejecutar MetaTrader 5 como se describe en la "Instalación en Linux".
Resulta más adecuado ejecutar los comandos de forma independiente, ya que la ejecución directa del script de instalación oficial sobrescribirá nuestro Wine en el paquete estable.

He escrito una guía MT5 Build 3550 Broken Launching On Linux Through Wine. How To Solve? (en inglés) en mi propio blog. Este artículo está dirigido a los usuarios que ya han instalado el paquete estable de Wine, y también a los que desean iniciar de nuevo la instalación con el paquete devel.

Una vez hecho esto, intente ejecutar nuevamente MetaTrader 5 a través de Wine y asegúrese de que todo está bien.

Observación

El script de instalación oficial creará un entorno Wine (llamado prefijo) en la dirección ~/.mt5. Le recomendamos añadir la siguiente línea a ~/.bash_aliases para facilitar el inicio de MetaTrader 5.

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

A continuación, úsela con

source ~/.bash_aliases

Por último, ejecute el siguiente comando para iniciar MetaTrader 5. La depuración de este se mostrará en el terminal.

mt5trader

Ejecutar MetaTrader 5 de esta forma nos permitirá más tarde ver el registro de depuración de nuestra aplicación experimental sin necesidad de complicar el código.

Instalación de Mingw

Ejecute el siguiente comando para instalar Mingw.

sudo apt install mingw-w64

Esto instalará en su sistema el kit de herramientas con el prefijo x86_w64-w64-mingw32-. Básicamente, trabajaremos o bien con x86_64-w64-mingw32-g++-posix, o bien con x86_64-w64-mingw32-win32, en el caso de usar el hilo win32.

Instalación de mingw-std-threads

mingw-std-threads es un proyecto que integra hilos win32 para ejecutarse en Linux. La solución se usa para trabajar con encabezados. La instalación resulta bastante sencilla y solo requiere colocar el archivo de encabezado en la ruta de inclusión.

Para instalarlo, siga los pasos indicados a continuación.

En primer lugar, clone el repositorio git en su sistema.

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

A continuación, cree un directorio para almacenar su encabezado en la ruta de inclusión.

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

Por último, copie en el directorio recién creado todos los archivos de encabezado (.h) del directorio del proyecto clonado.

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

Eso es todo. Luego, en el código, si decidimos usar un hilo win32, para algunos de los archivos de encabezado relacionados con la capacidad multihilo (por ejemplo, hilos, primitivas de sincronización, etc.), tendremos que incluirlo desde la ruta correspondiente con un cambio de nombre. Puede consultar la lista completa en el cuadro siguiente.

Inclusión del archivo de encabezado C++11 multihilo Inclusión del archivo de encabezado 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, etapa de desarrollo I - DLL (soporte multihilo en C++)

Ahora es el momento de pasar al código.

Nuestro objetivo es implementar una solución DLL experimental capaz de aprovechar las capacidades multihilo de la biblioteca estándar C++11 para comprender la idea e investigar más a fondo.

A continuación, le mostramos nuestra biblioteca y la estructura de la aplicación.

Estructura del proyecto

  • DLL
    • example.cpp
    • example.h
  • Usuario
    • main.cpp
  • Ensamblaje
    • Makefile - archivo de compilación cruzada que usa pthread
    • Makefile-th_win32 - archivo de compilación cruzada que usa hilos win32 
    • Makefile-g++ - archivo de compilación para pruebas en Linux nativo. Está diseñado para una rápida iteración y depuración durante el desarrollo del proyecto.

Estándar aplicable C++

Aunque usaremos principalmente el estándar C++11, a veces necesitaremos utilizar también elementos del estándar C++17, como el atributo de anotación 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);
};

Aunque #pragma once no forma parte del estándar C++, está soportado por GCC, y, por lo tanto, también por Mingw. Hablamos de la forma más flexible y breve de evitar la inclusión de encabezados duplicados.
Sin esta directiva, los usuarios aplicarían #ifdef y #define y tendrían que asegurarse de que cada definición tenga un nombre único para cada archivo de encabezado. Y esto llevaría mucho tiempo.

Tenemos #ifdef WINDOWS para guardar la declaración de la definición EXAMPLE_API. Esto nos permite compilar con la ayuda de Mingw y Linux nativo. Así, siempre que queramos realizar una compilación cruzada para una biblioteca compartida, añadiremos -DWINDOWS y -DEXAMPLE_EXPORT a la bandera de compilación. Si estamos compilando solo para probar el programa principal, podemos omitir -DEXAMPLE_EXPORT.

__declspec(dllexport) - directiva para exportar una función desde una DLL.

__declspec(dllimport) - directiva para importar una función desde una DLL.

Estas directivas son necesarias para compilar y poder trabajar con una DLL en Windows, y no son necesarias para sistemas que no sean Windows, pero sí para la compilación cruzada. Así, el valor permanece vacío para EXAMPLE_API cuando no existe una definición WINDOWS para compilar en Linux.

Las signaturas de las funciones deberán ser compatibles con la convención de llamadas de C.
Esta 'C' superficial evitará la tergiversación de las signaturas de funciones implementadas en la convención de llamadas de C++.

No podemos envolver las signaturas de función dentro de namespace o declararlas como funciones libres, porque el código MQL5 no será capaz de encontrar estas signaturas cuando utilicemos la DLL más tarde.

Para num_hardware_concurrency(), retornará el número de hilos simultáneos soportados por la implementación.
Por ejemplo, yo uso un procesador de 6 núcleos con 2 hilos por núcleo, por lo que en realidad tiene 12 hilos que pueden funcionar simultáneamente. En mi caso, retorna 12.

single_threaded_sum() y multi_threaded_sum_v2() muestran claramente las ventajas del multihilo y permiten comparar el rendimiento.

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

Más arriba tenemos el código completo. Vamos a desglosar cada parte por separado para facilitar la comprensión.

Podemos alternar entre pthread y win32.

#include "example.h"

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

Esta configuración permite una buena integración en nuestro sistema de compilación para alternar entre pthread y win32. Si añadimos -DUSE_MINGW_STD_THREAD a la bandera de compilación, podremos utilizar un hilo win32 durante la compilación cruzada.

Introducción de 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() y sub() son fáciles de entender. Para num_hardware_concurrency() necesitamos incluir un encabezado <thread> para usar std::thread::hardware_concurrency().

Función de utilidad de registro de depuración.

#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

Añadiendo -DENABLE_DEBUG a la bandera de compilación, permitimos la muestra del registro de depuración en la consola. Por eso le sugiero ejecutar MetaTrader 5 a través de la línea de comandos, para que podamos depurar nuestro programa en consecuencia.
Cada vez que no hagamos tal definición, DLOG() no significará nada y no tendrá ningún efecto en nuestro código, ni en cuanto a la velocidad de ejecución, ni en cuanto al tamaño binario de la biblioteca compartida o del archivo binario ejecutable. Y esto está muy bien.

El desarrollo de DLOG() se inspiró en el sistema operativo Android. Suele haber una cadena de contexto (independientemente del componente al que pertenezca la entrada del registro). En nuestro caso será ctx, seguida de una línea del registro de depuración.

Implementamos la función de suma de un solo hilo.

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

Aquí se simula el uso real al trabajar con código MQL5. Imaginemos una situación en la que MQL5 envía algunos datos como un array a una función DLL para calcular algo antes de que la DLL retorne el resultado al código MQL5.
Pero para esta función, iteraremos uno a uno por todos los elementos del array de entrada especificado hasta alcanzar el número total de elementos indicado por num_elem.

El código también valorará el tiempo total de ejecución usando la biblioteca std::chrono para calcular el tiempo transcurrido. Tenga en cuenta que usamos std::chrono::steady_clock. Se trata de un reloj monótono que avanza independientemente de la configuración del reloj del sistema. El reloj monótono resulta perfecto para medir intervalos temporales.

Implementamos la función de suma multihilo.

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

Tenga en cuenta que se marca como v2. Dejamos esto por razones históricas. En resumen, para un procesador moderno que utilice el protocolo de coherencia de caché MESI, no será necesario hacer una copia del conjunto de datos suministrado a cada hilo porque el MESI marcará dicha línea de caché como compartida por múltiples hilos y no malgastará recursos informáticos del procesador en señalizar y esperar una respuesta.
En mi anterior implementación de v1, se hacía todo lo posible para crear una copia del conjunto de datos suministrado a cada hilo. Pero por la razón mencionada, no resulta necesario incluir tal intento en el código fuente: v1 es más lento que v2 en un factor de aproximadamente 2-5.

La notificación worker_func es una función lambda que trabaja con el array de datos fuente y el rango de datos con el que se va a trabajar (un par de índices de inicio y finales). Suma todos los elementos dentro del ciclo en una variable local para evitar el uso compartido falso, que puede reducir significativamente el rendimiento, y luego suma en una variable de suma común para todos los hilos. Utiliza std::atomic para hacerlo seguro para los hilos. El número de veces que debe modificarse una variable de suma total de este tipo será bastante reducido y no tendrá un impacto significativo en el rendimiento. Digamos que se consigue un equilibrio entre la aplicación práctica y el aumento de la velocidad.

Nosotros calculamos cuántos hilos serán necesarios para dividir el trabajo, conociendo así el rango de trabajo para cada hilo. std::hardware_concurrency() puede devolver 0, Esto significa que no será capaz de determinar el número de hilos, así que gestionaremos este caso también y regresaremos a 2.

A continuación, crearemos un vector de hilos. Limitaremos su rendimiento a num_max_threads. A continuación, calcularemos de forma iterativa el intervalo del conjunto de datos para cada hilo sobre el que debemos trabajar. Tenga en cuenta que el último hilo necesitará todos los datos restantes, ya que el número de elementos del trabajo a realizar podría no ser divisible por el número de hilos de cálculo utilizados.

Y lo más importante: unimos todos los hilos. Para casos más complejos, podemos necesitar un entorno asíncrono que no bloquee el código MQL5 a la espera de los resultados. Normalmente utilizamos std::future, que es la base de std::async, std::promise y std::packaged_task. Así que solemos tener al menos dos interfaces: una para solicitar el envío de datos desde el código MQL5 para calcular con DLL sin bloqueo, y otra para recibir el resultado de dicha solicitud de vuelta según la solicitud, tras lo cual bloquea la llamada del código MQL5. Quizá hablemos de ello en el próximo artículo.

Además, podemos utilizar DLOG() para imprimir algunos estados de la depuración. Esto resulta útil para la depuración.

Ahora vamos a implementar el programa de pruebas principal móvil que se ejecutará en Linux nativo y en un entorno de compilación cruzada utilizando 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;
}

Incluiremos el archivo de encabezado example.h, por lo común, para poder llamar a interfaces ya creadas. También confirmaremos los resultados con assert().

Vamos a crear ambas bibliotecas compartidas (como libexample.so para Linux nativo) y el programa de prueba principal, a saber main.out. En primer lugar, vamos a hacer esto sin usar el sistema de compilación: utilizaremos la ejecución de la línea de comandos. El sistema de compilación usando Makefile se implementará más adelante.
Primero lo probaremos localmente en Linux antes de hacer una compilación cruzada.

Luego ejecutaremos el siguiente comando para crear una biblioteca compartida para la muestra como libexample.so.

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

A continuación, ofrecemos una descripción de cada bandera

Bandera Descripción
-shared Indicar al compilador que construya una biblioteca compartida
-std=c++17 Indicar al compilador que utilice la sintaxis C++ del estándar C++17.
-Wall Indicar que se muestren todas las advertencias al compilar
-Wextra Mostrar también advertencias adicionales al compilar
-fno-rtti Esto es parte de la optimización. Desactivar RTTI (información de tipo en tiempo de ejecución).
La RTTI permite determinar el tipo de objeto en tiempo de ejecución. No necesitamos esto. Además, reduce el rendimiento.
-O2 Activar el nivel de optimización 2, que incluye optimizaciones más agresivas además del nivel 1.
-I. Establecer la ruta de inclusión en el directorio actual para que el compilador pueda encontrar nuestro archivo de encabezado example.h, que se encuentra en el mismo directorio.
-fPIC Generalmente se necesita al crear una biblioteca compartida, ya que indica al compilador que genere código independiente de la posición (PIC) adecuado para crear una biblioteca compartida,
y trabajar con el programa principal al que hay que vincularse. La ausencia de una dirección de memoria fija para cargar la función específica de una biblioteca compartida también aumenta la seguridad.
-lpthread  Instrucción para enlazar con la biblioteca pthread

Ejecute el siguiente comando para crear el programa de prueba principal vinculado con libexample.so y la muestra como main.out.

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

La descripción de cada bandera, aparte de la mencionada anteriormente, será la siguiente

Bandera Descripción
-L. Establecer la ruta de inclusión de la biblioteca compartida en el mismo directorio.
 -lexample Vincular con la biblioteca compartida libexample.so.

Por último, iniciaremos el archivo ejecutable.

$ ./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 puede ver, la función multihilo resulta significativamente más rápida (unas 4,33 veces más) que la función de un solo hilo.

Ya sabemos cómo compilar y construir tanto una biblioteca compartida como un programa principal usando la línea de comandos. Ahora crearemos el sistema de compilación requerido usando Makefile.
Para este propósito, podríamos utilizar CMake, pero, como estamos desarrollando principalmente en Linux, CMake parece redundante. No necesitamos esta compatibilidad para realizar desarrollos para Windows, así que elegiremos Makefile.

Tendremos tres variantes de Makefile.

  1. Makefile
    Ha sido diseñado para la compilación cruzada tanto en Linux como en Windows. Aplica pthread, y se utiliza para construir una DLL que funcione con MetaTrader 5, además del programa de prueba principal, que se puede ejecutar a través de Wine.

  2. Makefile-th_win32
    Es lo mismo que Makefile, pero utiliza un hilo win32.

  3. Makefile-g++
    Ha sido diseñado para compilar en un sistema Linux nativo. Hablamos de los pasos que acabamos de dar más arriba.

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

El código de las tres variantes de Makefiles mostradas es casi idéntico con algunas diferencias mínimas,

a saber:

  • el nombre del archivo binario del compilador
  • -DUSE_MINGW_STD_THREAD
  • la presencia/ausencia de -lpthread
  • el nombre del archivo binario en la salida, por ejemplo libexample.so o example.dll, y main.out o main.exe según el sistema objetivo de la compilación


MORE_FLAGS se declara como

MORE_FLAGS ?=

lo cual significa que permite a los usuarios transmitir banderas de compilación adicionales desde la línea de comandos, por lo que el usuario podrá añadir banderas adicionales a demanda según sea necesario. Si las banderas no son transmitidas externamente por parte de los usuarios, se utilizará lo que esté ya definido en el código Makefile.

Luego haremos ejecutables todos los archivos Makefile

$ chmod 755 Makefile*

La información para construir la variante Makefile se muestra en el siguiente recuadro.

Sistema objetivo Equipo de ensamblaje  Comando de limpieza
Compilación cruzada con pthread make  make clean
Compilación cruzada usando win32 thread make -f Makefile-th_win32  make -f Makefile-th_win32 clean
Linux nativo make -f Makefile-g++  make -f Makefile-g++ clean

Creamos una DLL para su uso con MetaTrader 5 y Wine. De esta forma, podremos probar ambos.

Ejecutamos

$ make

Se generarán los siguientes archivos

  1. example.dll
  2. main.exe


Ejecución de prueba del archivo ejecutable de compilación 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

Bien, tenemos un problema. main.exe no puede encontrar las DLL necesarias.
La solución sería ponerlos en el mismo directorio que nuestro ejecutable.

Necesitaremos las siguientes DLL:

  • libgcc_s_seh-1.dll
    Se usa para ofrecer soporte a la gestión de excepciones de C++ y otras funciones de bajo nivel no soportadas originalmente por Windows.

  • libstdc++6.dll
    Base para mantener un programa C++. Contiene las funciones y clases utilizadas para realizar diversas operaciones, como la entrada y salida de datos, operaciones matemáticas y gestión de memoria.

  • libwinpthread-1.dll
    Implementación de la API pthread para Windows.
    Es posible que esta DLL no aparezca en la salida del terminal, pero dependerá de las dos DLL anteriormente mencionadas.

Como hemos instalado Mingw, estas DLL ya se encuentran en nuestro sistema Linux. Solo tenemos que encontrarlas.

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

Este comando descubre libgcc_s_seh-1.dll, ignorando los directorios (use -type f), ya que la búsqueda comienza desde el directorio raíz (utilice /). Si se produce un error, restableceremos a /dev/null (utilizando 2>/dev/null).

Veremos la salida correspondiente para 

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

Preste atención a win32 y posix como parte del nombre del directorio. Al construir con Makefile, copie esta DLL desde un directorio basado en posix. Al construir Makefile-th_win32, copie la DLL desde un directorio basado en win32.

Como hemos decidido basar nuestro trabajo principalmente en pthread, le propongo lo siguiente:

  • Copie la DLL del directorio posix al directorio de nuestro proyecto (el mismo directorio que el archivo binario ejecutable).
  • Puede que necesitemos probar un hilo win32, así que podemos crear un directorio win32 y otro posix y luego copiar las DLL correspondientes en cada directorio.
    Cada vez que deseemos copiar un hilo en particular, copiaremos la DLL creada y el archivo ejecutable a un directorio win32 o posix y luego ejecutaremos el programa desde allí a través de Wine o viceversa.

Finalmente, podemos probar el programa del siguiente modo

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

Ignoraremos las líneas de salida irrelevantes, que son advertencias y errores menores del propio Wine.

Podemos ver que la función multihilo es unas 3,4 veces más rápida que la función de un solo hilo. Eso sí, sigue resultando poco más lenta que la versión nativa de Linux, lo cual es comprensible.
Regresaremos al tema de las pruebas más adelante, cuando hayamos terminado de implementar el código MQL5 necesario.

Ya tenemos todo listo para implementar el código MQL5.


Experimento, etapa de desarrollo II - Código MQL5 para usar una DLL

Hemos tenido que recorrer un largo camino antes de la segunda fase de desarrollo del código MQL5.

Implementación del 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 ejecutar el código MQL5 como un asesor o indicador, pero para cumplir los propósitos de nuestro experimento, necesitaremos comprobar todos los pasos y el proceso de trabajo completo. Para ello, lo más adecuado será un script.
En una situación real solemos necesitar asesores o indicadores para obtener los datos del terminal, por ejemplo OnTick(), OnTrade(), OnCalculate(). Para más información sobre qué funciones soporta cada tipo de programa en MetaTrader 5, consulte el apartado "Ejecución de programas"

Ahora vamos a analizar el código anterior fragmento a fragmento.

Importación de signaturas de funciones desde una 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 llamar a las funciones derivadas de la DLL, deberemos declarar estas signaturas nuevamente en el código MQL5.

Qué debemos tener en cuenta:

  • Podremos omitir los nombres de los parámetros de las funciones, por ejemplo add(int, int) y sub(int, int).
  • El array se transmite en forma de enlace solo en MQL5. Preste atención a la diferencia entre las signaturas declaradas en el código DLL y MQL5. En el código MQL5 existe & (ampersand), mientras que en el código DLL, no lo hay.
    Tenga en cuenta que la sintaxis C++ usada en MQL5 y la sintaxis C++ estándar no coinciden perfectamente. Cada vez que transmitamos un array a MQL5, necesitaremos añadir &.

Luego crearemos un array de un gran conjunto de datos

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

Esto creará un array de números enteros para 1000 millones de elementos y pondrá cada elemento a 1. El array es dinámico y se almacena en memoria dinámica. En la pila no hay espacio suficiente para almacenar tal cantidad de datos.
Así, para hacer dinámico un array, utilizaremos la sintaxis de declaración int arr[].

A continuación, deberemos llamar a cada función DLL desde las signaturas declaradas según sea necesario. Luego comprobaremos el resultado. Si es incorrecto, se enviará Alert() al usuario. No saldremos enseguida.

Utilizaremos ArraySize() para obtener el número de elementos del array. Para transmitir un array a una función, bastará con transmitir directamente su variable a la función.

Luego compilaremos el script y finalizaremos la implementación.


Después copiaremos todas las DLL necesarias en MetaTrader 5

Antes de ejecutar el script MQL5, copiaremos todas las DLL necesarias en el directorio <terminal>/Libraries. La ruta completa suele ser la siguiente: ~/.mt5/drive_c/Program Files/MetaTrader 5/MQL5/Libraries.
Aquí es donde MetaTrader 5 buscará cualquier DLL necesaria según los programas que hayamos creado para MetaTrader 5. Volveremos al apartado "Probar la ejecución del archivo de compilación cruzada ejecutable" para ver la lista de DLL que debemos copiar.

Por defecto, el script oficial de instalación de MetaTrader 5 instalará automáticamente Wine con el prefijo ~/.mt5 . Esto solo se referirá a los usuarios que aplican el script de instalación oficial.


Simulación

Arrastramos la TestConsumeDLL compilada al gráfico

Arrastramos la TestConsumeDLL compilada al gráfico para iniciar la ejecución

Primero vamos a probar la ejecución de MetaTrader 5 a través de Wine en Linux.
Arrastramos la TestConsumeDLL compilada al gráfico. Entonces veremos una ventana de diálogo solicitando permiso para importar desde una DLL, así como una lista de dependencias DLL para el programa MQL5 que hemos creado.

Ventana de diálogo que solicita permiso para importar una DLL junto con una lista de dependencias DLL

Ventana de diálogo que solicita permiso para importar una DLL junto con una lista de dependencias DLL.

Aunque, eso sí, no hemos visto libwinpthread-1.dll, porque no es una dependencia directa del script MQL5 compilado, sino una dependencia tanto para libgcc_s_seh-1.dll como para libstdc++6.dll. Podemos comprobar la dependencia DLL del archivo DLL de destino con objdump de la forma siguiente.

$ 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 puede leer un archivo binario (biblioteca compartida o ejecutable) creado en Windows y Linux, y resulta lo suficientemente versátil como para descargar la información disponible según sea necesario. La bandera -x indica la representación del contenido de todos los encabezados.

El resultado se muestra en la pestaña "Expertos".

Resultados de la ejecución de TestConsumeDLL en la pestaña "Expertos"

Resultados de la ejecución de TestConsumeDLL en la pestaña "Expertos"


También se muestra el tiempo necesario para ejecutar cada función en la ventana de terminal original usada para ejecutar MetaTrader 5 en Linux.

El tiempo transcurrido se muestra en la consola para cada función

En la misma ventana de terminal utilizada para iniciar MetaTrader 5, los usuarios verán el tiempo invertido en la muestra de la ejecución de la DLL

Hasta ahora no hemos visto ningún aviso Alerts()y el tiempo de ejecución se muestra correctamente. A nuestro programa piloto le falta muy poco para estar listo.


Pruebas en Windows

Necesitaremos lo siguiente:

  • Virtualbox con adiciones de invitados instaladas
    La información sobre la instalación puede encontrarse en internet, por lo que no tiene sentido alargar el artículo.
    Importante: las adiciones de invitados son necesarias para utilizar las funciones de compartición de datos entre el equipo anfitrión y el invitado, para copiar example.dll junto con otras muchas DLL en la máquina huésped (máquina Windows).

  • Imagen ISO de 64 bits Windows 7+
    La imagen deberá descargarse e instalarse en el disco duro a través de Virtualbox.

Interfaz principal de VirtualBox

Interfaz principal de VirtualBox. Dependerá de los recursos de hardware disponibles que puedan asignarse. Cuantos más mejor, si necesitamos comprobar la velocidad de ejecución en una DLL


Dependerá mucho de los recursos de nuestra máquina que se puedan destinar a la ejecución de Windows a través de Virtualbox para probar la velocidad de ejecución desde una DLL. En mi caso, la configuración será la siguiente:

  • Sistema -> Placa base -> RAM ajustada a 20480 MB o 20 GB (tengo 32 GB en mi host)
  • Sistema -> Procesador -> Procesador(es) igual(es) a 6 con un límite de ejecución del 100% (6 es el valor máximo permitido aquí)
  • Display -> Screen -> Memoria de vídeo establecida al máximo (esto es opcional, pero resulta útil si usamos varios monitores; cuantos más monitores, más memoria de vídeo se necesitará)
  • Display -> Pantalla -> Número de monitores establecido en 1

Ha llegado el momento de hacer las pruebas. Podemos copiar el código MQL5 compilado de una máquina Linux o simplemente copiar todo el código y luego usar el MetaEditor para compilarlo nuevamente en una máquina Windows.
He descubierto que esta última opción resulta bastante adecuada. Se trata nuevamente de copiar y pegar, por eso la he elegido.

Resultados de la prueba

Resultados de TestConsumeDLL en la pestaña "Expertos" de Windows

Resultados en la pestaña "Expertos" de Windows


El problema es que el tiempo de ejecución está codificado para ser mostrado a través de la salida estándar (stdout), y no puedo encontrar una forma de guardar dicha salida cuando MetaTrader 5 se ejecuta en Windows. He intentado iniciar MetaTrader 5 con un archivo de configuración para ejecutar la secuencia de comandos desde el principio y luego redirigir la salida a un archivo, pero el intento ha fallado, ya que MetaTrader 5 no permite cargar cualquier DLL cuando se ejecuta desde la línea de comandos. Para solucionar esto sin interferir con el código principal de la DLL, realizaremos un pequeño ajuste en el código MQL5 para calcular el tiempo transcurrido a partir de ahí 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 ---");
}

Preste atención al comentario "// *". Son líneas adicionales a las que hay que prestar atención.

Hagamos la prueba de nuevo.

Resultados de la nueva prueba de TestConsumeDLL en Windows en la pestaña "Expertos"

Probado en Windows el código MQL5 actualizado para medir el tiempo de ejecución


Completamos la aplicación experimental creando una DLL con soporte multihilo, que luego utilizamos en el código MQL5. Después la probamos en Linux y Windows. Todo funciona correctamente.


Prueba sencilla de ambas implementaciones de los hilos Mingw

Realizaremos una prueba sencilla usando nuestro programa experimental, ya que para efectuar una prueba completa de las capacidades multihilo en C++ en diferentes plataformas deberemos tener en cuenta muchos factores, incluyendo múltiples primitivas de sincronización, thread_local, dominio del problema, etc.

La prueba se realizará del siguiente modo:

  • Linux
    • Realizamos el ensamblaje con ayuda de Makefile. A continuación, ejecutaremos la prueba cinco veces antes de promediar, y haremos lo mismo para Makefile-th_win32
    • Luego ejecutaremos el archivo binario con ayuda de WINEPREFIX=~/.mt5 wine main.exe
    • Utilizaremos los 12 hilos y los 32 GB de RAM disponibles.
  • Windows
    • Realizamos el ensamblaje con ayuda de Makefile. A continuación, ejecutaremos la prueba cinco veces antes de promediar, y haremos lo mismo para Makefile-th_win32
    • Luego copiaremos las DLL y los archivos ejecutables necesarios en la máquina huésped (Windows) utilizando Virtualbox
    • Ejecutaremos el archivo binario utilizando la línea de comandos main.exe
    • Límite de 6 hilos y 20 GB de RAM (ambos debidos al cumplimiento de la configuración permitida en VirtualBox).

Los resultados se redondearán a dos decimales,

y se mostrarán en la tabla siguiente.

Función  Linux + pthread (ms) Linux + hilo win32 (ms) Windows + pthread (ms) Windows + hilo 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


Conclusión

Mingw y Wine son herramientas multiplataforma que permiten a los desarrolladores usar Linux para crear aplicaciones multiplataforma que funcionen a la perfección tanto en Linux como en Windows. Esto se aplica igualmente al desarrollo de aplicaciones para MetaTrader 5. Nuestra aplicación experimental para desarrollar DLL multihilo en C++, probada tanto en Linux como en Windows, ofrece opciones alternativas para ampliar el acceso de los desarrolladores al entorno.



Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/12042

Archivos adjuntos |
ExampleLib.zip (5.37 KB)
Recetas MQL5 - Base de datos de eventos macroeconómicos Recetas MQL5 - Base de datos de eventos macroeconómicos
El presente artículo analiza las posibilidades de trabajar con bases de datos que utilizan el motor SQLite como base. Hemos creado una clase CDatabase para usar de forma cómoda y eficaz los principios de la programación orientada a objetos. Posteriormente se utilizará para crear y gestionar una base de datos de eventos macroeconómicos. Asimismo, ofreceremos ejemplos de muchos métodos de la clase CDatabase.
Algoritmos de optimización de la población: Algoritmo de forrajeo bacteriano (Bacterial Foraging Optimisation — BFO) Algoritmos de optimización de la población: Algoritmo de forrajeo bacteriano (Bacterial Foraging Optimisation — BFO)
La estrategia de búsqueda de alimento de la bacteria E.coli inspiró a los científicos para crear el algoritmo de optimización BFO. El algoritmo contiene ideas originales y enfoques prometedores para la optimización y merece ser investigado en profundidad.
Teoría de categorías en MQL5 (Parte 2) Teoría de categorías en MQL5 (Parte 2)
La teoría de categorías es una rama diversa y en expansión de las matemáticas, relativamente inexplorada aún en la comunidad MQL5. Esta serie de artículos tiene como objetivo destacar algunos de sus conceptos para crear una biblioteca abierta y seguir utilizando esta maravillosa sección para crear estrategias comerciales.
De nuevo sobre el sistema de Murray De nuevo sobre el sistema de Murray
Los sistemas gráficos de análisis de precios son merecidamente populares entre los tráders. En este artículo, hablaremos sobre el sistema completo de Murray, que incluye no solo sus famosos niveles, sino también algunas otras técnicas útiles para valorar la posición actual del precio y tomar una decisión comercial.