Bibliotecas: JSON Library for LLMs - página 4

 

I assume that in MQL5, due to the forced check for array out-of-bounds, this condition will be executed slower than the following one.

} else if (((с ^ '0') <= 9) || (c == '-')) {
 
Hi @fxsaber,

Man, these are fantastic suggestions. I really appreciate you taking the time to dig into the source code and point out where we could squeeze more performance.

You were absolutely right about the array bounds check overhead in MQL5. Even though g_cc is fast, the compiler's safety checks add up in a tight loop. I've scrapped the table lookup for digits and implemented your bitwise ALU check (c ^ '0') <= 9 . It’s cleaner and definitely faster.

I also took your advice on the number parsing and rewrote it to be Single-Pass. Now it consumes digits directly into the accumulator and only switches to float logic if it hits a decimal point or exponent. No more double-scanning.

Plus, I reordered the main loop branches to prioritize Strings ( " ) and Numbers, which should help with CPU branch prediction since those are the most common tokens.

Thanks again for the push. The library is significantly better because of your inputs!

🔗 v3.5.0 is live: GitHub/Forge

 
Speeded up parsing by ~10%.
Arquivos anexados:
fast_json2.mqh  40 kb
fast_json3.mqh  40 kb
 

Olá, Jônatas,

Obrigado pela excelente biblioteca.

Ao integrá-la com os fluxos Binance WebSocket, encontrei dois erros. Ambos se reproduzem na versão pública atual. Script de teste e saída no final da postagem.

(Ao integrá-la aos fluxos do Binance WebSocket, encontrei dois erros).

Bug nº 1 - true e false produzem entradas de fita idênticas

(Bug nº 1 - true e false produzem entradas de fita idênticas.)

No analisador, os ramos dos literais 't' e 'f' escrevem o mesmo bit de carga útil:

(No analisador, os ramos para os literais 't' e 'f' escrevem o mesmo bit de carga útil:)

} 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;   // deve ser | 0
  sp--;
}

GetBool() já lê o bit 0 corretamente ( tape[idx] & 1 ), portanto, isso é apenas um erro de digitação de um caractere no analisador:

(GetBool() já lê o bit 0 corretamente, portanto, isso é apenas um erro de digitação de um caractere no analisador:)

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

Após essa correção, ToBool() retorna o valor correto tanto para true quanto para false .

(Após essa correção, ToBool() retorna o valor correto tanto para true quanto para false).

 

Bug #2 - ToString() lê slots de fita adjacentes em nós que não são de string

(Bug #2 - ToString() lê slots de fita adjacentes em nós que não são strings)

CJsonNode::ToString() não tem verificação de tipo:

(CJsonNode::ToString() não tem verificação de tipo:).

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

GetStr() interpreta cegamente tape[idx + 1] como um par compactado (offset, length):

(GetStr() interpreta cegamente tape[idx + 1] como um par (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);
}

Para nós que não sejam de string, isso lê um slot de fita que não pertence ao nó atual. A manifestação mais visível ocorre em nós inteiros, em que tape[idx + 1] contém o próprio valor inteiro. Para {"v":42}, chamar r["v"].ToString() retorna todo o buffer de entrada (8 bytes), porque a carga útil 42 é decodificada como (offset=0, length=8).

Com números inteiros maiores ou deslocamentos diferentes, o mesmo caminho pode retornar um conteúdo de memória arbitrário do buffer de entrada, possivelmente fora dos limites.

Correção sugerida - corresponder à convenção usada por ToInt() :


(Para nós que não sejam strings, isso lê um slot de fita que não pertence ao nó atual. A manifestação mais perceptível ocorre em nós inteiros, onde tape[idx + 1] armazena o próprio valor inteiro. Para {"v":42}, chamar r["v"].ToString() retorna todo o buffer de entrada (8 bytes) - porque a carga útil 42 é decodificada como (offset=0, length=8). Com números inteiros maiores ou outros deslocamentos, o mesmo caminho pode retornar conteúdos arbitrários do buffer de entrada, possivelmente fora dele. A correção sugerida é seguir a convenção usada em ToInt().

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

Se o mesmo caminho retornar números inteiros ou outros deslocamentos, ele poderá retornar conteúdos arbitrários da memória do buffer de entrada, possivelmente fora dos limites.

Correção sugerida - siga a convenção usada por ToInt() :

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

Ou uma versão mais avançada que retorne uma representação de string de qualquer tipo de escalar (depende de o Bug nº 1 ser corrigido primeiro):

Ou uma versão mais avançada que retorne uma representação de string de qualquer tipo escalar (depende de o Bug nº 1 ser corrigido primeiro):

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

Reprodução

#include <fast_json.mqh>

void OnStart()
{
   //--- Bug nº 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;
}

Saída na versão atual:

---- 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=[7 B 22 76 22 3 A 34 32 7 D]                                   text=>{"v":42}
Case E: integer 42 with neighbour  -> length=21  bytes=[7 B 22 76 22 3 A 34 32 2 C 22 6 E 65 78 74 22 3 A 22 41 41 41 22 7 D]  text=>{"v":42,"next":"AAA"}

Observação Casos D e E: ToString() em um nó com valor int retornou todo o buffer de entrada bruto, porque a carga útil do int foi decodificada como offset=0, length=(tamanho do buffer) . O comprimento escala linearmente com a entrada - confirmando uma leitura fora do nó, não apenas um artefato de memória obsoleta.

(Observação sobre os casos D e E: ToString() no nó com int retornou todo o buffer de entrada original porque o payload 42 foi decodificado como (offset=0, length=buffer size). O comprimento cresce linearmente com o tamanho da entrada - isso confirma a leitura fora do nó, não apenas um artefato de memória).

Terei prazer em testar um patch se você quiser validar a correção antes de publicá-la.

Obrigado pelo trabalho nessa biblioteca!

 

Bug #2 - ToString() lê slots de fita adjacentes em nós que não são de string

CJsonNode::ToString() não tem verificação de tipo:

Bug #2 - ToString() lê slots de fita adjacentes em nós que não são strings

CJsonNode::ToString() não tem verificação de tipo:

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

GetStr() interpreta cegamente tape[idx + 1] como um par empacotado (offset, length):

GetStr() interpreta cegamente tape[idx + 1] como um par (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);
}

Para nós que não são de string, isso lê um slot de fita que não pertence ao nó atual. A manifestação mais visível ocorre em nós inteiros, em que tape[idx + 1] contém o próprio valor inteiro. Para {"v":42}, chamar r["v"].ToString() retorna todo o buffer de entrada (8 bytes), porque a carga útil 42 é decodificada como (offset=0, length=8).

Com números inteiros maiores ou deslocamentos diferentes, o mesmo caminho pode retornar um conteúdo de memória arbitrário do buffer de entrada, possivelmente fora dos limites.

Correção sugerida - corresponder à convenção usada por ToInt() :

Para nós que não sejam strings, isso lê um slot de fita que não pertence ao nó atual. A manifestação mais perceptível ocorre em nós inteiros, onde tape[idx + 1] armazena o próprio valor inteiro. Para {"v":42}, chamar r["v"].ToString() retorna todo o buffer de entrada (8 bytes) - porque a carga útil 42 é decodificada como (offset=0, length=8). Com números inteiros maiores ou outros deslocamentos, o mesmo caminho pode retornar conteúdos arbitrários do buffer de entrada, possivelmente fora dele. A correção sugerida é seguir a convenção usada em ToInt().

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

Ou uma versão mais avançada que retorne uma representação de string de qualquer tipo escalar (depende de o Bug nº 1 ser corrigido primeiro):

Ou uma versão mais avançada que retorna uma representação de string de qualquer tipo escalar (depende de o Bug nº 1 ser corrigido primeiro):

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

Reprodução

#include <fast_json.mqh>

void OnStart()
{
   //--- Bug nº 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;
}

Saída na versão atual:

---- 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=[7 B 22 76 22 3 A 34 32 7 D]                                   text=>{"v":42}
Case E: integer 42 with neighbour  -> length=21  bytes=[7 B 22 76 22 3 A 34 32 2 C 22 6 E 65 78 74 22 3 A 22 41 41 41 22 7 D]  text=>{"v":42,"next":"AAA"}

Observação Casos D e E: ToString() em um nó com valor int retornou todo o buffer de entrada bruto, porque a carga útil do int foi decodificada como offset=0, length=(tamanho do buffer) . O comprimento é escalonado linearmente com a entrada - confirmando uma leitura fora do nó, não apenas um artefato de memória obsoleta.

(Observação sobre os casos D e E: ToString() no nó com int retornou todo o buffer de entrada original porque o payload 42 foi decodificado como (offset=0, length=buffer size). O comprimento cresce linearmente com o tamanho da entrada - isso confirma a leitura fora do nó, não apenas um artefato de memória).

Terei prazer em testar um patch se você quiser validar a correção antes de publicá-la.

Obrigado pelo trabalho nessa biblioteca!

 

Thank you all for the contributions.

@fxsaber — Analyzed both files. The XOR-first pattern applied consistently across the number parsing pipeline (each byte XOR'd exactly once, all comparisons in the transformed domain) was incorporated in v3.6.0 and refined in v3.7.0. Surgical contribution, as always. Thank you.

@Sunriser — Both bugs confirmed and fixed in v3.7.0:

  • Bug #1: The  false  branch wrote  | 1  instead of  | 0 . Fixed.
  • Bug #2:  ToString()  is now type-safe — no longer reads adjacent tape slots on non-string nodes.

v3.7.0 also includes:  ParseBuffer(uchar &data[], int data_len)  for direct buffer parsing without  StringToCharArray , SWAR Unescape, key length stored in tape (eliminates scan-for-quote during serialization), direct byte writes in the serializer,  ArrayResize  with reserve, and HexToDec via lookup table.

Backward compatible. Zero breaking changes.

Note: v3.6.0 (with extended Pow10 tables, digit-pair serialization and 4x unrolled hash) was ready a while ago — I just forgot to post it. 😅

Thanks for the feedback — the library evolves thanks to contributions like these.