標準的な機能/アプローチの代替実装

 

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

結果は、MyNormalizeDouble(Optimize=1)が1123275、1666643と有利になります。最適化しない場合は、4倍(メモリ内)高速化されます。


 

交換する場合

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

をスイッチバリアントにすると、スイッチの実装品質を数字で見ることができます。

 

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

結果

Custom:   1110255 msc
Result:   1001.23456

Standard: 1684165 msc
Result:   1001.23456

即時の発言と説明。

  1. static は、コンパイラがこの配列を関数の外部に取り出し、関数が呼ばれるたびにスタック上に構築 しないようにするために必要です。C++コンパイラも同じです。
    static const double Points
  2. コンパイラがループを無駄だからと投げ出さないようにするためには、計算結果を利用することです。例えば、変数Priceを表示する。

  3. この関数にはエラーがあります。桁の境界がチェックされないため、簡単に配列のオーバーランが発生する可能性があります。

    例えば、MyNormalizeDouble(Price+point,10)として呼び出し、エラーをキャッチします。
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    確認しないことでスピードアップを図るという方法もありますが、私たちの場合はそうではありません。誤って入力されたデータにも対応しなければならない。

  4. コードを簡単にするために、変数digitの型をuintに置き換えて、<0という条件を追加する代わりに、>8という条件を1回で比較するようにします。
    //+------------------------------------------------------------------+
    //|                                                                  |
    //+------------------------------------------------------------------+
    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. コードを実行すると...私たちは驚いています!
    Custom:   1099705 msc
    Result:   1001.23456
    
    Standard: 1695662 msc
    Result:   1001.23456
    
    あなたのコードは、標準のNormalizeDouble関数を さらに追い越したのです

    しかも、条件を加えることでさらに時間が短縮される(実際は誤差の範囲内)。なぜ、これほどまでにスピードに差があるのでしょうか?

  6. これらはすべて、パフォーマンステスターの標準的なエラーと関係がある。

    テストを書く際には、コンパイラが適用できる最適化の全リストを覚えておく必要があります。簡易的なサンプルテストを書く際には、どのような入力データを使用し、どのように破棄するのかを明確にする必要があります。

    コンパイラが行う最適化一式を、順を追って評価・適用してみましょう。

  7. まずは定数伝播についてですが、これは今回のテストであなたが犯した重要なミスの一つです。

    入力データの半分を定数として持っていますね。それらの伝搬を考慮して、例を書き換えてみましょう。

    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);
      }
    
    起動しても何も変わらない--そうなのでしょう。

  8. 続けて - コードをインライン化します (私たちの NormalizeDouble はインライン化できません)。

    必然的にインライン化された後の関数は、現実にはこのような形になります。呼び出し時のセーブ、配列フェッチ時のセーブ、常時解析によるチェックが削除されています。
    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);
      }
    
    時間を節約するために純粋な定数をまとめなかった。それらはすべて、コンパイル時に崩壊することが保証されている。

    コードを実行すると、オリジナル版と同じタイムが表示されます。
    Custom:   1149536 msc
    Standard: 1767592 msc
    
    マイクロ秒、タイマーエラー、コンピュータの浮動負荷のレベルでは、これは正常な範囲内である - 数字のジッタに注意を払わない。

  9. ソースデータが固定されているため、実際にテストを開始したコードを見てください。

    コンパイラは非常に強力な最適化機能を持っているので、あなたの仕事は効果的に簡素化されました。


  10. では、どのように性能をテストすればよいのでしょうか。

    コンパイラの動作を理解することで、コンパイラが事前に最適化や簡略化を適用するのを防ぐ必要があります。

    例えば、digitというパラメータを変数にしてみましょう。

    #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");
      }
    
    実行すると...という結果が得られます。

    あなたのコードの利益は、以前と同じように約35%です。

  11. では、なぜそうなのか。

    インライン化による最適化からは、まだ救われない。同じような実装のNormalizeDouble関数にスタックを介してデータを渡すことで、10万回の呼び出しを節約することができるかもしれません。

    また、MQL5で関数再配置テーブルを読み込む際に、弊社のNormalizeDoubleがdirect_call機構で実装されていない疑いもあります。

    朝に確認して、もしそうならdirect_callに移動して、再度速度を確認します。

ここでは、NormalizeDoubleの研究をしています。

当社のMQL5コンパイラは、当社のシステム関数に勝っており、C++コードのスピードと比較すると、その妥当性がわかります。

 
fxsaber:

交換する場合

をスイッチバリアントにすると、スイッチの実装品質を数字で見ることができます。

定数インデックスによる静的配列への 直接アクセス(フィールドから定数に縮退する)とswitchを混同していますね。

そんな筐体ではSwitchはとても太刀打ちできない。Switchは、よく使われる形式の最適化をいくつか持っています。

  • "notoriously ordered and short values are put into static array and indexed" - 最もシンプルで高速、静的配列と競合できるが、常に競合できるわけではない
  • 「ゾーンバウンダリーチェックによる、順序付きで近い値のチャンクによる複数の配列」 - これはすでにブレーキがかかっています。
  • 「ifでチェックする値が少なすぎる」 - 速度は出ないが、プログラマがswitchを不適切に使っているのが原因。
  • 「バイナリサーチによる非常に疎な順序付きテーブル」 - 最悪のケースで非常に遅い

実は、スイッチに最適な戦略は、開発者が意図的に数値の下位集合をコンパクトにしようとした場合なのです。

 
Renat Fatkhullin:

NormalizeDoubleを使ったクリーンアップ版のスクリプトを考えてみましょう。

結果


即時の発言と説明。

  1. static は、コンパイラがこの配列を関数の外に置き、関数呼び出しのたびにスタックに構築 しないようにするために必要です。C++コンパイラも同じことをする。
Optimize=0」の場合は、このようになります。Optimize=1」であれば、それを捨てることもできる。オプティマイザコンパイラは賢い。
  1. コンパイラがループの無駄を理由に投げ出さないようにするためには、計算結果を利用する必要がある。例えば、変数Priceを表示する。
なんてかっこいい仕掛けなんだ
  1. 桁の境界をチェックしない関数にエラーがあり、配列のオーバーランが発生しやすくなっています。

    例えば、MyNormalizeDouble(Price+point,10)として呼び出し、エラーをキャッチします。
    確認しないことでスピードアップを図るという方法もありますが、私たちの場合はそうではありません。誤って入力されたデータにも対応しなければならない。

  2. コードを単純化するために、変数digitの型をuintに置き換えて、<0という条件を追加する代わりに>8という条件を1回だけ比較するようにします。
より最適になったようです!
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. これは、パフォーマンステスターの標準的なミスである。

    テストを書く際には、コンパイラが適用できる最適化のリストをすべて頭に入れておく必要があります。簡易的なサンプルテストを書く際には、どのような入力データを使用し、どのように破棄するのかを明確にする必要があります。
  2. では、どのように性能をテストすればよいのでしょうか。

    コンパイラの動作を理解することで、コンパイラが事前に最適化や簡略化を適用するのを防ぐ必要があります。

    例えば、digitというパラメータを変数にしてみましょう。
コンパイラの性能測定の正しい準備の仕方について、丁寧に説明していただき、ありがとうございました本当に定数を最適化する可能性を考慮していなかった。

これは、NormalizeDoubleの研究です。

当社のMQL5コンパイラは当社のシステム関数に勝っており、C++コードのスピードと比較すると、その妥当性がわかります。

そう、この結果はプライドに関わることなのです。
 
Renat Fatkhullin:

定数インデックスによる静的配列への 直接アクセス(フィールドから定数に縮退する)とswitchを混同していますね。

そんな筐体ではSwitchはとても太刀打ちできない。Switchは、よく使われる種類の最適化をいくつか持っています。

  • わざと順序を変えて短い値を静的配列に入れ、switchでインデックスを付ける」のが最もシンプルで速く、静的配列と競合することもありますが、常にそうとは限りません。

これはまさに、そんな注文の多いケースです。

実は、スイッチに最適な戦略は、開発者が意図的に底辺の数値の集合をコンパクトにしようとした場合なのです。

32ビットシステムで試しました。そこで、上記の例のスイッチを交換したところ、深刻なラグが発生しました。新しいマシンでのテストはしていません。
 
fxsaber:

ここでは、まさにそんな整頓の事例を紹介します。

別途、確認が必要ですが、後ほど。


32ビットシステムで試しました。そこでは、上記の例のスイッチ交換により、重大なブレーキが発生しました。新機種では確認していません。

MQL5には、32ビット用の簡易版と64ビット用に最適化された版の2つのコンパイルプログラムが存在します。32ビット版MT5では、新しいオプティマイザーは全く適用されず、32ビット演算のコードは、MT4のMQL4と同様にシンプルなものとなっています。

64ビット版のMT5で実行した場合のみ、10倍高速にコードを生成できるコンパイラのすべての効率化https://www.mql5.com/ru/forum/58241

私たちは、64ビット版のプラットフォームに全面的に注力しています。

 

NormalizeDoubleについてですが、こんなくだりがあります。

トレーディング、自動売買システム、ストラテジーテストに関するフォーラム

エニュメレーションを一貫して行うにはどうしたらいいですか?

fxsaber さん 2016.08.26 16:08

機能の説明の 中に、こんな記述があります。

これは、最小価格ステップが10^Nであるシンボルにのみ当てはまります(Nは整数で正ではありません)。最小価格ステップが異なる値を持っている場合、OrderSendの前に価格レベルを正規化することは無意味な操作であり、ほとんどの場合、偽のOrderSendを返します。


ヘルプで古くなった表現を修正するのは良いアイデアです。

NormalizeDoubleは完全に信用を失いました。実装が遅いだけでなく、複数の交換シンボル(RTS、MIXなど)では意味がない

NormalizeDoubleは、もともとOrder*操作のためにあなたが作成したものです。主に価格とロットについて。しかし、非標準のTickSizeとVolumeStepが表示されました。そして、その機能は単に時代遅れなのです。そのため、彼らは遅いコードを書く。標準ライブラリの例
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);
  }

まあ、不器用な人は無理でしょうけど!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);
}

そして、同じボリュームに対して、次のようにします。

volume = NormalizePrice(volume, stepvol);

価格について

NormalizePrice(Price, TickSize)

NormalizeDouble規格をオーバーロードするようなものを追加するのが正しいようです。第2引数 "digits "はintではなくdoubleになります。

 

2016年までに、ほとんどのC++コンパイラが同じレベルの最適化に到達しています。

MSVCはアップデートを重ねるごとに改善され、Intel C++はコンパイラとして統合され、大規模プロジェクトでの「内部エラー」は決して治りませんでした。

1400 ビルドでのコンパイラのもう一つの改良点は、複雑なプロジェクトのコンパイルをより高速に行えるようにしたことです。

 

トピックについて標準の関数では間違った出力が出ることがあるので、代替の関数を作る必要があります。以下は、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);
}

テスターの各イベントNewTickで SymbolInfoTickを呼び出し、Volumeフィールドを合計することで、株価の回転数を知ることができます。でも、ダメなんです!もっと論理的なMySymbolInfoDoubleを作らなければならない。

 
fxsaber:

NormalizeDoubleについてですが、こんなくだりがあります。

NormalizeDoubleは、もともとOrder*操作のためにあなたが作成したものです。主に価格とロットについて。しかし、非標準のTickSizeとVolumeStepが表示されました。そして、その機能は単に時代遅れなのです。そのため、彼らは遅いコードを書く。以下は標準ライブラリの例です。

まあ、そんな不器用なわけがないんですけどね。NormalizeDoubleのことを忘れると何倍も速くなることがあります。

そして、同じボリュームで行う

価格について

NormalizeDouble規格のオーバーロードとして、このようなものを追加するのが正しいようです。第2引数 "digits "はintではなくdoubleになります。

その周辺をすべて最適化することができるのです。

これは終わりのない作業です。しかし、99%のケースで経済的に採算がとれないのです。

理由: