Implementazioni alternative di funzioni/approcci standard

 

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 ускоряет код в три раза (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;
};

Il risultato è 1123275 e 1666643 a favore di MyNormalizeDouble (Optimize=1). Senza ottimizzazione, è quattro volte più veloce (in memoria).


 

Se si sostituisce

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

alla variante dello switch, si può vedere la qualità dell'implementazione dello switch in numeri.

 

Considerate la versione ripulita dello 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");
  }

Risultati:

Custom:   1110255 msc
Result:   1001.23456

Standard: 1684165 msc
Result:   1001.23456

Osservazioni e spiegazioni immediate:

  1. static è necessario qui in modo che il compilatore prenda questo array fuori dalla funzione e non lo costruisca sullo stack ogni volta che la funzione viene chiamata. Il compilatore C++ fa lo stesso.
    static const double Points
  2. Per evitare che il compilatore butti via il ciclo perché è inutile, dovremmo usare i risultati dei calcoli. Per esempio, stampare la variabile Price.

  3. C'è un errore nella tua funzione - i confini delle cifre non sono controllati, il che può facilmente portare a un overrun dell'array.

    Per esempio, chiamatelo come MyNormalizeDouble(Price+point,10) e catturate l'errore:
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    Il metodo di accelerare non controllando è accettabile, ma non nel nostro caso. Dobbiamo gestire qualsiasi inserimento di dati errati.

  4. Aggiungiamo una semplice condizione per un indice maggiore di 8. Per semplificare il codice, sostituiamo il tipo della variabile digits con uint, per fare un confronto per >8 invece di una condizione aggiuntiva <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. Eseguiamo il codice e... Siamo sorpresi!
    Custom:   1099705 msc
    Result:   1001.23456
    
    Standard: 1695662 msc
    Result:   1001.23456
    
    Il tuo codice ha superato ancora di più la funzione standard NormalizeDouble!

    Inoltre, l'aggiunta della condizione riduce addirittura il tempo (in realtà rientra nel margine di errore). Perché c'è una tale differenza di velocità?

  6. Tutto questo ha a che fare con un errore standard dei tester di prestazioni.

    Quando si scrivono i test si dovrebbe tenere a mente l'elenco completo delle ottimizzazioni che possono essere applicate dal compilatore. Dovete essere chiari su quali dati di input state usando e come saranno distrutti quando scrivete un test di esempio semplificato.

    Valutiamo e applichiamo l'intera serie di ottimizzazioni che il nostro compilatore fa, passo dopo passo.

  7. Cominciamo con la propagazione costante - questo è uno degli errori importanti che hai fatto in questo test.

    Avete metà dei vostri dati di input come costanti. Riscriviamo l'esempio tenendo presente la loro propagazione.

    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);
      }
    
    Dopo averlo lanciato, non è cambiato nulla - deve essere così.

  8. Andate avanti - inlineate il vostro codice (il nostro NormalizeDouble non può essere inlineato)

    Questo è ciò che la vostra funzione diventerà in realtà dopo l'inelining. Il risparmio sulle chiamate, il risparmio sulle ricerche di array, i controlli vengono rimossi grazie all'analisi costante:
    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);
      }
    
    Non ho riassunto le costanti pure per risparmiare tempo. sono tutte garantite per collassare al momento della compilazione.

    Esegui il codice e ottieni lo stesso tempo della versione originale:
    Custom:   1149536 msc
    Standard: 1767592 msc
    
    non prestare attenzione al jitter nei numeri - a livello di microsecondi, errore del timer e carico fluttuante sul computer, questo è entro limiti normali. la proporzione è completamente mantenuta.

  9. Guardate il codice che avete effettivamente iniziato a testare a causa dei dati sorgente fissi.

    Poiché il compilatore ha un'ottimizzazione molto potente, il vostro compito è stato effettivamente semplificato.


  10. Quindi come dovreste testare le prestazioni?

    Comprendendo come funziona il compilatore, dovete impedirgli di applicare preottimizzazioni e semplificazioni.

    Per esempio, rendiamo variabile il parametro digits:

    #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");
      }
    
    Eseguilo e... otteniamo lo stesso risultato di velocità di prima.

    Il vostro codice guadagna circa il 35% come prima.

  11. Allora perché è così?

    Non possiamo ancora salvarci dall'ottimizzazione dovuta all'inlining. Risparmiare 100 000 000 chiamate passando i dati attraverso lo stack nella nostra funzione NormalizeDouble, che ha un'implementazione simile, potrebbe dare lo stesso aumento di velocità.

    C'è un altro sospetto che il nostro NormalizeDouble non sia stato implementato nel meccanismo direct_call quando si carica la tabella di riposizionamento delle funzioni nel programma MQL5.

    Lo controlleremo domattina e se è così, lo sposteremo su direct_call e controlleremo di nuovo la velocità.

Ecco uno studio di NormalizeDouble.

Il nostro compilatore MQL5 ha battuto la nostra funzione di sistema, il che dimostra la sua adeguatezza rispetto alla velocità del codice C++.

 
fxsaber:

Se si sostituisce

alla variante dello switch, si può vedere la qualità dell'implementazione dello switch in numeri.

State confondendo l'accesso diretto indicizzato a un array statico tramite un indice costante (che degenera in una costante da un campo) e lo switch.

Switch non può davvero competere con un caso del genere. Switch ha diverse ottimizzazioni di uso frequente della forma:

  • "i valori notoriamente ordinati e brevi sono messi in un array statico e indicizzati" - il più semplice e veloce, può competere con l'array statico, ma non sempre
  • "diversi array per pezzi di valori ordinati e vicini con controlli dei confini di zona" - questo ha già un freno
  • "controlliamo troppo pochi valori attraverso if" - nessuna velocità, ma è colpa del programmatore, che usa lo switch in modo inappropriato
  • "tabella ordinata molto rada con ricerca binaria" - molto lento nei casi peggiori

Infatti, la migliore strategia per lo switch è quando lo sviluppatore ha deliberatamente cercato di fare un insieme compatto di valori nell'insieme inferiore di numeri.

 
Renat Fatkhullin:

Considerate la versione ripulita dello script con NormalizeDouble:

Risultati:


Osservazioni e spiegazioni immediate:

  1. static è necessario qui perché il compilatore metta questo array fuori dalla funzione e non lo costruisca sullo stack ad ogni chiamata di funzione. Il compilatore C++ fa la stessa cosa.
Con "Optimize=0" questo è il caso. Con "Optimize=1", puoi anche buttarlo via - il compilatore ottimizzatore è intelligente, come si è scoperto.
  1. Per evitare che il compilatore butti via il ciclo a causa della sua inutilità, dobbiamo usare i risultati dei calcoli. Per esempio, stampare la variabile Price.
Che bel trucco!
  1. C'è un errore nella vostra funzione che non controlla i limiti delle cifre, il che può facilmente portare all'overrun dell'array.

    Per esempio, chiamatelo come MyNormalizeDouble(Price+point,10) e catturate l'errore:
    Il metodo di accelerare non controllando è accettabile, ma non nel nostro caso. Dobbiamo gestire qualsiasi inserimento di dati errati.

  2. Aggiungiamo una semplice condizione sull'indice maggiore di 8. Per semplificare il codice, sostituiamo il tipo della variabile digits con uint, per fare un confronto per >8 invece della condizione aggiuntiva <0
Sembra essere più ottimale!
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. Questo è un errore standard dei tester di prestazioni.

    Quando scriviamo i test dovremmo tenere a mente l'elenco completo delle ottimizzazioni che possono essere applicate dal compilatore. Dovete essere chiari su quali dati di input state usando e come saranno distrutti quando scrivete un test di esempio semplificato.
  2. Quindi come dovreste testare le prestazioni?

    Comprendendo come funziona il compilatore, dovete impedirgli di applicare preottimizzazioni e semplificazioni.

    Per esempio, rendiamo variabile il parametro digits:
Grazie mille per le spiegazioni approfondite su come preparare correttamente le misurazioni delle prestazioni del compilatore! Davvero non ha preso in considerazione la possibilità di ottimizzare la costante.

Questo è lo studio NormalizeDouble.

Il nostro compilatore MQL5 ha battuto la nostra funzione di sistema, il che dimostra la sua adeguatezza rispetto alla velocità del codice C++.

Sì, questo risultato è una questione di orgoglio.
 
Renat Fatkhullin:

State confondendo l'accesso diretto indicizzato a un array statico tramite un indice costante (che degenera in una costante da un campo) e lo switch.

Switch non può davvero competere con un caso del genere. Switch ha alcune ottimizzazioni comunemente usate del genere:

  • Il "valori volutamente ordinati e brevi sono messi in un array statico e indicizzati per switch" è il più semplice e veloce, e può competere con un array statico, ma non sempre.

Questo è proprio un caso di ordinazione.

Infatti, la migliore strategia per lo switch è quando lo sviluppatore ha deliberatamente cercato di fare un insieme compatto di valori nella serie inferiore di numeri.

L'ho provato su un sistema a 32 bit. Lì, la sostituzione dell'interruttore nell'esempio precedente ha causato gravi ritardi. Non l'ho testato su una nuova macchina.
 
fxsaber:

Ecco un caso di ordine come questo.

Dobbiamo controllare separatamente, ma più tardi.


L'ho provato su un sistema a 32 bit. Il cambio di interruttore nell'esempio precedente ha causato una grave frenata. Non ho controllato sulla nuova macchina.

Ci sono in realtà due programmi compilati in ogni MQL5: uno semplificato per 32 bit e uno ottimizzato al massimo per 64 bit. In MT5 a 32 bit il nuovo ottimizzatore non si applica affatto e il codice per le operazioni a 32 bit è semplice come MQL4 in MT4.

Tutta l'efficienza del compilatore che può generare codice dieci volte più veloce solo se eseguito nella versione a 64 bit di MT5: https://www.mql5.com/ru/forum/58241

Siamo completamente concentrati sulle versioni a 64 bit della piattaforma.

 

A proposito di NormalizeDouble c'è questa sciocchezza

Forum sul trading, sistemi di trading automatico e test di strategia

Come si fa a passare un'enumerazione in modo coerente?

fxsaber, 2016.08.26 16:08

C'è questa nota nella descrizione della funzione

Questo è vero solo per i simboli che hanno un passo di prezzo minimo 10^N, dove N è intero e non positivo. Se il livello di prezzo minimo ha un valore diverso, allora la normalizzazione dei livelli di prezzo prima di OrderSend è un'operazione senza senso che restituirà OrderSend falso nella maggior parte dei casi.


È una buona idea correggere le rappresentazioni obsolete nell'aiuto.

NormalizeDouble è completamente screditato. Non solo l'implementazione lenta, ma anche senza senso su più simboli di scambio (ad esempio RTS, MIX, ecc.).

NormalizeDouble è stato originariamente creato da voi per le operazioni di Order*. Principalmente per i prezzi e i lotti. Ma sono apparsi TickSize e VolumeStep non standard. E la funzione è semplicemente obsoleta. Per questo motivo scrivono codice lento. Un esempio dalla libreria standard
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);
  }

Beh, non si può fare così maldestramente! Potrebbe essere molto più veloce, dimenticando 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);
}

E per lo stesso volume poi fare

volume = NormalizePrice(volume, stepvol);

Per i prezzi fare

NormalizePrice(Price, TickSize)

Sembra corretto aggiungere qualcosa di simile per sovraccaricare lo standard NormalizeDouble. Dove il secondo parametro "digits" sarà un doppio invece di int.

 

Nel 2016, la maggior parte dei compilatori C++ sono arrivati agli stessi livelli di ottimizzazione.

MSVC fa dubitare dei miglioramenti ad ogni aggiornamento, e Intel C++ come compilatore si è fuso - mai veramente guarito dal suo "errore interno" su grandi progetti.

Un altro dei nostri miglioramenti nel compilatore nella build 1400 è che è più veloce nel compilare progetti complessi.

 

In tema. Dovete creare delle alternative alle funzioni standard, perché a volte vi danno l'output sbagliato. Ecco un esempio di 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);
}

Potete chiamare SymbolInfoTick su ogni evento NewTick nel tester e sommare il campo volume per conoscere il fatturato delle azioni. Ma no, non si può! Dobbiamo fare un MySymbolInfoDouble molto più logico.

 
fxsaber:

A proposito di NormalizeDouble c'è questa sciocchezza

NormalizeDouble è stato originariamente creato da voi per le operazioni di Order*. Principalmente per i prezzi e i lotti. Ma sono apparsi TickSize e VolumeStep non standard. E la funzione è semplicemente obsoleta. Per questo motivo scrivono codice lento. Ecco un esempio dalla libreria standard

Beh, non può essere così maldestro! Potete renderlo molte volte più veloce dimenticando NormalizeDouble.

E per lo stesso volume fare

Per i prezzi fare

Sembra corretto aggiungere qualcosa del genere come sovraccarico allo standard NormalizeDouble. Dove il secondo parametro "digits" sarà un doppio invece di int.

Si può ottimizzare tutto intorno ad esso.

Questo è un processo senza fine. Ma nel 99% dei casi non è economicamente redditizio.