标准功能/方法的其他实现方式

 

归一化的双数

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

结果是1123275和166643支持MyNormalizeDouble(优化=1)。如果不进行优化,它的速度是四倍(在内存中)。


 

如果你更换

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. 静态在这里是必要的,这样编译器就会在函数之外获取这个数组,而不会在每次函数被调用时都在堆栈中构建它。C++编译器也是这样做的。
    static const double Points
  2. 为了防止编译器因为循环无用而将其扔掉,我们应该使用计算的结果。例如,打印变量Price。

  3. 你的函数中存在一个错误--没有检查数字的边界,这可能很容易导致数组超限。

    例如,以MyNormalizeDouble(Price+point,10)的形式调用,并捕获错误。
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    通过不检查来加速的方法是可以接受的,但在我们的情况下不是。我们必须处理任何错误的数据输入。

  4. 让我们为指数大于8的情况添加一个简单的条件。 为了简化代码,将变量digits的类型改为uint,以便对>8进行一次比较,而不是附加条件<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. 让我们运行代码并...我们很惊讶!
    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. 那么,你应该如何进行性能测试?

    通过了解编译器的工作方式,你需要防止它应用预优化和简化。

    例如,让我们把数字参数变成变量。

    #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来节省100 000次调用,这在实现上是相似的,很可能会有同样的速度提升。

    还有一个疑点是,在MQL5程序中加载函数重定位表时,我们的NormalizeDouble还没有在direct_call机制中实现。

    我们将在早上检查,如果是这样,我们将把它移到direct_call并再次检查速度。

下面是对NormalizeDouble的研究。

我们的MQL5编译器已经击败了我们的系统功能,这表明它与C++代码的速度相比是足够的。

 
fxsaber:

如果你更换

到交换机的变体,你可以从数字中看到交换机实现的质量。

你混淆了通过常数索引(从字段退化为常数)对静态数组 的直接索引访问和开关。

交换机真的无法与这样的箱子竞争。Switch有几个经常使用的优化形式。

  • "臭名昭著的有序和短小的值被放入静态数组并进行索引" - 最简单和最快的,可以与静态数组竞争,但并不总是如此
  • "通过有序的、带有区域边界检查的近似值块的几个数组" - 这已经有一个刹车了
  • "我们通过if检查的值太少了" - 没有速度,但这是程序员自己的错,他不适当地使用了switch。
  • "非常稀疏的有序表与二进制搜索" - 在最坏的情况下非常慢

事实上,切换的最佳策略是当开发者刻意尝试在较低的一组数字中做出一个紧凑的数值。

 
Renat Fatkhullin:

考虑一下带有NormalizeDouble的清理过的脚本版本。

结果。


立即评论和解释。

  1. 这里需要静态化,以便编译器将这个数组放在函数之外,而不是在每次函数调用时将其建立在堆栈上。C++编译器也做同样的事情。
在 "优化=0 "的情况下,就是这样。使用 "Optimize=1",你甚至可以把它扔掉--事实证明,优化器编译器很聪明。
  1. 为了防止编译器因循环无用而将其抛出,我们必须使用计算的结果。例如,打印变量Price。
多么酷的把戏啊!
  1. 你的函数中存在一个错误,没有检查数字的边界,这很容易导致数组超限。

    例如,以MyNormalizeDouble(Price+point,10)的形式调用,并捕获错误。
    通过不检查来加速的方法是可以接受的,但在我们的情况下不是。我们必须处理任何错误的数据输入。

  2. 为了简化代码,我们将变量digits的类型替换为uint,以便对>8进行一次比较,而不是附加条件<0。
这似乎是更理想的!
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. 那么,你应该如何进行性能测试?

    通过了解编译器的工作方式,你需要防止它应用预优化和简化。

    例如,让我们把数字参数变成变量。
非常感谢你对如何正确准备编译器的性能测量进行了详尽的解释!真的没有考虑到优化常数的可能性。

这就是NormalizeDouble研究。

我们的MQL5编译器击败了我们的系统函数,这表明与C++代码的速度相比,它是足够的。

是的,这个结果是一个值得骄傲的问题。
 
Renat Fatkhullin:

你混淆了通过常数索引(从字段退化为常数)对静态数组 的直接索引访问和开关。

交换机真的无法与这样的箱子竞争。Switch有一些常用的优化种类。

  • 故意将有序的短值放入静态数组中,并通过switch进行索引 "是最简单和最快的,可以与静态数组竞争,但不一定。

这正是这样一个订购案例。

事实上,切换的最佳策略是当开发者刻意尝试在底层的数字集合中做出一个紧凑的数值。

在32位系统上试了一下。在那里,更换上述例子中的开关造成了严重的滞后。我还没有在新机器上测试过它。
 
fxsaber:

这里就是这样一个有序性的案例。

我们必须单独检查,但要稍后。


在32位系统上试了一下。在那里,上述例子中的开关更换导致了严重的制动。我还没有在新机器上检查过它。

每个MQL5中实际上有两个编译程序:一个是用于32位的简化程序,一个是为64位最大限度优化的程序。在32位MT5中,新的优化器完全不适用,32位操作的代码和MT4中的MQL4一样简单。

编译器的所有效率,只有在MT5的64位版本中执行时才能生成快十倍的代码: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标准似乎是正确的。其中第二个参数 "digits "将是一个双数,而不是int。

 

到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-field相加以了解股票的成交量。但是,不,你不能这样做。我们必须做一个更合理的MySymbolInfoDouble。

 
fxsaber:

关于NormalizeDouble的问题,有这样一句废话

NormalizeDouble最初是由你为Order*操作创建的。主要是为了价格和地段。但出现了非标准的TickSize和VolumeStep。而这个功能根本就已经过时了。正因为如此,他们写的代码很慢。下面是一个来自标准库的例子

好吧,它不能这么笨拙!你可以让它快很多倍,忘记NormalizeDouble。

而对于同样的体积,做

对于价格来说,做

把这样的东西作为一个重载添加到NormalizeDouble标准中似乎是正确的。其中第二个参数 "digits "将是一个双数,而不是int。

你可以围绕它优化一切。

这是一个无止境的过程。但在99%的情况下,这在经济上是无利可图的。