Если заменить
static const double Points[] = {1.0e-0, 1.0e-1, 1.0e-2, 1.0e-3, 1.0e-4, 1.0e-5, 1.0e-6, 1.0e-7, 1.0e-8};
на switch-вариант, можно будет увидеть качество реализации switch в цифрах.
Рассмотрим почищенный вариант скрипта с NormalizeDouble:
#define EPSILON (1.0e-7 + 1.0e-13) #define HALF_PLUS (0.5 + EPSILON) //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double MyNormalizeDouble(const double Value,const int digits) { static const double Points[]={1.0e-0,1.0e-1,1.0e-2,1.0e-3,1.0e-4,1.0e-5,1.0e-6,1.0e-7,1.0e-8}; return((int)((Value > 0) ? Value / Points[digits] + HALF_PLUS : Value / Points[digits] - HALF_PLUS) * Points[digits]); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ ulong BenchStandard(const int Amount=1.0e8) { 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.0e8) { 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 тут нужен обязательно, чтобы компилятор вынес этот массив за пределы функции и не конструировал его на стеке при каждом вызове функции. Компилятор С++ делает тоже самое.
static const double Points
- Чтобы компилятор не выкинул цикл из-за его бесполезности, надо воспользоваться результатами вычислений. Например, сделать Print переменной Price.
- В вашей функции есть ошибка - не проверяются границы digits, что легко может дать выход за пределы массива.
Например, вызвать как MyNormalizeDouble(Price+point,10) и ловим ошибку:array out of range in 'BenchNormalizeDouble.mq5' (19,45)
Метод ускорения путем отказа от проверок - это приемлемо, но не в нашем случае. Мы ведь обязаны обрабатывать любой ошибочный ввод данных. - Добавим простое условие на индекс больше 8, причем для упрощения кода тип переменной digits заменим на uint, чтобы делать одно сравнение на >8 вместо дополнительного условия <0
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double MyNormalizeDouble(const double Value,uint digits) { static const double Points[]={1.0e-0,1.0e-1,1.0e-2,1.0e-3,1.0e-4,1.0e-5,1.0e-6,1.0e-7,1.0e-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.0e8;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.0e8;i++) { Price=MyNormalizeDouble(Price + 0.00001,5); } Print("Result: ",Price," ",1.0e8); //--- return(GetMicrosecondCount() - StartTime); }
После запуска ничего не изменилось - так и должно быть. - Идем дальше - инлайним ваш код (наш NormalizeDouble заинлайнить нельзя)
Вот во что превращается ваша функция в реальности после неминуемого инлайна. Экономия на call, экономия на извлечениях из массива, проверки удаляются за счет анализа констант:ulong BenchCustom(void) { double Price=1.23456; const ulong StartTime=GetMicrosecondCount(); //--- for(int i=0; i<1.0e8;i++) { //--- этот код полностью вырезается, так как у нас заведомо константа 5 //if(digits>8) // digits=8; //--- распространяем переменные и активно заменяем константы if((Price+0.00001)>0) Price=int((Price+0.00001)/1.0e-5+(0.5+1.0e-7+1.0e-13))*1.0e-5; else Price=int((Price+0.00001)/1.0e-5-(0.5+1.0e-7+1.0e-13))*1.0e-5; } Print("Result: ",Price); //--- return(GetMicrosecondCount() - StartTime); }
я не стал суммировать чистые константы, чтобы не терять время. все они гарантированно сворачиваются на этапе компиляции.
запускаем код и получаем такое же время как в исходном варианте:Custom: 1149536 msc Standard: 1767592 msc
на дребезг цифр внимание не обращайте - на уровне микросекунд, погрешности таймера и плавающей нагрузки на компьютере это в пределах нормы. пропорция сохраняется полностью. - Посмотрите на код, который вы в реальности начали тестировать из-за фиксированных исходных данных.
Так как у компилятора очень мощная оптимизация, то ваша задача была эффективно упрощена. - Так как же надо тестировать на производительность?
Понимая работу компилятора, нужно не дать ему применять предварительную оптимизацию и упрощения.
Например, сделаем вариабельным параметр digits:#define EPSILON (1.0e-7 + 1.0e-13) #define HALF_PLUS (0.5 + EPSILON) //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ double MyNormalizeDouble(const double Value,uint digits) { static const double Points[]={1.0e-0,1.0e-1,1.0e-2,1.0e-3,1.0e-4,1.0e-5,1.0e-6,1.0e-7,1.0e-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.0e8) { 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.0e8) { 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%. - Почему же так?
От оптимизации мы все равно не спаслись из-за инлайнинга. Экономия в 100 000 000 вызовов с передачей данных через стек в нашу такую же по реализации функцию NormalizeDouble запросто может дать такое ускорение.
Есть еще одно подозрение, что наш NormalizeDouble мы не вынесли в direct_call механизм при загрузке таблицы релокаций функций у MQL5 программы.
С утра проверим и если это так, то перенесем в direct_call и проверим скорость заново.
Вот такое исследование NormalizeDouble.
Наш MQL5 компилятор победил нашу системную функцию, что показывает его адекватность при сравнении со скоростью С++ кода.
Если заменить
на switch-вариант, можно будет увидеть качество реализации switch в цифрах.
Вы путаете прямой индексированный доступ к статическому массиву по константному индексу (который вырождается в константу из поля) и switch.
switch не очень-то может конкурировать с таким случаем. У switch есть несколько часто употребимых оптимизаций вида:
- "заведомо упорядоченные и короткие значения ложатся в статический массив и индексным переходом" - самый простой и быстрый, может конкурировать со статическим массивом, но не всегда.
- "несколько массивов по упорядоченным и близким кускам значений с проверками границ зон" - тут уже с тормозами
- "слишком мало проверяемых значений тупо через if проверяем" - скорость никакая, но программист сам виноват, не по месту использует switch
- "совсем разреженная упорядоченная таблица с бинарным поиском" - совсем медленно для ужастных случаев
Фактически самая лучшая стратегия для switch - это когда разработчик заведомо постарался сделать компактный по размаху набор значений в нижнем наборе чисел.
Рассмотрим почищенный вариант скрипта с NormalizeDouble:
Результаты:
Сразу замечания и объяснения:
- static тут нужен обязательно, чтобы компилятор вынес этот массив за пределы функции и не конструировал его на стеке при каждом вызове функции. Компилятор С++ делает тоже самое.
- Чтобы компилятор не выкинул цикл из-за его бесполезности, надо воспользоваться результатами вычислений. Например, сделать Print переменной Price.
- В вашей функции есть ошибка - не проверяются границы digits, что легко может дать выход за пределы массива.
Например, вызвать как MyNormalizeDouble(Price+point,10) и ловим ошибку:
Метод ускорения путем отказа от проверок - это приемлемо, но не в нашем случае. Мы ведь обязаны обрабатывать любой ошибочный ввод данных. - Добавим простое условие на индекс больше 8, причем для упрощения кода тип переменной digits заменим на uint, чтобы делать одно сравнение на >8 вместо дополнительного условия <0
double MyNormalizeDouble( const double Value, const uint digits ) { static const double Points[] = {1.0e-0, 1.0e-1, 1.0e-2, 1.0e-3, 1.0e-4, 1.0e-5, 1.0e-6, 1.0e-7, 1.0e-8}; const double point = digits > 8 ? 1.0e-8 : Points[digits]; return((int)((Value > 0) ? Value / point + HALF_PLUS : Value / point - HALF_PLUS) * point); }
- Все дело в стандартной ошибке тестировщиков производительности.
При написании тестов нужно иметь в виду полный список оптимизаций, который может быть применен компилятором. Нужно четко осознавать, какие входные данные вы используете и как они будут уничтожены, когда вы пишите упрощенный пример теста. - Так как же надо тестировать на производительность?
Понимая работу компилятора, нужно не дать ему применять предварительную оптимизацию и упрощения.
Например, сделаем вариабельным параметр digits:
Вот такое исследование NormalizeDouble.
Наш MQL5 компилятор победил нашу системную функцию, что показывает его адекватность при сравнении со скоростью С++ кода.
Вы путаете прямой индексированный доступ к статическому массиву по константному индексу (который вырождается в константу из поля) и switch.
switch не очень-то может конкурировать с таким случаем. У switch есть несколько часто употребимых оптимизаций вида:
- "заведомо упорядоченные и короткие значения ложатся в статический массив и индексным переходом" - самый простой и быстрый, может конкурировать со статическим массивом, но не всегда.
Вот тут как раз такой случай упорядоченности.
Фактически самая лучшая стратегия для switch - это когда разработчик заведомо постарался сделать компактный по размаху набор значений в нижнем наборе чисел.
Вот тут как раз такой случай упорядоченности.
Пробовал на 32-х битной системе. Там замена в примере выше на switch приводила к серьезным тормозам. На новой машине не проверял.
В каждом MQL5 есть на самом деле две скомпилированные программы: упрощенная для 32 бит и максимально оптимизированная под 64 бита. На 32 битной МТ5 вообще новый оптимизатор не применяется и код для 32 битных операционок такой же простой как для MQL4 в МТ4.
Вся эффективность компилятора, который может генерировать в десяток раз быстрее код только при исполнении в 64 битной версии МТ5: https://www.mql5.com/ru/forum/58241
Мы полностью сконцентриованы на 64 битных версиях платформы.
- отзывов: 8
- www.mql5.com
На тему NormalizeDouble есть такая ерунда
Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий
Как последовательно перебрать перечисление?
fxsaber, 2016.08.26 16:08
В описании функции есть такое примечание
Это верно только для символов, которые имеют минимальный шаг цены 10^N, где N - целое и не положительное. Если минимальный шаг цены имеет другое значение, то нормализация ценовых уровней перед OrderSend является бессмысленной операцией, которая в большинстве случаев будет приводить к возврату false 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 тогда делать
volume = NormalizePrice(volume, stepvol);
Для цен делать
NormalizePrice(Price, TickSize)
Видится правильным добавить что-то подобное в качестве перегрузки стандартной NormalizeDouble. Где второй параметр "digits" будет не int, а double.
К 2016 году большинство С++ компиляторов пришло к одинаковым уровням оптимизации.
MSVC заставляет удивляться улучшениям с каждым апдейтом, а Intel C++ как компилятор слился - так и не вылечился от своих "internal error" на больших проектах.
Еще из наших улучшений в 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.
На тему NormalizeDouble есть такая ерунда
Ну нельзя так топорно делать! Можно же в разы быстрее, забыв про NormalizeDouble
И для того же volume тогда делать
Для цен делать
Видится правильным добавить что-то подобное в качестве перегрузки стандартной NormalizeDouble. Где второй параметр "digits" будет не int, а double.
Можно оптимизировать все вокруг.
Это бесконечный процесс. Но в 99% случаев экономически невыгодный.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
NormalizeDouble
Результат 1123275 и 1666643 в пользу MyNormalizeDouble (Optimize=1). Без оптимизации - быстрее раза в четыре (на память).