Библиотеки: Библиотека JSON для LLM - страница 4

 

Предполагаю, что в MQL5, из-за принудительной проверки на выход за границы массива, это условие будет выполняться медленнее, чем следующее.

} else if (((с ^ '0') <= 9) || (c == '-')) {
 
Привет @fxsaber,

Чувак, это фантастические предложения. Я очень ценю, что вы нашли время покопаться в исходном коде и указать, где мы могли бы выжать больше производительности.

Ты был абсолютно прав насчет накладных расходов на проверку границ массива в MQL5. Несмотря на то, что g_cc работает быстро, проверки компилятора на безопасность увеличиваются в узком цикле. Я отказался от поиска цифр в таблице и реализовал вашу побитовую проверку ALU (c ^ '0') <= 9 . Это чище и определенно быстрее.

Я также последовал вашему совету по поводу разбора чисел и переписал его, сделав однопроходным. Теперь он потребляет цифры непосредственно в аккумулятор и переключается на плавающую логику только при попадании на десятичную точку или экспоненту. Больше никакого двойного сканирования.

Кроме того, я изменил порядок ветвей главного цикла, отдав приоритет строкам ( " ) и числам, что должно помочь в предсказании ветвей процессора, поскольку это самые распространенные лексемы.

Еще раз спасибо за помощь. Библиотека стала значительно лучше благодаря вашему вкладу!

🔗 v3.5.0 уже в продаже: GitHub/Forge

 
Ускорение парсинга на ~10%.
Файлы:
fast_json2.mqh  40 kb
fast_json3.mqh  40 kb
 

Hi Jonathan,

Thanks for the great library.

While integrating it with Binance WebSocket streams I ran into two bugs. Both reproduce on the current public version. Test script and output at the end of the post.

(При интеграции с Binance WebSocket streams я столкнулся с двумя багами.)

Bug #1 — true and false produce identical tape entries

(Баг #1 — true и false создают одинаковые записи в tape)

In the parser, the branches for 't' and 'f' literals write the same payload bit:

(В парсере ветки для литералов 't' и 'f' записывают одинаковый бит payload:)

} else if (c == 't') {
  cur += 4;
  tape[tape_pos++] = ((long)J_BOOL << 56) | 1;
  sp--;
} else if (c == 'f') {
  cur += 5;
  tape[tape_pos++] = ((long)J_BOOL << 56) | 1;   // should be | 0
  sp--;
}

GetBool() already reads bit 0 correctly ( tape[idx] & 1 ), so this is just a one-character typo in the parser:

(GetBool() уже правильно читает бит 0, поэтому это просто опечатка в одну букву в парсере:)

tape[tape_pos++] = ((long)J_BOOL << 56) | 0;   // false branch

After this fix ToBool() returns the correct value for both true and false .

(После исправления ToBool() возвращает правильное значение и для true, и для false.)

 

Bug #2 — ToString() reads adjacent tape slots on non-string nodes

CJsonNode::ToString() has no type check:

Баг #2 — ToString() читает соседние слоты tape для не-строковых узлов

CJsonNode::ToString() не проверяет тип:

string ToString() { return IsValid() ? ctx.GetStr(idx) : ""; }

GetStr() blindly interprets tape[idx + 1] as a packed (offset, length) pair:

GetStr() слепо интерпретирует tape[idx + 1] как пару (offset, length):

string GetStr(int idx) {
    if (idx < 0 || idx >= tape_pos) return "";
    long data = tape[idx + 1];
    int p = (int)(data >> 32);
    int l = (int)(data & 0xFFFFFFFF);
    return Unescape(p, l);
}

For non-string nodes this reads a tape slot that does not belong to the current node. The most visible manifestation is on integer nodes, where tape[idx + 1] holds the integer value itself. For {"v":42} calling r["v"].ToString() returns the entire input buffer (8 bytes) — because the payload 42 happens to decode as (offset=0, length=8) .

With larger integers or different offsets the same path can return arbitrary memory contents from the input buffer, potentially out of bounds.

Suggested fix — match the convention used by ToInt() :

Для не-строковых узлов это читает слот tape, который не принадлежит текущему узлу. Самое заметное проявление — на целочисленных узлах, где tape[idx + 1] хранит само значение integer. Для {"v":42} вызов r["v"].ToString() возвращает весь входной буфер (8 байт) — потому что payload 42 случайно декодируется как (offset=0, length=8). С большими целыми числами или другими смещениями этот же путь может возвращать произвольное содержимое входного буфера, потенциально за его пределами. Предлагаемое исправление — следовать соглашению, используемому в ToInt().

string ToString() {
   if (!IsValid()) return "";
   if (ctx.GetType(idx) != J_STR) return "";
   return ctx.GetStr(idx);
}

Or a richer version that returns a string representation of any scalar type (depends on Bug #1 being fixed first):

Или более полная версия, которая возвращает строковое представление любого скалярного типа (зависит от предварительного исправления Bug #1):

string ToString() {
   if (!IsValid()) return "";
   switch(ctx.GetType(idx)) {
      case J_STR:  return ctx.GetStr(idx);
      case J_BOOL: return ctx.GetBool(idx) ? "true" : "false";
      case J_INT:  return IntegerToString(ctx.GetInt(idx));
      case J_DBL:  return DoubleToString(ctx.GetDouble(idx));
      case J_NULL: return "null";
      default:     return "";
   }
}

Reproduction

#include <fast_json.mqh>

void OnStart()
{
   //--- Bug #1
   {
      string js = "{\"a\":true,\"b\":false}";
      CJson p; if(!p.Parse(js)) { Print("parse fail"); return; }
      CJsonNode r = p.GetRoot();
      Print("---- BUG #1: true and false are indistinguishable ----");
      Print("a.ToBool() = ", r["a"].ToBool(false),
            "   (JSON: true,  expected: true)");
      Print("b.ToBool() = ", r["b"].ToBool(true),
            "   (JSON: false, expected: false)");
      Print("");
   }

   //--- Bug #2
   Print("---- BUG #2: ToString() reads adjacent tape slots on non-string nodes ----");
   TryToString("Case A: bool true                 ", "{\"v\":true}");
   TryToString("Case B: bool false                ", "{\"v\":false}");
   TryToString("Case C: null                      ", "{\"v\":null}");
   TryToString("Case D: integer 42                ", "{\"v\":42}");
   TryToString("Case E: integer 42 with neighbour ", "{\"v\":42,\"next\":\"AAA\"}");
}

void TryToString(string label, string js)
{
   CJson p; if(!p.Parse(js)) { Print(label, "  PARSE FAIL"); return; }
   CJsonNode r = p.GetRoot();
   string s = r["v"].ToString();
   PrintFormat("%s -> length=%d  bytes=[%s]  text=>%s<",
               label, StringLen(s), HexDump(s), s);
}

string HexDump(string s)
{
   string out = "";
   int n = StringLen(s);
   for(int i = 0; i < n && i < 32; i++) {
      ushort code = StringGetCharacter(s, i);
      out += StringFormat("%02X ", code);
   }
   return out;
}

Output on the current version:

---- BUG #1: true and false are indistinguishable ----
a.ToBool() = true   (JSON: true,  expected: true)
b.ToBool() = true   (JSON: false, expected: false)

---- BUG #2: ToString() reads adjacent tape slots on non-string nodes ----
Case A: bool true                  -> length=0   bytes=[]                                                          text=>
Case B: bool false                 -> length=0   bytes=[]                                                          text=>
Case C: null                       -> length=0   bytes=[]                                                          text=>
Case D: integer 42                 -> length=8   bytes=[7B 22 76 22 3A 34 32 7D]                                   text=>{"v":42}
Case E: integer 42 with neighbour  -> length=21  bytes=[7B 22 76 22 3A 34 32 2C 22 6E 65 78 74 22 3A 22 41 41 41 22 7D]  text=>{"v":42,"next":"AAA"}

Note Cases D and E: ToString() on an int-valued node returned the entire raw input buffer, because the int's payload decoded as offset=0, length=(buffer size) . Length scales linearly with input — confirming an out-of-node read, not just a stale memory artifact.

(Примечание по случаям D и E: ToString() на узле с int вернул весь исходный входной буфер, так как payload 42 декодировался как (offset=0, length=размер буфера). Длина растёт линейно с размером входных данных — это подтверждает чтение за пределами узла, а не просто артефакт памяти.)

Happy to test a patch if you want to validate the fix before publishing.

Thanks for the work on this library!

 

Спасибо всем за участие.

@fxsaber - Проанализировал оба файла. Шаблон XOR-first, применяемый последовательно во всем конвейере разбора чисел (каждый байт XOR'ится ровно один раз, все сравнения в преобразованной области), был включен в v3.6.0 и доработан в v3.7.0. Хирургический вклад, как всегда. Спасибо.

@Sunriser - Обе ошибки подтверждены и исправлены в v3.7.0:

  • Ошибка #1: ложная ветка писала | 1 вместо | 0 . Исправлено.
  • Ошибка #2: ToString() теперь безопасна для типов - больше не считывает соседние слоты ленты на нестроковых узлах.

Вv3.7.0 также добавлены: ParseBuffer(uchar &data[], int data_len) для прямого разбора буфера без StringToCharArray , SWAR Unescape, длина ключа хранится в ленте (исключает сканирование для цитирования при сериализации), прямая запись байтов в сериализатор, ArrayResize с запасом и HexToDec через таблицу поиска.

Обратная совместимость. Ноль ломающих изменений.

Примечание: версия 3.6.0 (с расширенными таблицами Pow10, сериализацией по парам цифр и 4-кратным развёрнутым хэшем) была готова уже давно - я просто забыл её выложить. 😅

Спасибо за отзывы - библиотека развивается благодаря подобным вкладам.

 
Вот файл, в котором я исправил несколько ошибок, связанных с тем, что функция HasKey() возвращала значение false, хотя JSON-файл по-прежнему существовал. Также я удалил несколько предупреждений.
Файлы: