交換する場合
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
即時の発言と説明。
- static は、コンパイラがこの配列を関数の外部に取り出し、関数が呼ばれるたびにスタック上に構築 しないようにするために必要です。C++コンパイラも同じです。
static const double Points
- コンパイラがループを無駄だからと投げ出さないようにするためには、計算結果を利用することです。例えば、変数Priceを表示する。
- この関数にはエラーがあります。桁の境界がチェックされないため、簡単に配列のオーバーランが発生する可能性があります。
例えば、MyNormalizeDouble(Price+point,10)として呼び出し、エラーをキャッチします。array out of range in 'BenchNormalizeDouble.mq5' (19,45)
確認しないことでスピードアップを図るという方法もありますが、私たちの場合はそうではありません。誤って入力されたデータにも対応しなければならない。 - コードを簡単にするために、変数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]); }
- コードを実行すると...私たちは驚いています!
Custom: 1099705 msc Result: 1001.23456 Standard: 1695662 msc Result: 1001.23456
あなたのコードは、標準のNormalizeDouble関数を さらに追い越したのです
しかも、条件を加えることでさらに時間が短縮される(実際は誤差の範囲内)。なぜ、これほどまでにスピードに差があるのでしょうか? - これらはすべて、パフォーマンステスターの標準的なエラーと関係がある。
テストを書く際には、コンパイラが適用できる最適化の全リストを覚えておく必要があります。簡易的なサンプルテストを書く際には、どのような入力データを使用し、どのように破棄するのかを明確にする必要があります。
コンパイラが行う最適化一式を、順を追って評価・適用してみましょう。 - まずは定数伝播についてですが、これは今回のテストであなたが犯した重要なミスの一つです。
入力データの半分を定数として持っていますね。それらの伝搬を考慮して、例を書き換えてみましょう。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); }
起動しても何も変わらない--そうなのでしょう。 - 続けて - コードをインライン化します (私たちの 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
マイクロ秒、タイマーエラー、コンピュータの浮動負荷のレベルでは、これは正常な範囲内である - 数字のジッタに注意を払わない。 - ソースデータが固定されているため、実際にテストを開始したコードを見てください。
コンパイラは非常に強力な最適化機能を持っているので、あなたの仕事は効果的に簡素化されました。 - では、どのように性能をテストすればよいのでしょうか。
コンパイラの動作を理解することで、コンパイラが事前に最適化や簡略化を適用するのを防ぐ必要があります。
例えば、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%です。 - では、なぜそうなのか。
インライン化による最適化からは、まだ救われない。同じような実装のNormalizeDouble関数にスタックを介してデータを渡すことで、10万回の呼び出しを節約することができるかもしれません。
また、MQL5で関数再配置テーブルを読み込む際に、弊社のNormalizeDoubleがdirect_call機構で実装されていない疑いもあります。
朝に確認して、もしそうならdirect_callに移動して、再度速度を確認します。
ここでは、NormalizeDoubleの研究をしています。
当社のMQL5コンパイラは、当社のシステム関数に勝っており、C++コードのスピードと比較すると、その妥当性がわかります。
交換する場合
をスイッチバリアントにすると、スイッチの実装品質を数字で見ることができます。
定数インデックスによる静的配列への 直接アクセス(フィールドから定数に縮退する)とswitchを混同していますね。
そんな筐体ではSwitchはとても太刀打ちできない。Switchは、よく使われる形式の最適化をいくつか持っています。
- "notoriously ordered and short values are put into static array and indexed" - 最もシンプルで高速、静的配列と競合できるが、常に競合できるわけではない
- 「ゾーンバウンダリーチェックによる、順序付きで近い値のチャンクによる複数の配列」 - これはすでにブレーキがかかっています。
- 「ifでチェックする値が少なすぎる」 - 速度は出ないが、プログラマがswitchを不適切に使っているのが原因。
- 「バイナリサーチによる非常に疎な順序付きテーブル」 - 最悪のケースで非常に遅い
実は、スイッチに最適な戦略は、開発者が意図的に数値の下位集合をコンパクトにしようとした場合なのです。
NormalizeDoubleを使ったクリーンアップ版のスクリプトを考えてみましょう。
結果
即時の発言と説明。
- static は、コンパイラがこの配列を関数の外に置き、関数呼び出しのたびにスタックに構築 しないようにするために必要です。C++コンパイラも同じことをする。
- コンパイラがループの無駄を理由に投げ出さないようにするためには、計算結果を利用する必要がある。例えば、変数Priceを表示する。
- 桁の境界をチェックしない関数にエラーがあり、配列のオーバーランが発生しやすくなっています。
例えば、MyNormalizeDouble(Price+point,10)として呼び出し、エラーをキャッチします。
確認しないことでスピードアップを図るという方法もありますが、私たちの場合はそうではありません。誤って入力されたデータにも対応しなければならない。 - コードを単純化するために、変数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); }
- これは、パフォーマンステスターの標準的なミスである。
テストを書く際には、コンパイラが適用できる最適化のリストをすべて頭に入れておく必要があります。簡易的なサンプルテストを書く際には、どのような入力データを使用し、どのように破棄するのかを明確にする必要があります。 - では、どのように性能をテストすればよいのでしょうか。
コンパイラの動作を理解することで、コンパイラが事前に最適化や簡略化を適用するのを防ぐ必要があります。
例えば、digitというパラメータを変数にしてみましょう。
これは、NormalizeDoubleの研究です。
当社のMQL5コンパイラは当社のシステム関数に勝っており、C++コードのスピードと比較すると、その妥当性がわかります。
定数インデックスによる静的配列への 直接アクセス(フィールドから定数に縮退する)とswitchを混同していますね。
そんな筐体ではSwitchはとても太刀打ちできない。Switchは、よく使われる種類の最適化をいくつか持っています。
- わざと順序を変えて短い値を静的配列に入れ、switchでインデックスを付ける」のが最もシンプルで速く、静的配列と競合することもありますが、常にそうとは限りません。
これはまさに、そんな注文の多いケースです。
実は、スイッチに最適な戦略は、開発者が意図的に底辺の数値の集合をコンパクトにしようとした場合なのです。
ここでは、まさにそんな整頓の事例を紹介します。
32ビットシステムで試しました。そこでは、上記の例のスイッチ交換により、重大なブレーキが発生しました。新機種では確認していません。
MQL5には、32ビット用の簡易版と64ビット用に最適化された版の2つのコンパイルプログラムが存在します。32ビット版MT5では、新しいオプティマイザーは全く適用されず、32ビット演算のコードは、MT4のMQL4と同様にシンプルなものとなっています。
64ビット版のMT5で実行した場合のみ、10倍高速にコードを生成できるコンパイラのすべての効率化https://www.mql5.com/ru/forum/58241。
私たちは、64ビット版のプラットフォームに全面的に注力しています。
- レビュー: 8
- www.mql5.com
NormalizeDoubleについてですが、こんなくだりがあります。
トレーディング、自動売買システム、ストラテジーテストに関するフォーラム
fxsaber さん 2016.08.26 16:08
機能の説明の 中に、こんな記述があります。
これは、最小価格ステップが10^Nであるシンボルにのみ当てはまります(Nは整数で正ではありません)。最小価格ステップが異なる値を持っている場合、OrderSendの前に価格レベルを正規化することは無意味な操作であり、ほとんどの場合、偽のOrderSendを返します。
NormalizeDoubleは完全に信用を失いました。実装が遅いだけでなく、複数の交換シンボル(RTS、MIXなど)では意味がない。
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になります。
トピックについて標準の関数では間違った出力が出ることがあるので、代替の関数を作る必要があります。以下は、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を作らなければならない。
NormalizeDoubleについてですが、こんなくだりがあります。
まあ、そんな不器用なわけがないんですけどね。NormalizeDoubleのことを忘れると何倍も速くなることがあります。
そして、同じボリュームで行う
価格について
NormalizeDouble規格のオーバーロードとして、このようなものを追加するのが正しいようです。第2引数 "digits "はintではなくdoubleになります。
その周辺をすべて最適化することができるのです。
これは終わりのない作業です。しかし、99%のケースで経済的に採算がとれないのです。
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
NormalizeDouble
結果は、MyNormalizeDouble(Optimize=1)が1123275、1666643と有利になります。最適化しない場合は、4倍(メモリ内)高速化されます。