Implementaciones alternativas de funciones/enfoques estándar

 

NormalizarDoble

#define  EPSILON (1.0 e-7 + 1.0 e-13)
#define  HALF_PLUS  (0.5 + EPSILON)

double MyNormalizeDouble( const double Value, const int digits )
{
  // Добавление static ускоряет код в три раза (Optimize=0)!
  static const double Points[] = {1.0 e-0, 1.0 e-1, 1.0 e-2, 1.0 e-3, 1.0 e-4, 1.0 e-5, 1.0 e-6, 1.0 e-7, 1.0 e-8};

  return((int)((Value > 0) ? Value / Points[digits] + HALF_PLUS : Value / Points[digits] - HALF_PLUS) * Points[digits]);
}

ulong Bench( const int Amount = 1.0 e8 )
{
  double Price = 1.23456;
  const double point = 0.00001;
  
  const ulong StartTime = GetMicrosecondCount();
  
  int Tmp = 0;
  
  for (int i = 0; i < Amount; i++)
  {
    Price = NormalizeDouble(Price + point, 5); // замените на MyNormalizeDouble и почувствуйте разницу
    
    // Если убрать, то общее время выполнения будет нулевым при любом Amount (Optimize=1) - круто! В варианте NormalizeDouble оптимизации такой не будет.  
    if (i + i > Amount + Amount)
      return(0);
  }
  
  return(GetMicrosecondCount() - StartTime);
}

void OnStart( void )
{
  Print(Bench());
    
  return;
};

El resultado es 1123275 y 1666643 a favor de MyNormalizeDouble (Optimize=1). Sin optimización, es cuatro veces más rápido (en memoria).


 

Si sustituye

static const double Points[] = {1.0 e-0, 1.0 e-1, 1.0 e-2, 1.0 e-3, 1.0 e-4, 1.0 e-5, 1.0 e-6, 1.0 e-7, 1.0 e-8};

a la variante del interruptor, se puede ver la calidad de la implementación del interruptor en números.

 

Considere la versión limpia del script con NormalizeDouble:

#define  EPSILON (1.0 e-7 + 1.0 e-13)
#define  HALF_PLUS  (0.5 + EPSILON)
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double MyNormalizeDouble(const double Value,const int digits)
  {
   static const double Points[]={1.0 e-0,1.0 e-1,1.0 e-2,1.0 e-3,1.0 e-4,1.0 e-5,1.0 e-6,1.0 e-7,1.0 e-8};

   return((int)((Value > 0) ? Value / Points[digits] + HALF_PLUS : Value / Points[digits] - HALF_PLUS) * Points[digits]);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
ulong BenchStandard(const int Amount=1.0 e8)
  {
   double       Price=1.23456;
   const double point=0.00001;
   const ulong  StartTime=GetMicrosecondCount();
//---
   for(int i=0; i<Amount;i++)
     {
      Price=NormalizeDouble(Price+point,5);
     }
   
   Print("Result: ",Price);   // специально выводим результат, чтобы цикл не оптимизировался в ноль
//---
   return(GetMicrosecondCount() - StartTime);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
ulong BenchCustom(const int Amount=1.0 e8)
  {
   double       Price=1.23456;
   const double point=0.00001;
   const ulong  StartTime=GetMicrosecondCount();
//---
   for(int i=0; i<Amount;i++)
     {
      Price=MyNormalizeDouble(Price+point,5);
     }
   
   Print("Result: ",Price);   // специально выводим результат, чтобы цикл не оптимизировался в ноль
//---
   return(GetMicrosecondCount() - StartTime);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   Print("Standard: ",BenchStandard()," msc");
   Print("Custom:   ",BenchCustom(),  " msc");
  }

Resultados:

Custom:   1110255 msc
Result:   1001.23456

Standard: 1684165 msc
Result:   1001.23456

Observaciones y explicaciones inmediatas:

  1. static es necesario aquí para que el compilador tome este array fuera de la función y no lo construya en la pila cada vez que se llame a la función. El compilador de C++ hace lo mismo.
    static const double Points
  2. Para evitar que el compilador deseche el bucle por ser inútil, debemos utilizar los resultados de los cálculos. Por ejemplo, imprime la variable Precio.

  3. Hay un error en su función: no se comprueban los límites de los dígitos, lo que puede llevar fácilmente a desbordamientos de la matriz.

    Por ejemplo, llámelo como MyNormalizeDouble(Price+point,10) y capture el error:
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    El método de acelerar no verificando es aceptable, pero no en nuestro caso. Debemos manejar cualquier entrada de datos errónea.

  4. Añadamos una condición simple para un índice mayor que 8. Para simplificar el código, sustituya el tipo de la variable dígitos por uint, para hacer una comparación para >8 en lugar de una condición adicional <0
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    double MyNormalizeDouble(const double Value,uint digits)
      {
       static const double Points[]={1.0 e-0,1.0 e-1,1.0 e-2,1.0 e-3,1.0 e-4,1.0 e-5,1.0 e-6,1.0 e-7,1.0 e-8};
    //---
       if(digits>8)
          digits=8;
    //---
       return((int)((Value > 0) ? Value / Points[digits] + HALF_PLUS : Value / Points[digits] - HALF_PLUS) * Points[digits]);
      }
    

  5. Vamos a ejecutar el código y... Estamos sorprendidos.
    Custom:   1099705 msc
    Result:   1001.23456
    
    Standard: 1695662 msc
    Result:   1001.23456
    
    Su código ha superado aún más la función estándar NormalizeDouble.

    Además, la adición de la condición incluso reduce el tiempo (en realidad está dentro del margen de error). ¿Por qué hay tanta diferencia de velocidad?

  6. Todo esto tiene que ver con un error estándar de los probadores de rendimiento.

    Al escribir las pruebas hay que tener en cuenta la lista completa de optimizaciones que puede aplicar el compilador. Debe tener claro qué datos de entrada va a utilizar y cómo se van a destruir cuando escriba una prueba de muestra simplificada.

    Evaluemos y apliquemos todo el conjunto de optimizaciones que realiza nuestro compilador, paso a paso.

  7. Empecemos por la propagación constante: es uno de los errores importantes que ha cometido en esta prueba.

    Tienes la mitad de tus datos de entrada como constantes. Reescribamos el ejemplo teniendo en cuenta su propagación.

    ulong BenchStandard(void)
      {
       double      Price=1.23456;
       const ulong StartTime=GetMicrosecondCount();
    //---
       for(int i=0; i<1.0 e8;i++)
         {
          Price=NormalizeDouble(Price + 0.00001,5);
         }
    
       Print("Result: ",Price);
    //---
       return(GetMicrosecondCount() - StartTime);
      }
    
    ulong BenchCustom(void)
      {
       double      Price=1.23456;
       const ulong StartTime=GetMicrosecondCount();
    //---
       for(int i=0; i<1.0 e8;i++)
         {
          Price=MyNormalizeDouble(Price + 0.00001,5);
         }
    
       Print("Result: ",Price," ",1.0 e8);
    //---
       return(GetMicrosecondCount() - StartTime);
      }
    
    Después de lanzarlo, nada ha cambiado - debe ser así.

  8. Adelante - inline su código (nuestro NormalizeDouble no puede ser inline)

    Esto es lo que su función se convertirá en realidad después de inelining. El ahorro en las llamadas, el ahorro en la obtención de matrices, las comprobaciones se eliminan debido al análisis constante:
    ulong BenchCustom(void)
      {
       double              Price=1.23456;
       const ulong         StartTime=GetMicrosecondCount();
    //---
       for(int i=0; i<1.0 e8;i++)
         {
          //--- этот код полностью вырезается, так как у нас заведомо константа 5
          //if(digits>8)
          //   digits=8;
          //--- распространяем переменные и активно заменяем константы
          if((Price+0.00001)>0)
             Price=int((Price+0.00001)/1.0 e-5+(0.5+1.0 e-7+1.0 e-13))*1.0 e-5;
          else
             Price=int((Price+0.00001)/1.0 e-5-(0.5+1.0 e-7+1.0 e-13))*1.0 e-5;
         }
    
       Print("Result: ",Price);
    //---
       return(GetMicrosecondCount() - StartTime);
      }
    
    No he resumido las constantes puras para ahorrar tiempo. todas están garantizadas para colapsar en tiempo de compilación.

    Ejecuta el código y obtén el mismo tiempo que en la versión original:
    Custom:   1149536 msc
    Standard: 1767592 msc
    
    no te preocupes por el parloteo de los números - a nivel de microsegundos, error del temporizador y carga flotante en el ordenador, esto está dentro de los límites normales. la proporción se mantiene completamente.

  9. Mira el código que realmente comenzó a probar debido a los datos de origen fijo.

    Como el compilador tiene una optimización muy potente, su tarea se ha simplificado de forma efectiva.


  10. Entonces, ¿cómo se debe comprobar el rendimiento?

    Al entender cómo funciona el compilador, hay que evitar que aplique preoptimizaciones y simplificaciones.

    Por ejemplo, hagamos variable el parámetro dígitos:

    #define  EPSILON (1.0 e-7 + 1.0 e-13)
    #define  HALF_PLUS  (0.5 + EPSILON)
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    double MyNormalizeDouble(const double Value,uint digits)
      {
       static const double Points[]={1.0 e-0,1.0 e-1,1.0 e-2,1.0 e-3,1.0 e-4,1.0 e-5,1.0 e-6,1.0 e-7,1.0 e-8};
    //---
       if(digits>8)
          digits=8;
    //---   
       return((int)((Value > 0) ? Value / Points[digits] + HALF_PLUS : Value / Points[digits] - HALF_PLUS) * Points[digits]);
      }
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    ulong BenchStandard(const int Amount=1.0 e8)
      {
       double       Price=1.23456;
       const double point=0.00001;
       const ulong  StartTime=GetMicrosecondCount();
    //---
       for(int i=0; i<Amount;i++)
         {
          Price=NormalizeDouble(Price+point,2+(i&15));
         }
    
       Print("Result: ",Price);   // специально выводим результат, чтобы цикл не оптимизировался в ноль
    //---
       return(GetMicrosecondCount() - StartTime);
      }
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    ulong BenchCustom(const int Amount=1.0 e8)
      {
       double       Price=1.23456;
       const double point=0.00001;
       const ulong  StartTime=GetMicrosecondCount();
    //---
       for(int i=0; i<Amount;i++)
         {
          Price=MyNormalizeDouble(Price+point,2+(i&15));
         }
    
       Print("Result: ",Price);   // специально выводим результат, чтобы цикл не оптимизировался в ноль
    //---
       return(GetMicrosecondCount() - StartTime);
      }
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    void OnStart(void)
      {
       Print("Standard: ",BenchStandard()," msc");
       Print("Custom:   ",BenchCustom()," msc");
      }
    
    Ejecútalo y... obtenemos el mismo resultado de velocidad que antes.

    Su código gana un 35% como antes.

  11. ¿Y por qué es así?

    Todavía no podemos salvarnos de la optimización debido al inlining. Ahorrar 100 000 000 de llamadas pasando los datos a través de la pila a nuestra función NormalizarDoble, que es similar en la implementación, bien podría dar el mismo aumento de velocidad.

    Hay otra sospecha de que nuestro NormalizeDouble no ha sido implementado en el mecanismo direct_call cuando se carga la tabla de reubicación de funciones en el programa MQL5.

    Lo comprobaremos por la mañana y si es así, lo pasaremos a direct_call y comprobaremos de nuevo la velocidad.

Aquí hay un estudio de NormalizeDouble.

Nuestro compilador MQL5 ha superado la función de nuestro sistema, lo que demuestra su idoneidad cuando se compara con la velocidad del código C++.

 
fxsaber:

Si sustituye

a la variante del interruptor, se puede ver la calidad de la implementación del interruptor en números.

Estás confundiendo el acceso indexado directo a un array estático mediante un índice constante (que degenera en una constante de un campo) y el switch.

Switch no puede competir con un caso así. Switch tiene varias optimizaciones de uso frecuente de la forma:

  • "los valores notoriamente ordenados y cortos se ponen en un array estático y se indexan" - el más sencillo y rápido, puede competir con el array estático, pero no siempre
  • "varias matrices por trozos de valores ordenados y cerrados con comprobaciones de límites de zona" - esto ya tiene un freno
  • "comprobamos muy pocos valores a través de if" - no hay velocidad, pero es culpa del propio programador, que utiliza el switch de forma inapropiada
  • "tabla ordenada muy dispersa con búsqueda binaria" - muy lento para los peores casos

De hecho, la mejor estrategia para el cambio es cuando el desarrollador trató deliberadamente de hacer un conjunto compacto de valores en el conjunto inferior de números.

 
Renat Fatkhullin:

Considere la versión limpiada del script con NormalizeDouble:

Resultados:


Observaciones y explicaciones inmediatas:

  1. static es necesario aquí para que el compilador ponga este array fuera de la función y no lo construya en la pila en cada llamada a la función. El compilador de C++ hace lo mismo.
Con "Optimizar=0" este es el caso. Con "Optimize=1", puedes incluso desecharlo - el compilador optimizador es inteligente, como resulta.
  1. Para evitar que el compilador deseche el bucle por su inutilidad, debemos utilizar los resultados de los cálculos. Por ejemplo, imprime la variable Precio.
¡Qué truco más chulo!
  1. Hay un error en su función que no comprueba los límites de los dígitos, lo que puede llevar fácilmente a desbordamientos del array.

    Por ejemplo, llámelo como MyNormalizeDouble(Price+point,10) y capture el error:
    El método de acelerar no verificando es aceptable, pero no en nuestro caso. Debemos manejar cualquier entrada de datos errónea.

  2. Añadamos una condición simple sobre el índice mayor que 8. Para simplificar el código, sustituyamos el tipo de la variable dígitos por uint, para hacer una comparación para >8 en lugar de la condición adicional <0
¡Parece que es más óptimo!
double MyNormalizeDouble( const double Value, const uint digits )
{
  static const double Points[] = {1.0 e-0, 1.0 e-1, 1.0 e-2, 1.0 e-3, 1.0 e-4, 1.0 e-5, 1.0 e-6, 1.0 e-7, 1.0 e-8};
  const double point = digits > 8 ? 1.0 e-8 : Points[digits];

  return((int)((Value > 0) ? Value / point + HALF_PLUS : Value / point - HALF_PLUS) * point);
}
  1. Este es un error habitual de los probadores de rendimiento.

    Al escribir las pruebas debemos tener en cuenta la lista completa de optimizaciones que puede aplicar el compilador. Debe tener claro qué datos de entrada va a utilizar y cómo se van a destruir cuando escriba una prueba de muestra simplificada.
  2. Entonces, ¿cómo se debe comprobar el rendimiento?

    Al entender cómo funciona el compilador, hay que evitar que aplique preoptimizaciones y simplificaciones.

    Por ejemplo, hagamos variable el parámetro dígitos:
Muchas gracias por las exhaustivas explicaciones sobre cómo preparar correctamente las medidas de rendimiento del compilador. Realmente no se tuvo en cuenta la posibilidad de optimizar la constante.

Este es el estudio NormalizeDouble.

Nuestro compilador MQL5 superó la función de nuestro sistema, lo que demuestra su idoneidad cuando se compara con la velocidad del código C++.

Sí, este resultado es una cuestión de orgullo.
 
Renat Fatkhullin:

Estás confundiendo el acceso indexado directo a un array estático mediante un índice constante (que degenera en una constante de un campo) y el switch.

Switch no puede competir con un caso así. Switch cuenta con algunas optimizaciones de uso común del tipo:

  • El "ordenado deliberadamente y los valores cortos se ponen en un array estático y se indexan por interruptor" es el más simple y rápido, y puede competir con un array estático, pero no siempre.

Este es precisamente un caso de pedido.

De hecho, la mejor estrategia para el cambio es cuando el desarrollador ha intentado deliberadamente hacer un conjunto compacto de valores en el conjunto inferior de números.

Lo he probado en un sistema de 32 bits. Allí, la sustitución del interruptor en el ejemplo anterior provocó graves retrasos. No lo he probado en una máquina nueva.
 
fxsaber:

Este es un caso de orden.

Tenemos que comprobarlo por separado, pero más tarde.


Lo he probado en un sistema de 32 bits. En este caso, la sustitución del interruptor en el ejemplo anterior provocó una frenada grave. No lo he comprobado en la nueva máquina.

En realidad hay dos programas compilados en cada MQL5: uno simplificado para 32 bits y otro optimizado al máximo para 64 bits. En MT5 de 32 bits el nuevo optimizador no se aplica en absoluto y el código para las operaciones de 32 bits es tan simple como MQL4 en MT4.

Toda la eficiencia del compilador que puede generar código diez veces más rápido sólo cuando se ejecuta en la versión de 64 bits de MT5: https://www.mql5.com/ru/forum/58241

Estamos totalmente centrados en las versiones de 64 bits de la plataforma.

 

Sobre el tema de NormalizeDouble existe esta tontería

Foro sobre comercio, sistemas de comercio automatizados y pruebas de estrategias

¿Cómo puedo pasar por una enumeración de forma coherente?

fxsaber, 2016.08.26 16:08

Hay esta nota en la descripción de la función

Esto sólo es cierto para los símbolos que tienen un paso de precio mínimo de 10^N, donde N es un número entero y no positivo. Si el paso de precio mínimo tiene un valor diferente, entonces la normalización de los niveles de precio antes de OrderSend es una operación sin sentido que devolverá un falso OrderSend en la mayoría de los casos.


Es una buena idea corregir las representaciones obsoletas en la ayuda.

NormalizeDouble está completamente desacreditado. No sólo se trata de una implementación lenta, sino que además carece de sentido en múltiples símbolos de intercambio (por ejemplo, RTS, MIX, etc.).

NormalizeDouble fue creado originalmente por usted para las operaciones de Order*. Principalmente por los precios y los lotes. Pero aparecieron TickSize y VolumeStep no estándar. Y la función es simplemente obsoleta. Por ello, escriben un código lento. Un ejemplo de la biblioteca estándar
double CTrade::CheckVolume(const string symbol,double volume,double price,ENUM_ORDER_TYPE order_type)
  {
//--- check
   if(order_type!=ORDER_TYPE_BUY && order_type!=ORDER_TYPE_SELL)
      return(0.0);
   double free_margin=AccountInfoDouble(ACCOUNT_FREEMARGIN);
   if(free_margin<=0.0)
      return(0.0);
//--- clean
   ClearStructures();
//--- setting request
   m_request.action=TRADE_ACTION_DEAL;
   m_request.symbol=symbol;
   m_request.volume=volume;
   m_request.type  =order_type;
   m_request.price =price;
//--- action and return the result
   if(!::OrderCheck(m_request,m_check_result) && m_check_result.margin_free<0.0)
     {
      double coeff=free_margin/(free_margin-m_check_result.margin_free);
      double lots=NormalizeDouble(volume*coeff,2);
      if(lots<volume)
        {
         //--- normalize and check limits
         double stepvol=SymbolInfoDouble(symbol,SYMBOL_VOLUME_STEP);
         if(stepvol>0.0)
            volume=stepvol*(MathFloor(lots/stepvol)-1);
         //---
         double minvol=SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN);
         if(volume<minvol)
            volume=0.0;
        }
     }
   return(volume);
  }

Bueno, ¡no se puede hacer tan torpemente! Podría ser muchas veces más rápido, olvidándose de NormalizeDouble.

double NormalizePrice( const double dPrice, double dPoint = 0 )
{
  if (dPoint == 0) 
    dPoint = ::SymbolInfoDouble(::Symbol(), SYMBOL_TRADE_TICK_SIZE);

  return((int)((dPrice > 0) ? dPrice / dPoint + HALF_PLUS : dPrice / dPoint - HALF_PLUS) * dPoint);
}

Y para el mismo volumen entonces hacer

volume = NormalizePrice(volume, stepvol);

Para los precios sí

NormalizePrice(Price, TickSize)

Parece correcto añadir algo similar para sobrecargar el estándar NormalizeDouble. Donde el segundo parámetro "dígitos" será un doble en lugar de un int.

 

En 2016, la mayoría de los compiladores de C++ han llegado a los mismos niveles de optimización.

MSVC hace que uno se pregunte sobre las mejoras con cada actualización, e Intel C++ como compilador se ha fusionado - nunca se ha curado realmente de su "error interno" en proyectos grandes.

Otra de nuestras mejoras en el compilador en la compilación 1400 es que es más rápido en la compilación de proyectos complejos.

 

Sobre el tema. Hay que crear alternativas a las funciones estándar, porque a veces dan una salida equivocada. Este es un ejemplo de la alternativa SymbolInfoTick

// Получение тика, который на самом деле вызвал крайнее событие NewTick
bool MySymbolInfoTick( const string Symb, MqlTick &Tick, const uint Type = COPY_TICKS_ALL )
{
  MqlTick Ticks[];
  const int Amount = ::CopyTicks(Symb, Ticks, Type, 0, 1);
  const bool Res = (Amount > 0);
  
  if (Res)
    Tick = Ticks[Amount - 1];
  
  return(Res);
}

// Возвращает в точности то, что SymbolInfoTick
bool CloneSymbolInfoTick( const string Symb, MqlTick &Tick )
{
  MqlTick TickAll, TickTrade, TickInfo;
  const bool Res = (MySymbolInfoTick(Symb, TickAll) &&
                    MySymbolInfoTick(Symb, TickTrade, COPY_TICKS_TRADE) &&
                    MySymbolInfoTick(Symb, TickInfo, COPY_TICKS_INFO));
  
  if (Res)
  {
    Tick = TickInfo;

    Tick.time = TickAll.time;
    Tick.time_msc = TickAll.time_msc;
    Tick.flags = TickAll.flags;
    
    Tick.last = TickTrade.last;
    Tick.volume = TickTrade.volume;    
  }
  
  return(Res);
}

Puede llamar a SymbolInfoTick en cada evento NewTick en el probador y sumar el campo de volumen para conocer la rotación de las acciones. Pero no, no se puede. Tenemos que hacer un MySymbolInfoDouble mucho más lógico.

 
fxsaber:

Sobre el tema de NormalizeDouble existe esta tontería

NormalizeDouble fue creado originalmente por usted para las operaciones de Order*. Principalmente por los precios y los lotes. Pero aparecieron TickSize y VolumeStep no estándar. Y la función es simplemente obsoleta. Por ello, escriben un código lento. Este es un ejemplo de la biblioteca estándar

Bueno, ¡no puede ser tan torpe! Puede ser muchas veces más rápido olvidándose de NormalizeDouble.

Y por el mismo volumen hacer

Para los precios sí

Parece correcto añadir algo así como una sobrecarga al estándar NormalizeDouble. Donde el segundo parámetro "dígitos" será un doble en lugar de un int.

Puedes optimizar todo a su alrededor.

Este es un proceso interminable. Pero en el 99% de los casos no es económicamente rentable.