почти первоапрельское

1 апреля 2024, 19:29
Maxim Kuznetsov
4
94

Короткая история иллюстрированная кодом, о том как компилируемый MQL слил интерпретатору Tcl в 8 раз 

Все наверное слушали, что MQL настолько крут и быстр, что порой обгоняет процессор.

А особо продвинутые слышали что интерпретаторы тормозят, а в особенности Tcl и Python.

Так вот это не так :-)

И начать наверное стоит сразу со скриншота :

и пояснений, о чём тест и что-же тут считалось:

Тест воспроизводит типичную ситуацию: из сети получен json, из него надо получить данные и посчитать.
В данном тесте взят реальный снепшот ленты сделок и по нему рассчитывается средне-взвешенная цена.

Это критичная по времени операция, потому-что потоки сделок очень быстрые и такие снепшоты прилетают со страшной силой.

Для теста на MQL взята пожалуй единственная библиотека JAson. Которой довольно часто пользуюсь, хорошая вещь, всем рекомендую.

Тест на Tcl сделан максимально подобно, без возможных оптимизаций - почти строчка в строку такой-же

#include <ATcl.mqh>
#include <JAson.mqh>
ATcl *tcl=NULL;
// типичный json часто-часто прилетающий по сети
// это небольшой снепшот торговой ленты
string message="{\"topic\":\"publicTrade.BTCUSDT\",\"ts\":1711983184581,\"type\":\"snapshot\",\"data\":[{\"i\":\"2100000000067221094\",\"T\":1711983184577,\"p\":\"68795.01\",\"v\":\"0.001577\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"210000000006722109\
5\",\"T\":1711983184577,\"p\":\"68795.01\",\"v\":\"0.020162\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221096\",\"T\":1711983184577,\"p\":\"68795.05\",\"v\":\"0.008596\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000\
000067221097\",\"T\":1711983184577,\"p\":\"68795.07\",\"v\":\"0.012178\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221098\",\"T\":1711983184577,\"p\":\"68795.09\",\"v\":\"0.008906\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"\
i\":\"2100000000067221099\",\"T\":1711983184577,\"p\":\"68795.11\",\"v\":\"0.018014\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221100\",\"T\":1711983184577,\"p\":\"68804.39\",\"v\":\"0.00189\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\"\
:false},{\"i\":\"2100000000067221101\",\"T\":1711983184577,\"p\":\"68804.41\",\"v\":\"0.001732\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221102\",\"T\":1711983184577,\"p\":\"68804.43\",\"v\":\"0.002062\",\"S\":\"Buy\",\"s\":\"BT\
CUSDT\",\"BT\":false},{\"i\":\"2100000000067221103\",\"T\":1711983184577,\"p\":\"68804.45\",\"v\":\"0.002006\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221104\",\"T\":1711983184577,\"p\":\"68804.47\",\"v\":\"0.001706\",\"S\":\"B\
uy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221105\",\"T\":1711983184577,\"p\":\"68804.49\",\"v\":\"0.002052\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221106\",\"T\":1711983184577,\"p\":\"68849.56\",\"v\":\"0.316\
107\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false},{\"i\":\"2100000000067221107\",\"T\":1711983184577,\"p\":\"69043.11\",\"v\":\"0.140744\",\"S\":\"Buy\",\"s\":\"BTCUSDT\",\"BT\":false}]}";

// обработка json
// эмулируем что-о полезное - считаем средневзвешенную цену
 double TestJAson(string msg) {
   CJAVal json; 
   double averageBuy=0,averageSell=0,averagePrice=0;
   json.Deserialize(msg);
   long ts=json["ts"].ToInt();
   if (json["topic"]=="publicTrade.BTCUSDT" && json["type"]=="snapshot") {
      CJAVal data=json["data"];
      int total=data.Size();
      double weightBuy=0,volumeBuy=0;
      double weightSell=0,volumeSell=0;
      for(int i=0;i<total;i++) {
         double price=data[i]["p"].ToDbl();
         double volume=data[i]["v"].ToDbl();
         if (data[i]["S"]=="Buy") {
            weightBuy=weightBuy+price*volume;
            volumeBuy=volumeBuy+volume;
         } else /* if (data[i]["S"]=="Sell") */ {
            weightSell=weightSell+price*volume;
            volumeSell=volumeSell+volume;
         }
      }
      if (volumeBuy!=0) averageBuy=weightBuy/volumeBuy;
      if (volumeSell!=0) averageSell=weightSell/volumeSell;
      if (volumeBuy+volumeSell!=0) averagePrice=(weightBuy+weightSell)/(volumeBuy+volumeSell);
   }
   return averagePrice;
}
// максимально идентичная процедура на tcl
// делает ровно то-же самое и так-же
string textRLJson="proc TestRLJson { msg } {   \n\
   set averageBuy 0     \n\
   set averageSell 0    \n\
   set averagePrice 0   \n\
   set ts [ json get $msg \"ts\" ] \n\
   if { [ json get $msg \"topic\" ] == \"publicTrade.BTCUSDT\" && \n\
      [ json get $msg \"type\" ] == \"snapshot\" } { \n\
      set weightBuy 0   \n\
      set weightSell 0  \n\
      set volumeBuy 0   \n\
      set volumeSell 0  \n\
      json foreach record [ json extract $msg \"data\" ] { \n\
         set price [ json get $record \"p\" ]         \n\
         set volume [ json get $record \"v\" ]        \n\
         if { [ json get $record \"S\" ] ==\"Buy\" } {   \n\
            set weightBuy [ expr { $weightBuy + $price * $volume } ] \n\
            set volumeBuy [ expr { $volumeBuy + $volume } ] \n\
         } else { \n\
            set weightSell [ expr { $weightSell + $price * $volume } ]  \n\
            set volumeSell [ expr { $volumeSell + $volume } ]  \n\
         }  \n\
      }  \n\
      if { $volumeBuy != 0 } { set averageBuy [ expr { $weightBuy / $volumeBuy } ] }   \n\
      if { $volumeSell != 0 } { set averageBuy [ expr { $weightSell / $volumeSell } ] }   \n\
      if { $volumeBuy!=0 || $volumeSell != 0 } { set averagePrice [ expr { ($weightBuy+$weightSell) / ($volumeBuy+$volumeSell) } ] }   \n\
   }  \n\
   return $averagePrice   \n\
}";

#define PASS 1000
void OnStart()
{
   ATcl_OnInit();
   /////////////////////
   // MQL
   /////////////////////
   
   // проверяем - печатаем результат
   double res1=TestJAson(message);
   PrintFormat("MQL result=%f",res1);
   // делаем PASS прогонов, считаем среднее
   ulong from=GetMicrosecondCount();
   double sum=0;
   for(int i=0;i<PASS;i++) {
      sum=sum+TestJAson(message);
   }
   ulong to=GetMicrosecondCount();
   PrintFormat("MQL %lu mcs/iteration",(to-from)/PASS);
   
   ////////////////////
   // TCL
   ////////////////////
   
   // инициализация интепретатора и литералов  
   tcl=new ATcl();

   tcl.Eval("package require rl_json");
   tcl.Eval("namespace import rl_json::json");
   tcl.Eval(textRLJson);
   
   Tcl_Obj procRLJson=tcl.Ref(tcl.Obj("TestRLJson"));   
   Tcl_Obj objMessage=tcl.Ref(tcl.Obj(message));
   
   // тестовый вызов - пачеть результата
   if (tcl.Call(procRLJson,objMessage)!=TCL_OK) {
      PrintFormat("error %s",tcl.StringResult());
      return;
   }
   PrintFormat("TCL result=%f",tcl.DoubleResult());
   
   // в цикле PASS походов и считаем среднее
   from=GetMicrosecondCount();
   sum=0;
   for(int i=0;i<PASS;i++) {
      sum=sum+tcl.DoubleCall(procRLJson,objMessage);
   }
   to=GetMicrosecondCount();
   PrintFormat("TCL %lu mcs/iteration",(to-from)/PASS);
   
   tcl.Unref(procRLJson);
   tcl.Unref(objMessage);
   delete tcl;
}

Запустили, получили неслабую разницу в 8 раз

Компилятор проиграл интерпретатору 

И пояснения что-же такое произошло-то..как-же так-то.

Конечно MQL считает математику и выполняет код гораздо быстрее. Но MQL подвёл парсинг текста и неэффективный (хоть и удобный) доступ к полям структур json. Плюс(минус) динамические структуры и ассоциативные массивы это не его. 

А Tcl внутри транслируется в эффективный байт-код, и с ассоциативными массивами у него всё очень хорошо. И парсер rl_json очень быстрый. 

То есть можно много привести объяснений почему так вышло, но

в 8 раз это в восемь раз

PS/ чуть не забыл дать ссылки на всё упомянутое:

- библиотека Json : https://www.mql5.com/ru/code/13663 Палочка-выручалочка когда надо писать/читать json или сохранять/восстанавливать состояние роботов. Чуть не единственное что есть в MQL для работы с json

- ATcl, интерпретатор Tcl для MetaTrader: https://sourceforge.net/projects/mt-atcl/ моё поделие

- для особо интересующихся, rl_json https://github.com/RubyLane/rl_json включен в состав ATcl, но по ссылке можно почитать документацию или порыться в коде