Implémentations alternatives de fonctions/approches 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;
};

Le résultat est de 1123275 et 1666643 en faveur de MyNormalizeDouble (Optimize=1). Sans optimisation, il est quatre fois plus rapide (en mémoire).


 

Si vous remplacez

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

à la variante du commutateur, vous pouvez voir la qualité de l'implémentation du commutateur en chiffres.

 

Considérez la version nettoyée du script avec 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");
  }

Résultats :

Custom:   1110255 msc
Result:   1001.23456

Standard: 1684165 msc
Result:   1001.23456

Remarques et explications immédiates :

  1. static est nécessaire ici pour que le compilateur prenne ce tableau en dehors de la fonction et ne le construise pas sur la pile à chaque fois que la fonction est appelée. Le compilateur C++ fait de même.
    static const double Points
  2. Pour éviter que le compilateur ne rejette la boucle parce qu'elle est inutile, nous devons utiliser les résultats des calculs. Par exemple, imprimer la variable Prix.

  3. Il y a une erreur dans votre fonction - les limites des chiffres ne sont pas vérifiées, ce qui peut facilement conduire à des dépassements de tableau.

    Par exemple, appelez-le comme MyNormalizeDouble(Price+point,10) et attrapez l'erreur :
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    La méthode consistant à accélérer en ne vérifiant pas est acceptable, mais pas dans notre cas. Nous devons gérer toute entrée de données erronées.

  4. Pour simplifier le code, remplacez le type de la variable digits par uint, afin d'effectuer une seule comparaison pour >8 au lieu d'une condition supplémentaire <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. Exécutons le code et... Nous sommes surpris !
    Custom:   1099705 msc
    Result:   1001.23456
    
    Standard: 1695662 msc
    Result:   1001.23456
    
    Votre code a dépassé encore plus la fonction standard NormalizeDouble!

    De plus, l'ajout de la condition réduit même le temps (en fait, il se situe dans la marge d'erreur). Pourquoi y a-t-il une telle différence de vitesse ?

  6. Tout ceci est lié à une erreur standard des testeurs de performance.

    Lorsque vous écrivez des tests, vous devez garder à l'esprit la liste complète des optimisations qui peuvent être appliquées par le compilateur. Vous devez être clair sur les données d'entrée que vous utilisez et sur la manière dont elles seront détruites lorsque vous écrivez un test type simplifié.

    Évaluons et appliquons l'ensemble des optimisations effectuées par notre compilateur, étape par étape.

  7. Commençons par la propagation constante - c'est l'une des erreurs importantes que vous avez commises dans ce test.

    La moitié de vos données d'entrée sont des constantes. Réécrivons l'exemple en tenant compte de leur propagation.

    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);
      }
    
    Après l'avoir lancé, rien n'a changé - il doit en être ainsi.

  8. Continuez - inlinez votre code (notre NormalizeDouble ne peut pas être inline).

    C'est ce que votre fonction deviendra en réalité après l'inelining. Économie sur les appels, économie sur les recherches de tableaux, les contrôles sont supprimés grâce à une analyse 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);
      }
    
    Je n'ai pas résumé les constantes pures pour gagner du temps. Elles sont toutes garanties de s'effondrer au moment de la compilation.

    Exécutez le code et obtenez le même temps que dans la version originale :
    Custom:   1149536 msc
    Standard: 1767592 msc
    
    ne faites pas attention à la gigue dans les chiffres - au niveau des microsecondes, l'erreur de minuterie et la charge flottante sur l'ordinateur, cela reste dans les limites normales. la proportion est entièrement maintenue.

  9. Regardez le code que vous avez commencé à tester en raison des données sources fixes.

    Comme le compilateur dispose d'une optimisation très puissante, votre tâche a été effectivement simplifiée.


  10. Alors comment tester les performances ?

    En comprenant comment le compilateur fonctionne, vous devez l'empêcher d'appliquer des pré-optimisations et des simplifications.

    Par exemple, faisons du paramètre "chiffres" une variable :

    #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");
      }
    
    Exécutez-le et... nous obtenons le même résultat de vitesse que précédemment.

    Votre code gagne environ 35% comme avant.

  11. Pourquoi en est-il ainsi ?

    Nous ne pouvons toujours pas nous épargner l'optimisation due à l'inlining. Économiser 100 000 000 d'appels en faisant passer les données par la pile dans notre fonction NormalizeDouble, dont l'implémentation est similaire, pourrait bien donner le même gain de vitesse.

    Il y a un autre soupçon que notre NormalizeDouble n'a pas été implémenté dans le mécanisme d'appel direct lors du chargement de la table de relocalisation des fonctions dans le programme MQL5.

    Nous le vérifierons demain matin et si c'est le cas, nous le déplacerons vers direct_call et vérifierons à nouveau la vitesse.

Voici une étude de NormalizeDouble.

Notre compilateur MQL5 a battu notre fonction système, ce qui montre son adéquation par rapport à la vitesse du code C++.

 
fxsaber:

Si vous remplacez

à la variante du commutateur, vous pouvez voir la qualité de l'implémentation du commutateur en chiffres.

Vous confondez accès direct indexé à un tableau statique par un index constant (qui dégénère en constante à partir d'un champ) et switch.

La Switch ne peut pas vraiment rivaliser avec un tel boîtier. Switch possède plusieurs optimisations de la forme fréquemment utilisées :

  • "les valeurs notoirement ordonnées et courtes sont placées dans un tableau statique et indexées" - le plus simple et le plus rapide, peut concurrencer le tableau statique, mais pas toujours
  • "plusieurs tableaux par morceaux ordonnés et rapprochés de valeurs avec contrôle des limites de zones" - ceci a déjà un frein
  • "nous vérifions trop peu de valeurs par le biais de if" - pas de vitesse, mais c'est la faute du programmeur, il utilise le switch de manière inappropriée
  • "tableau ordonné très clairsemé avec recherche binaire" - très lent dans le pire des cas

En fait, la meilleure stratégie pour le switch est celle où le développeur a délibérément essayé de faire un ensemble compact de valeurs dans l'ensemble inférieur de nombres.

 
Renat Fatkhullin:

Considérez la version nettoyée du script avec NormalizeDouble :

Résultats :


Remarques et explications immédiates :

  1. static est nécessaire ici pour que le compilateur place ce tableau en dehors de la fonction et ne le construise pas sur la pile à chaque appel de fonction. Le compilateur C++ fait la même chose.
C'est le cas avec "Optimize=0". Avec "Optimize=1", vous pouvez même le rejeter - le compilateur optimiseur est intelligent, comme il s'avère.
  1. Pour éviter que le compilateur ne rejette la boucle en raison de son inutilité, nous devons utiliser les résultats des calculs. Par exemple, imprimer la variable Prix.
Quel truc génial !
  1. Il y a une erreur dans votre fonction qui ne vérifie pas les limites des chiffres, ce qui peut facilement conduire à des dépassements de tableau.

    Par exemple, appelez-le comme MyNormalizeDouble(Price+point,10) et attrapez l'erreur :
    La méthode consistant à accélérer en ne vérifiant pas est acceptable, mais pas dans notre cas. Nous devons gérer toute entrée de données erronées.

  2. Pour simplifier le code, remplaçons le type de la variable digits par uint, afin d'effectuer une comparaison pour >8 au lieu d'une condition supplémentaire <0
Il semble que ce soit plus optimal !
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. Il s'agit d'une erreur standard des testeurs de performance.

    Lorsque nous écrivons des tests, nous devons garder à l'esprit la liste complète des optimisations qui peuvent être appliquées par le compilateur. Vous devez être clair sur les données d'entrée que vous utilisez et sur la manière dont elles seront détruites lorsque vous écrivez un test type simplifié.
  2. Alors comment tester les performances ?

    En comprenant comment le compilateur fonctionne, vous devez l'empêcher d'appliquer des pré-optimisations et des simplifications.

    Par exemple, faisons du paramètre "chiffres" une variable :
Merci beaucoup pour les explications détaillées sur la façon de préparer correctement les mesures de performance du compilateur ! Je n'ai vraiment pas pris en compte la possibilité d'optimiser la constante.

C'est l'étude NormalizeDouble.

Notre compilateur MQL5 a battu notre fonction système, ce qui montre son adéquation par rapport à la vitesse du code C++.

Oui, ce résultat est une question de fierté.
 
Renat Fatkhullin:

Vous confondez accès direct indexé à un tableau statique par un index constant (qui dégénère en constante à partir d'un champ) et switch.

La Switch ne peut pas vraiment rivaliser avec un tel boîtier. Switch possède quelques optimisations de ce type couramment utilisées :

  • La solution "les valeurs délibérément ordonnées et courtes sont placées dans un tableau statique et indexées par switch" est la plus simple et la plus rapide, et peut rivaliser avec un tableau statique, mais pas toujours.

C'est justement un tel cas de commande.

En fait, la meilleure stratégie pour le switch est celle où le développeur a délibérément essayé de faire un ensemble compact de valeurs dans l'ensemble inférieur de chiffres.

Je l'ai essayé sur un système 32 bits. Là, le remplacement de l'interrupteur dans l'exemple ci-dessus a provoqué de sérieux décalages. Je ne l'ai pas testé sur une nouvelle machine.
 
fxsaber:

Voici un tel cas d'ordre.

Nous devons le vérifier séparément, mais plus tard.


Je l'ai essayé sur un système 32 bits. Là, le remplacement de l'interrupteur dans l'exemple ci-dessus a provoqué un freinage grave. Je ne l'ai pas vérifié sur la nouvelle machine.

Il y a en fait deux programmes compilés dans chaque MQL5 : un simplifié pour 32 bits et un optimisé au maximum pour 64 bits. Dans MT5 32 bits, le nouvel optimiseur ne s'applique pas du tout et le code pour les opérations 32 bits est aussi simple que MQL4 dans MT4.

Toute l'efficacité du compilateur qui peut générer un code dix fois plus rapide uniquement lorsqu'il est exécuté dans la version 64 bits de MT5 : https://www.mql5.com/ru/forum/58241.

Nous sommes entièrement concentrés sur les versions 64 bits de la plate-forme.

 

Au sujet de NormalizeDouble, il y a cette absurdité

Forum sur le trading, les systèmes de trading automatisés et les tests de stratégie

Comment faire pour passer une énumération de manière cohérente ?

fxsaber, 2016.08.26 16:08

Il y a cette note dans la description de la fonction

Ceci n'est vrai que pour les symboles qui ont un échelon de prix minimum 10^N, où N est un nombre entier et non positif. Si le pas de prix minimum a une valeur différente, alors la normalisation des niveaux de prix avant l'envoi de l'ordre est une opération sans signification qui renverra un faux ordre dans la plupart des cas.


Il est bon de corriger les représentations périmées dans l'aide.

NormalizeDouble est complètement discrédité. Non seulement la mise en œuvre est lente, mais elle n'a aucun sens sur les symboles d'échange multiples (par exemple RTS, MIX, etc.).

NormalizeDouble a été créé à l'origine par vous pour les opérations Order*. Principalement pour les prix et les lots. Mais des TickSize et VolumeStep non standard sont apparus. Et la fonction est tout simplement obsolète. C'est pourquoi ils écrivent du code lent. Un exemple tiré de la bibliothèque 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);
  }

Eh bien, vous ne pouvez pas le faire maladroitement ! Il pourrait être plusieurs fois plus rapide, en oubliant 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);
}

Et pour le même volume alors faire

volume = NormalizePrice(volume, stepvol);

Pour les prix, faire

NormalizePrice(Price, TickSize)

Il semble correct d'ajouter quelque chose de similaire pour surcharger la norme NormalizeDouble. Où le second paramètre "digits" sera un double au lieu d'un int.

 

En 2016, la plupart des compilateurs C++ sont arrivés aux mêmes niveaux d'optimisation.

MSVC suscite des interrogations quant aux améliorations apportées à chaque mise à jour, et Intel C++ en tant que compilateur a fusionné - il n'a jamais vraiment été guéri de son "erreur interne" sur les grands projets.

Une autre de nos améliorations du compilateur dans la version 1400 est qu'il est plus rapide pour compiler des projets complexes.

 

Sur le sujet. Vous devez créer des alternatives aux fonctions standard, car elles donnent parfois des résultats erronés. Voici un exemple d'alternative à 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);
}

Vous pouvez appeler SymbolInfoTick sur chaque événement NewTick dans le testeur et additionner le champ volume pour connaître la rotation des actions. Mais non, vous ne pouvez pas ! Nous devons créer un MySymbolInfoDouble beaucoup plus logique.

 
fxsaber:

Au sujet de NormalizeDouble, il y a cette absurdité

NormalizeDouble a été créé à l'origine par vous pour les opérations Order*. Principalement pour les prix et les lots. Mais des TickSize et VolumeStep non standard sont apparus. Et la fonction est tout simplement obsolète. C'est pourquoi ils écrivent du code lent. Voici un exemple tiré de la bibliothèque standard

Eh bien, il ne peut pas être si maladroit ! Il peut être plusieurs fois plus rapide en oubliant NormalizeDouble.

Et pour le même volume, faites

Pour les prix, faire

Il semble correct d'ajouter quelque chose comme ceci en tant que surcharge à la norme NormalizeDouble. Où le second paramètre "digits" sera un double au lieu d'un int.

Vous pouvez optimiser tout ce qui l'entoure.

C'est un processus sans fin. Mais dans 99% des cas, c'est économiquement non rentable.