Skip to content

Optimizations for CPU and MCU environments#989

Open
MarcAntoineCRUE wants to merge 2 commits intoDaveGamble:masterfrom
MarcAntoineCRUE:optimasationsPhase1
Open

Optimizations for CPU and MCU environments#989
MarcAntoineCRUE wants to merge 2 commits intoDaveGamble:masterfrom
MarcAntoineCRUE:optimasationsPhase1

Conversation

@MarcAntoineCRUE
Copy link

@MarcAntoineCRUE MarcAntoineCRUE commented Feb 22, 2026

Summary

This PR applies five surgical optimizations to cJSON.c,

Changes

1 — cJSON_Delete() : recursive → iterative traversal

File: cJSON.c · cJSON_Delete()

Before: The original implementation used implicit recursion through child nodes. Each nesting level consumed a stack frame.

After: Fully iterative using a pointer-threading technique: the sibling chain is appended to the tail of the child chain before descending, eliminating all recursion.

/* Before: implicit recursion (simplified view of original logic) */
void cJSON_Delete(cJSON *item) {
    if (item->child) cJSON_Delete(item->child);  /* stack frame per level */
    free(item);
}

/* After: iterative, O(n) time, O(1) stack */
cJSON *child = item->child;
cJSON *last_child = child;
while (last_child->next != NULL) { last_child = last_child->next; }
last_child->next = next;   /* thread siblings after child chain */
next = child;              /* descend without recursion */

2 — parse_number() : stack buffer replaces heap allocation

File: cJSON.c · parse_number()

Before: Every number encountered during parsing triggered a malloc() + free() to copy the number string before passing it to strtod().

After: A 64-byte stack buffer is used for all valid JSON numbers (max representation is ~25 chars for ±1.7976931348623157e+308). Heap allocation only occurs as a fallback for pathologically long numeric strings.

/* 64-byte stack buffer: covers 100% of real-world JSON numbers */
unsigned char number_c_string_stack[64];
unsigned char *number_c_string = NULL;

if (number_string_length < sizeof(number_c_string_stack)) {
    number_c_string = number_c_string_stack;  /* no malloc */
} else {
    number_c_string = input_buffer->hooks.allocate(number_string_length + 1);
}

3 — parse_number() : fast integer path bypasses strtod()

File: cJSON.c · parse_number()

Before: All numbers, including simple integers like 42 or -7, went through strtod() — a heavy function that handles locales, state, NaN/Inf detection, and exponent parsing.

After: A fast path detects simple integers (no ., no e/E, ≤9 digits) and computes them directly with a digit loop, bypassing strtod() entirely. All other numeric forms (floats, exponents, large integers) fall through to the original strtod() path unchanged.

/* Fast path: pure digit accumulation, no strtod */
while (*p >= '0' && *p <= '9' && digit_count < 9) {
    fast_int = fast_int * 10u + (unsigned long)(*p - '0');
    p++; fast_len++; digit_count++;
}
if ((*p < '0' || *p > '9') && *p != '.' && *p != 'e' && *p != 'E') {
    /* Simple integer confirmed: fill item and return */
    item->valuedouble = fast_negative ? -(double)fast_int : (double)fast_int;
    item->type = cJSON_Number;
    return true;
}
/* else: fall through to strtod for floats/exponents */

Safety: NaN and Infinity never reach parse_number()parse_value() rejects them at the dispatch stage (neither starts with - or 0–9). Floats and exponents are always forwarded to strtod().


4 — buffer_skip_whitespace() : explicit comparison replaces isspace()

File: cJSON.c · buffer_skip_whitespace()

Before: Used isspace() — a libc function that internally performs a locale table lookup and function call overhead on every character.

After: Explicit comparison against the four whitespace characters defined by the JSON specification (' ', '\t', '\r', '\n'). This is both correct per spec and faster.

/* Before */
while (isspace(buffer_at_offset(buffer)[0])) { buffer->offset++; }

/* After */
unsigned char c = buffer_at_offset(buffer)[0];
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { break; }
buffer->offset++;

5 — parse_value() : if-else chain → switch dispatch

File: cJSON.c · parse_value()

Before: Sequential if/else if comparisons tested strncmp() against "null", "true", "false", etc., even for common types like strings and numbers.

After: A single switch on the first byte dispatches directly to the correct handler. The compiler generates a jump table; number handling is reached with zero comparisons on common types.

switch (buffer_at_offset(input_buffer)[0]) {
    case '\"': return parse_string(item, input_buffer);
    case '-': case '0': ... case '9': return parse_number(item, input_buffer);
    case '[':  return parse_array(item, input_buffer);
    case '{':  return parse_object(item, input_buffer);
    case 'n':  /* null */ ...
    case 't':  /* true */ ...
    case 'f':  /* false */ ...
}

@MarcAntoineCRUE MarcAntoineCRUE marked this pull request as ready for review February 22, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant