Implementações alternativas de funções/abordagens padrão

 

NormalizeDuplo

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

O resultado é 1123275 e 1666643 em favor do MyNormalizeDouble (Optimize=1). Sem otimização, é quatro vezes mais rápido (em memória).


 

Se você substituir

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

para a variante switch, você pode ver a qualidade da implementação do switch em números.

 

Considere a versão limpa do roteiro com 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");
  }

Resultados:

Custom:   1110255 msc
Result:   1001.23456

Standard: 1684165 msc
Result:   1001.23456

Observações e explicações imediatas:

  1. estática é necessária aqui para que o compilador leve esta matriz para fora da função e não a construa sobre a pilha toda vez que a função é chamada. O compilador C++ faz o mesmo.
    static const double Points
  2. Para evitar que o compilador jogue o laço fora por ser inútil, devemos usar os resultados dos cálculos. Por exemplo, Imprimir a variável Preço.

  3. Há um erro em sua função - os limites dos dígitos não são verificados, o que pode facilmente levar a ultrapassagens de matriz.

    Por exemplo, chame-o como MyNormalizeDouble(Preço+ponto,10) e pegue o erro:
    array out of range in 'BenchNormalizeDouble.mq5' (19,45)
    
    O método de aceleração por não verificação é aceitável, mas não no nosso caso. Devemos lidar com qualquer entrada de dados errônea.

  4. Vamos adicionar uma condição simples para um índice maior que 8. Para simplificar o código, substitua o tipo de dígitos variáveis por uint, para fazer uma comparação para >8 ao invés de uma condição adicional <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. Vamos executar o código e... Estamos surpresos!
    Custom:   1099705 msc
    Result:   1001.23456
    
    Standard: 1695662 msc
    Result:   1001.23456
    
    Seu código ultrapassou ainda mais a função NormalizeDuplo padrão!

    Além disso, a adição da condição reduz até mesmo o tempo (na verdade, ele está dentro da margem de erro). Por que há tanta diferença na velocidade?

  6. Tudo isso tem a ver com um erro padrão dos testadores de desempenho.

    Ao escrever testes, você deve ter em mente a lista completa de otimizações que podem ser aplicadas pelo compilador. Você precisa ser claro sobre quais dados de entrada você está usando e como eles serão destruídos quando você escrever um teste de amostra simplificado.

    Vamos avaliar e aplicar todo o conjunto de otimizações que nosso compilador faz, passo a passo.

  7. Vamos começar com a propagação constante - este é um dos erros importantes que você cometeu neste teste.

    Você tem metade de seus dados de entrada como constantes. Vamos reescrever o exemplo levando em conta sua propagação.

    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);
      }
    
    Após o seu lançamento, nada mudou - deve ser assim.

  8. Continue - inline seu código (nosso NormalizeDouble não pode ser inlined)

    Esta é a aparência de sua função na realidade, depois de inevitável em linha. A economia nas chamadas, a economia nas buscas de matriz, as verificações são removidas devido à análise 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);
      }
    
    Não resumi constantes puras para economizar tempo. todas elas têm garantia de colapso em tempo de compilação.

    Execute o código e obtenha o mesmo tempo que na versão original:
    Custom:   1149536 msc
    Standard: 1767592 msc
    
    não se importe com a tagarelice dos números - ao nível de microssegundos, erro de temporizador e carga flutuante no computador, isto está dentro dos limites normais. a proporção é totalmente mantida.

  9. Veja o código que você realmente começou a testar por causa dos dados da fonte fixa.

    Como o compilador tem uma otimização muito poderosa, sua tarefa foi efetivamente simplificada.


  10. Então, como você deve testar o desempenho?

    Ao entender como o compilador funciona, você precisa impedir que ele aplique pré-optimizações e simplificações.

    Por exemplo, vamos tornar o parâmetro de dígitos variável:

    #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");
      }
    
    Execute-o e... obtemos o mesmo resultado de velocidade de antes.

    Seu código ganha cerca de 35%, como antes.

  11. Então por que é assim?

    Ainda não podemos nos salvar da otimização por causa da linha de costura. Economizar 100 000 000 de chamadas passando dados através da pilha em nossa função NormalizeDouble, que é semelhante na implementação, pode muito bem dar o mesmo aumento de velocidade.

    Há outra suspeita de que nosso NormalizeDouble não tenha sido implementado no mecanismo de chamada direta ao carregar a tabela de realocação de funções no programa MQL5.

    Vamos verificá-lo pela manhã e, se for o caso, vamos movê-lo para o direct_call e verificar novamente a velocidade.

Aqui está um estudo de NormalizeDouble.

Nosso compilador MQL5 venceu nossa função de sistema, o que mostra sua adequação quando comparado com a velocidade do código C++.

 
fxsaber:

Se você substituir

para a variante switch, você pode ver a qualidade da implementação do switch em números.

Você está confundindo o acesso direto indexado a uma matriz estática por um índice constante (que degenera em uma constante de um campo) e muda.

Switch não pode realmente competir com um caso desses. Switch tem várias otimizações do formulário utilizadas com freqüência:

  • "notoriamente ordenados e valores curtos são colocados em uma matriz estática e indexados" - os mais simples e rápidos, podem competir com a matriz estática, mas nem sempre
  • "várias matrizes por ordem e fecham pedaços de valores com verificações de limites de zona" - isto já tem um freio
  • "verificamos muito poucos valores através de se" - sem velocidade, mas é culpa do próprio programador, ele usa o interruptor inadequadamente
  • "mesa muito esparsa com busca binária" - muito lenta para os piores casos

Na verdade, a melhor estratégia para a troca é quando o desenvolvedor tentou deliberadamente fazer um conjunto compacto de valores no conjunto inferior de números.

 
Renat Fatkhullin:

Considere a versão limpa do roteiro com NormalizeDouble:

Resultados:


Observações e explicações imediatas:

  1. é necessário aqui para que o compilador coloque esta matriz fora da função e não a construa sobre a pilha a cada chamada de função. O compilador C++ faz a mesma coisa.
Com "Optimize=0", este é o caso. Com "Optimize=1", você pode até mesmo jogá-lo fora - o compilador otimizador é inteligente, como se vê.
  1. Para evitar que o compilador jogue fora o loop devido a sua inutilidade, devemos utilizar os resultados dos cálculos. Por exemplo, Imprimir a variável Preço.
Que truque legal!
  1. Há um erro em sua função que não verifica os limites dos dígitos, o que pode facilmente levar a ultrapassagens de matriz.

    Por exemplo, chame-o como MyNormalizeDouble(Preço+ponto,10) e pegue o erro:
    O método de aceleração por não verificação é aceitável, mas não no nosso caso. Devemos lidar com qualquer entrada de dados errônea.

  2. Vamos adicionar uma condição simples sobre o índice maior que 8. Para simplificar o código, vamos substituir o tipo de dígitos variáveis por uint, para fazer uma comparação para >8 ao invés de uma condição adicional <0
Parece ser mais ideal!
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. Este é um erro padrão dos testadores de desempenho.

    Ao escrever testes, devemos ter em mente a lista completa de otimizações que podem ser aplicadas pelo compilador. Você precisa ser claro sobre quais dados de entrada você está usando e como eles serão destruídos quando você escrever um teste de amostra simplificado.
  2. Então, como você deve testar o desempenho?

    Ao entender como o compilador funciona, você precisa impedir que ele aplique pré-optimizações e simplificações.

    Por exemplo, vamos tornar o parâmetro de dígitos variável:
Muito obrigado por explicações detalhadas sobre como preparar corretamente as medidas de desempenho do compilador! Realmente não levava em conta a possibilidade de otimizar a constante.

Este é o estudo NormalizeDouble.

Nosso compilador MQL5 superou nossa função de sistema, o que mostra sua adequação quando comparado com a velocidade do código C++.

Sim, este resultado é uma questão de orgulho.
 
Renat Fatkhullin:

Você está confundindo o acesso direto indexado a uma matriz estática por um índice constante (que degenera em uma constante de um campo) e muda.

Switch não pode realmente competir com um caso desses. O Switch tem algumas otimizações do tipo comumente utilizadas:

  • Os "valores deliberadamente ordenados e curtos são colocados em uma matriz estática e indexados por interruptor" é o mais simples e rápido, e pode competir com uma matriz estática, mas nem sempre.

Este é exatamente um caso de encomenda.

Na verdade, a melhor estratégia para a troca é quando o desenvolvedor tentou deliberadamente fazer um conjunto compacto de valores no conjunto inferior de números.

Experimentei-o em um sistema de 32 bits. Ali, a substituição do interruptor no exemplo acima causou sérios atrasos. Ainda não o testei em uma máquina nova.
 
fxsaber:

Este é apenas um caso de ordenação.

Temos que verificá-lo separadamente, mas mais tarde.


Experimentei-o em um sistema de 32 bits. Ali, a substituição do interruptor no exemplo acima causou uma frenagem séria. Ainda não chequei na nova máquina.

Na verdade, há dois programas compilados em cada MQL5: um simplificado para 32 bits e um maximamente otimizado para 64 bits. Em 32 bit MT5 o novo otimizador não se aplica e o código para operações de 32 bit é tão simples quanto o MQL4 em MT4.

Toda a eficiência do compilador que pode gerar código dez vezes mais rápido apenas quando executado na versão de 64 bits do MT5: https://www.mql5.com/ru/forum/58241

Estamos totalmente concentrados nas versões de 64 bits da plataforma.

 

Sobre o assunto de NormalizeDouble há este absurdo

Fórum sobre comércio, sistemas automatizados de comércio e testes estratégicos

Como faço para passar por uma enumeração de forma consistente?

fxsaber, 2016.08.26 16:08

Há esta nota na descrição da função

Isto só é verdade para símbolos que têm uma etapa de preço mínimo de 10^N, onde N é um número inteiro e não positivo. Se a etapa de preço mínimo tem um valor diferente, então a normalização dos níveis de preço antes da OrderSend é uma operação sem sentido que devolverá a OrderSend falsa na maioria dos casos.


É uma boa idéia corrigir as representações desatualizadas na ajuda.

NormalizeDouble está completamente desacreditado. Não apenas a implementação lenta, mas também sem sentido em múltiplos símbolos de troca (por exemplo, RTS, MIX, etc.).

NormalizeDouble foi originalmente criado por você para operações de Ordem*. Principalmente para preços e lotes. Mas apareceram o TickSize e o VolumeStep não padronizados. E a função é simplesmente obsoleta. Por causa disso, eles escrevem em código lento. Um exemplo da biblioteca padrão
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);
  }

Bem, você não pode fazer isso desajeitadamente! Poderia ser muitas vezes mais rápido, esquecendo a 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 para o mesmo volume, então faça

volume = NormalizePrice(volume, stepvol);

Para preços fazem

NormalizePrice(Price, TickSize)

Parece correto acrescentar algo semelhante à sobrecarga do padrão NormalizeDouble. Onde o segundo parâmetro "dígitos" será um duplo em vez de int.

 

Até 2016, a maioria dos compiladores C++ chegou aos mesmos níveis de otimização.

A MSVC faz uma maravilha sobre as melhorias a cada atualização, e a Intel C++ como compilador se fundiu - nunca realmente curado de seu "erro interno" em grandes projetos.

Outra de nossas melhorias no compilador na construção do 1400 é que ele é mais rápido na compilação de projetos complexos.

 

Sobre o assunto. Você tem que criar alternativas para as funções padrão, porque elas às vezes lhe dão o resultado errado. Aqui está um exemplo de 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);
}

Você pode chamar a SymbolInfoTick em cada evento NewTick no campo de teste e resumir o volume do campo de teste para saber a rotatividade do estoque. Mas não, você não pode! Temos que fazer um MySymbolInfoDouble muito mais lógico.

 
fxsaber:

Sobre o assunto de NormalizeDouble há este absurdo

NormalizeDouble foi originalmente criado por você para operações de Ordem*. Principalmente para preços e lotes. Mas apareceram o TickSize e o VolumeStep não padronizados. E a função é simplesmente obsoleta. Por causa disso, eles escrevem em código lento. Aqui está um exemplo da biblioteca padrão

Bem, não pode ser tão desajeitado! Você pode fazer isso muitas vezes mais rápido esquecendo a NormalizeDouble.

E para o mesmo volume fazem

Para preços fazem

Parece correto acrescentar algo como isto como uma sobrecarga ao padrão NormalizeDouble. Onde o segundo parâmetro "dígitos" será um duplo em vez de int.

Você pode otimizar tudo ao seu redor.

Este é um processo interminável. Mas em 99% dos casos é economicamente não rentável.