001 Notes
inih Optimization: Simpler Scans Beat Fancy Tricks
0. Abstract
the best speedups did not come from adding a lot more machinery
they came from removing avoidable work that the baseline kept paying for
| Area | Main change | Best role in the final result |
|---|---|---|
| String input | memchr() plus memcpy() instead of byte-at-a-time copying | removes avoidable front-end work |
| Parser scan | explicit ASCII whitespace and delimiter tests | biggest parser win |
| SIMD | AVX2 only for a narrow delimiter/comment scan | useful on long lines, not the main story |
| C++ wrapper | sorted flat storage instead of tree storage | huge enumeration win |
bulk copy the line input
use the exact character tests the format needs
store parsed key/value data in the shape readers actually use
1. Baseline And Choke Points
2. Bulk Line Reads Beat Byte-At-A-Time Copying
Old Shape
while (num > 1 && ctx_num_left != 0) {
c = *ctx_ptr++;
ctx_num_left--;
*strp++ = c;
if (c == '\n')
break;
num--;
}
New Shape
max_copy = (size_t)num - 1;
if (max_copy > ctx_num_left)
max_copy = ctx_num_left;
newline = (const char*)memchr(ctx_ptr, '\n', max_copy);
copy_len = newline ? (size_t)(newline - ctx_ptr) + 1 : max_copy;
memcpy(str, ctx_ptr, copy_len);
str[copy_len] = '\0';
Measured Result
| Dataset | Before | After | Speedup |
|---|---|---|---|
comments-1m | 3.047 ms | 2.403 ms | 1.268x |
comments-16m | 50.175 ms | 39.473 ms | 1.271x |
long-1m | 5.127 ms | 4.366 ms | 1.174x |
long-16m | 83.372 ms | 71.810 ms | 1.161x |
3. Simpler Character Tests Were The Biggest Parser Win
Old Shape
while (end > s && isspace((unsigned char)(*--end)))
*end = '\0';
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
New Shape
static int ini_isspace(unsigned char c)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
c == '\f' || c == '\v';
}
static int ini_char_in(const char* chars, char c)
{
if (!chars)
return 0;
while (*chars) {
if (*chars++ == c)
return 1;
}
return 0;
}
Compatibility Boundary
Measured Result
| Dataset | Mode | Before | After | Speedup |
|---|---|---|---|---|
comments-1m | string warm | 2.403 ms | 1.106 ms | 2.173x |
comments-1m | file warm | 2.869 ms | 1.433 ms | 2.002x |
comments-16m | string warm | 39.473 ms | 17.932 ms | 2.201x |
comments-16m | file warm | 48.753 ms | 23.546 ms | 2.071x |
long-1m | string warm | 4.366 ms | 2.409 ms | 1.812x |
long-16m | file warm | 75.565 ms | 41.526 ms | 1.820x |
4. AVX2 Helped, But It Was Not The Main Story
Measured Result
| Dataset | Baseline | SIMD-only | Speedup |
|---|---|---|---|
long | 186.49 MiB/s | 349.71 MiB/s | 1.88x |
comments | 312.95 MiB/s | 539.66 MiB/s | 1.72x |
5. Flat Storage Made The C++ Wrapper Behave Like A Reader
New Shape
Measured Result
| Dataset | Baseline | Flat storage | Speedup |
|---|---|---|---|
compact | 138.66 ms | 1.32 ms | 105.05x |
multiline | 104.47 ms | 1.12 ms | 93.28x |
duplicate | 78.08 ms | 0.68 ms | 114.82x |
6. Combined Results
| Variant | Parser string | Parser file | C++ parse | C++ lookup | C++ enumerate |
|---|---|---|---|---|---|
| baseline | 1.000x | 1.000x | 1.000x | 1.000x | 1.000x |
| scalar-only | 2.750x | 2.138x | 0.909x | 0.702x | 0.666x |
| SIMD-only | 1.407x | 1.414x | 1.117x | 1.012x | 1.061x |
| flat reader only | 1.304x | 1.342x | 1.037x | 1.171x | 7.203x |
| final combined | 2.572x | 2.068x | 1.331x | 1.176x | 7.414x |
| Dataset | Baseline | Final |
|---|---|---|
comments | 312.95 MiB/s | 767.08 MiB/s |
long | 186.49 MiB/s | 563.58 MiB/s |
compact | 128.80 MiB/s | 238.47 MiB/s |
| Metric | Baseline | Final |
|---|---|---|
compact parse | 49.95 MiB/s | 51.13 MiB/s |
compact lookup | 7,760,030/s | 7,714,430/s |
compact enumerate | 138.66 ms | 1.36 ms |
duplicate lookup | 5,351,000/s | 7,231,130/s |
duplicate enumerate | 78.08 ms | 0.70 ms |
7. Takeaway
the parser got faster because it stopped doing unnecessary work
the wrapper got faster because it stopped storing data in a reader-hostile shape
Comments