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

 

Спасибо за подробный обзор. Ваши замечания подтолкнули библиотеку вперед. Вот техническая информация о том, что было добавлено в v3.4.0:

Принятые оптимизации

1. Сериализация с нулевым распределением (больше никаких строк)


Вы были абсолютно правы насчет того, что IntegerToString() создает временные MQL-строки, которые давят на GC. Я реализовал PutRawInteger и PutRawDouble для записи цифр непосредственно в байтовый буфер.

// Old (Heap Allocation):
PutRaw(IntegerToString(GetInt(idx)), out, pos, cap);

// New v3.4.0 (Zero-Alloc, Direct Buffer Write):
void PutRawInteger(long value, uchar &out[], int &pos, int &cap) {
    // ... writes bytes '0'-'9' directly to out[] ...
}

2. Гибридный парсинг длинных/двухзначных чисел + таблица Exp10

Я принял вашу идею накапливать целые числа как длинные (более быстрые операции ALU) и использовать таблицу поиска для дробной части (лучшая точность FP). Однако я добавил меры безопасности, которые отсутствовали в вашем фрагменте:

// My implementation of your suggestion (with safety):
if (use_long && int_val < 922337203685477580) { // LONG_MAX / 10
    int_val = int_val * 10 + (c - '0');
} else {
    // Overflow guard: Fallback to double if number > 19 digits
    if (use_long) { val = (double)int_val; use_long = false; }
    val = val * 10.0 + (c - '0');
}

Это дает нам скорость целых чисел в 99,9% случаев, но при этом безопасно обрабатывает массивные числа (например, 30-значные бигинты) без тихого переполнения.


Отклоненные предложения (и почему)

1. Использовать c > '9' в качестве разделителя.

Это ослабляет соответствие RFC 8259. Ввод типа 123abc будет молча принят как 123. Я сохранил явные проверки:

if (c == '.' || c == 'e' || c == 'E') break; // Strict validation


2. Удаление & 0xFF из GetType()
Это не лишнее. long в MQL5 является знаковым. Если в ленточном значении установлен 63-й бит (большое смещение), то >> 56 выполняет арифметический сдвиг, заполняя старшие биты 1s. Маска необходима для корректности.


3. Вывод if (i > 0) за пределы цикла
Предсказатель ветвлений процессора справляется с этим паттерном (1 промах на цикл) с незначительными затратами (~15 циклов). Это не оправдывает дублирование кода.


Версия 3.4.0 уже доступна. Спасибо за сотрудничество.

 
Jonathan Pereira #:

Отклоненные предложения (и почему)

1. Использование c > '9' в качестве разделителя

Это ослабляет соответствие стандарту RFC 8259. Ввод типа 123abc будет молча принят как 123. Я сохранил явные проверки:

Я не понял контраргумента.
 

Спор о соответствии RFC 8259 касается принципа надежности (закон Постела) против строгости при обмене данными.

Ссылка: RFC 8259, раздел 6 (Числа)
Грамматика определяется следующим образом:

number = [ minus ] int [ frac ] [ exp ]
int = zero / ( digit1-9 *DIGIT )
frac = decimal-point 1*DIGIT

Грамматика не позволяет использовать символы с конца, такие как 'a', 'b', '-' и т. д. Значение типа 123abc не является числом. Это неправильно сформированный токен.

Если парсер использует c > '9' в качестве условия остановки, он потребляет 123 и оставляет abc в буфере. В высокоскоростном синтаксическом анализаторе такое поведение неоднозначно:

  1. Внутри массива [123abc] : синтаксический анализатор считывает 123, а затем следующий токен - abc (недействительный). В конце концов, это приводит к сбою, но ошибка выглядит как "Invalid Token 'abc'", а не как первопричина "Malformed Number '123abc'". Это затрудняет отладку.
  2. Конкатенированный JSON (NDJSON): если я отправлю {"val":123}abc, то свободный парсер может принять объект и проигнорировать мусор, что потенциально может скрыть проблемы с повреждением данных в потоке.

Применяя c == '.' || c == 'e' || c == 'E', мы явно заявляем: "Это ЕДИНСТВЕННЫЕ допустимые продолжения для числа". Все остальное вызывает немедленную проверку на структурные разделители ( , } ] или пробельные символы) в вызывающем цикле или мгновенную ошибку.

Это выбор дизайна: Fail Fast vs. Fail Later. Для финансовых данных (цены/объемы) я отдаю предпочтение Failing Fast при любой неоднозначности, а не экономии 1 процессорного цикла на цифру.

Надеюсь, это прояснило ситуацию!

 
Jonathan Pereira #:

Надеюсь, это прояснит ситуацию!

Мы говорим о парсере только в FastAtof, где длина n_len уже известна. Поэтому мое условие не может создать ни малейшей проблемы.
 
fxsaber # :
Мы говорим о парсере только в FastAtof, где длина n_len уже известна. Поэтому мое условие не может создать ни малейшей проблемы.

Вы абсолютно правы. Поскольку n_len уже определяется парсером перед вызовом FastAtof , ограничения строго контролируются, поэтому предложенное вами условие ( c > '9' ) действительно безопасно и не может вызвать проблем.

Я применил его оптимизацию в последней версии (v3.4.2). Чтобы проверить влияние на производительность и убедиться в надежности, я провел бенчмарк-тест с использованием реальных рыночных данных из Binance API (OHLCV, Depth, Trades) против `JAson`. Результаты подтверждают, что анализатор работает стабильно и невероятно быстро.


Среда: MetaTrader 5 build 5614, Intel Core i7-10750H @ 2,60 ГГц

Тип полезной нагрузки Размер fast_json (Анализ) Jason (Анализировать) Ускорение
Klines (OHLCV)
100 свечей x 12 полей
17,9 КБ 134,7 мкс 2268,4 мкс 16.8x
Книга ордеров (глубина)
100 уровней (вложенные массивы)
6.4 КБ 45.3 мкс 626.0 мкс 13.8x
Последние сделки
100 обменов (Различные элементы)
14.7 КБ 121,2 мкс 1747.6 мкс 14.4x

Еще раз спасибо за то, что настаивали на деталях. Теперь код стал чище и быстрее.

🔗 Обновленный репозиторий: GitHub/Forge

fast_json
fast_json
  • 14134597
  • forge.mql5.io
Uma biblioteca JSON projetada para uso massivo de LLMs e menor latência.
 
Jonathan Pereira #:
предложенное вами условие ( c > '9' ) действительно безопасно и не может вызвать проблем.

Интересный результат.

template <typename T>
string ToBits( const T Value )
{
  string Str = NULL;
  
  for(uint i = sizeof(T) << 3; (bool)i--;)
    Str += (string)(int)(!!(Value & ((T)1 << i)));
    
  return(Str);
}

bool IsDigit( const uchar Char )
{
  return((bool)(Char & (1 << 4)));
}

void PrintChar( const uchar Char )
{
  Print(CharToString(Char) + ": " + ToBits(Char) + " - " + (string)IsDigit(Char));
}

void OnStart()
{
  for (uchar Char = '0'; Char <= '9'; Char++)
    PrintChar(Char);
    
  PrintChar('.');
  PrintChar('e');
  PrintChar('E');
}


Результат.

0: 0011 0000 - true
1: 0011 0001 - true
2: 0011 0010 - true
3: 0011 0011 - true
4: 0011 0100 - true
5: 0011 0101 - true
6: 0011 0110 - true
7: 0011 0111 - true
8: 0011 1000 - true
9: 0011 1001 - true
.: 0010 1110 - false
e: 0110 0101 - false
E: 0100 0101 - false


Таким образом, двойную проверку можно заменить одинарной.

// if (c == '.' || c > '9') // Предварительно проверенный токен: c>'9' ловит e/E
if (!(c & (1 << 4))) 
 
Более универсальный вариант.
bool IsDigit( const uchar Char )
{
// return((bool)(Char & (1 << 4));
  return((Char & 0xF0) == 0x30);
}

void OnStart()
{
  for (int Char = 0; Char <= UCHAR_MAX; Char++)
    PrintChar((uchar)Char);
}


Результат.

... - false

.: 00101110 - false
/: 00101111 - false
0: 00110000 - true
1: 00110001 - true
2: 00110010 - true
3: 00110011 - true
4: 00110100 - true
5: 00110101 - true
6: 00110110 - true
7: 00110111 - true
8: 00111000 - true
9: 00111001 - true
:: 00111010 - true
;: 00111011 - true
<: 00111100 - true
=: 00111101 - true
>: 00111110 - true
?: 00111111 - true
@: 01000000 - false
A: 01000001 - false

... - false
 
Я внимательно изучил ваш исходный код.

Имеет смысл создать счетчик для каждого условия.

int Counter[9]

if (c == '{') {
  // ...
  Counter[0]++;
} else if (c == '[') {
  // ...
  Counter[1]++;
} else if (c == '"') {
  // ...
  Counter[2]++;
} else if (c == 't') {
  // ...
  Counter[3]++;
} else if (c == 'f') {
  // ...
  Counter[4]++;
} else if (c == 'n') {
  // ...
  Counter[5]++;
} else if (g_cc[c] == CC_DIGIT) {
  // ...
  Counter[6]++;
} else {
  // ...
  Counter[7]++;
}

И расположить условия в порядке убывания счетчика.
 

При работе с числами вы делаете двойной проход по массиву.

        } else if (g_cc[c] == CC_DIGIT) {
          int start = cur;
          bool is_float = false;
          while (cur < len) {
            uchar cc = buffer[cur];
            if (cc == '.' || cc == 'e' || cc == 'E')
              is_float = true;
            else if (cc != '+' && g_cc[cc] != CC_DIGIT)
              break;
            cur++;
          }
          int n_len = cur - start;
          int idx = tape_pos++;
          if (is_float) {
            tape[idx] = ((long)J_DBL << 56) | 2;
            tape[tape_pos++] = DBL2LONG(FastAtof(start, n_len));
          } else {
            tape[idx] = ((long)J_INT << 56) | 2;
            tape[tape_pos++] = FastAtoi(start, n_len);
          }
          sp--;
        }
Но можно обойтись и одним проходом по массиву.
 


Идеальный вариант.

bool IsDigit( const uchar Char )
{
// return((Char >= '0') && (Char <= '9'));

// return((Char ^ (UCHAR_MAX - '1')) >= (UCHAR_MAX - 9));
// return((Char ^ '1') <= 9);

// return((Char ^ (UCHAR_MAX - '0')) >= (UCHAR_MAX - 9));
  return((Char ^ '0') <= 9);
}