Skip to content

Experiment: SOUFFLE_INLINE_STRING_MAX = 13#96

Merged
frostney merged 7 commits into
mainfrom
exp/inline-string-15
Mar 16, 2026
Merged

Experiment: SOUFFLE_INLINE_STRING_MAX = 13#96
frostney merged 7 commits into
mainfrom
exp/inline-string-15

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Mar 15, 2026

Experiment

Reduces SOUFFLE_INLINE_STRING_MAX from 23 to 15, shrinking TSouffleValue from 26 bytes to 16 bytes. Strings longer than 15 chars spill to TSouffleHeapString on the GC heap.

Hypothesis: Smaller record size improves cache density in the VM register file (FRegisters), reducing cache misses in the dispatch loop. 15 chars is enough to cover most runtime strings (property names, typeof results, short literals) without heap spill, making this a less aggressive trade-off than the 7-char variant (PR #95).

Size comparison:

Variant Inline max Record size Strings that spill
Current 23 chars 26 bytes None (all common strings fit)
This PR 13 chars 16 bytes 14-23 char strings
PR #95 7 chars 10 bytes 8-23 char strings (6 test failures)

All tests pass in both modes — no strings in the test suite are in the 16-23 char range that would spill.

Test plan

  • Interpreter: 3422 passed, 0 failed, 41 skipped
  • Bytecode: 3463 passed, 0 failed, 0 skipped
  • CI benchmarks for performance comparison

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes

    • String equality now compares actual string content for heap-backed strings.
    • Relational operators (>=, <=) consistently perform lexicographic comparison for string operands.
  • Optimizations

    • Reduced inline string storage threshold so shorter strings are stored inline more often.
  • Tests

    • Added comprehensive string-comparison tests covering lengths, boundaries, Unicode, and edge cases.

Shrinks TSouffleValue record from 26 bytes to 18 bytes. Strings longer
than 15 chars spill to TSouffleHeapString. This tests whether the
smaller register file footprint improves cache locality without the
heap allocation penalty of the more aggressive 7-char variant.

All tests pass in both modes.

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Lowers inline string threshold, compares heap-backed strings by value, treats string primitives as non-reference for method invocation, adds explicit string-aware >=/<= comparison, and introduces comprehensive string comparison tests including Unicode and boundary cases.

Changes

Cohort / File(s) Summary
String Value Handling
souffle/Souffle.Value.pas
Reduced SOUFFLE_INLINE_STRING_MAX from 23 to 13; updated SouffleValuesEqual to compare contents when both references are TSouffleHeapString, keeping other reference comparisons unchanged.
Runtime Operations
units/Goccia.Runtime.Operations.pas
Modified InvokeMethodForPrimitive condition to treat string primitives as non-reference by adding SouffleIsStringValue(APrimitive) to the branch decision.
Evaluator Comparisons
units/Goccia.Evaluator.Comparison.pas
Added explicit lexicographic handling for >= and <= when both operands are TGocciaStringLiteralValue, bypassing numeric coercion in that branch.
Tests
tests/language/expressions/comparison/string-comparison.js
Added extensive string comparison tests (equality, inequality, ordering) covering empty, short, medium, long, boundary-length strings and broad Unicode/edge-case scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰✨ I nibble bytes where strings grow and play,
From twenty-three down to thirteen I stray,
Heap tales judged by meaning, not by sight,
Primitives hop different in the moonlight,
Unicode blossoms — I thump and say hooray!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: reducing SOUFFLE_INLINE_STRING_MAX from 23 to 13, which is the core experimental change affecting TSouffleValue size and cache density.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch exp/inline-string-15
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Fix InvokeMethodForPrimitive to treat TSouffleHeapString as a
primitive value (matching the existing pattern at line 1461). Without
this, toString() returning a string >13 chars becomes a heap string
reference, causing a false TypeError.

Made-with: Cursor
Same fix as exp/inline-string-7: two TSouffleHeapString objects with
identical content must compare by value, not pointer identity. This
ensures string equality works correctly when strings spill from inline
to heap representation.

Made-with: Cursor
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 15, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 3497 3497
Tests Passed 3456 ✅ 3497 ✅
Tests Skipped 41 0
Tests Execution 161.6ms 159.9ms
Tests Engine 320.4ms 646.5ms
Benchmarks Total 254 254
Benchmarks Duration 6.69min 11.72min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 15, 2026

Benchmark Results

254 benchmarks

Interpreted: 🟢 135 improved · 🔴 2 regressed · 117 unchanged · avg +7.8%
Bytecode: 🟢 100 improved · 🔴 11 regressed · 143 unchanged · avg +6.3%

arraybuffer.js — Interp: 🟢 12, 2 unch. · avg +16.9% · Bytecode: 14 unch. · avg +0.5%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 422,378 → 495,695 🟢 +17.4% 144,098 → 147,711 +2.5%
create ArrayBuffer(64) 410,258 → 488,391 🟢 +19.0% 140,912 → 144,268 +2.4%
create ArrayBuffer(1024) 309,423 → 387,558 🟢 +25.3% 127,618 → 130,108 +2.0%
create ArrayBuffer(8192) 136,669 → 168,628 🟢 +23.4% 80,623 → 82,172 +1.9%
slice full buffer (64 bytes) 503,510 → 594,726 🟢 +18.1% 395,815 → 384,955 -2.7%
slice half buffer (512 of 1024 bytes) 431,493 → 516,458 🟢 +19.7% 343,411 → 339,914 -1.0%
slice with negative indices 438,625 → 524,440 🟢 +19.6% 374,336 → 368,065 -1.7%
slice empty range 498,505 → 585,757 🟢 +17.5% 396,238 → 383,994 -3.1%
byteLength access 1,583,894 → 1,685,910 +6.4% 1,205,985 → 1,187,366 -1.5%
Symbol.toStringTag access 1,174,935 → 1,143,843 -2.6% 587,177 → 601,616 +2.5%
ArrayBuffer.isView 739,575 → 805,174 🟢 +8.9% 497,837 → 499,686 +0.4%
clone ArrayBuffer(64) 365,854 → 450,085 🟢 +23.0% 328,206 → 334,415 +1.9%
clone ArrayBuffer(1024) 286,172 → 360,540 🟢 +26.0% 251,916 → 255,650 +1.5%
clone ArrayBuffer inside object 263,438 → 303,778 🟢 +15.3% 158,012 → 161,069 +1.9%
arrays.js — Interp: 🟢 17, 2 unch. · avg +9.6% · Bytecode: 🟢 10, 9 unch. · avg +11.8%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 14,008 → 15,322 🟢 +9.4% 13,778 → 15,047 🟢 +9.2%
Array.from 10 elements 230,520 → 258,802 🟢 +12.3% 164,111 → 167,218 +1.9%
Array.of 10 elements 314,696 → 349,116 🟢 +10.9% 231,316 → 231,331 +0.0%
spread into new array 339,606 → 375,921 🟢 +10.7% 685,207 → 890,720 🟢 +30.0%
map over 50 elements 28,972 → 31,061 🟢 +7.2% 24,022 → 25,761 🟢 +7.2%
filter over 50 elements 24,196 → 26,038 🟢 +7.6% 22,819 → 25,467 🟢 +11.6%
reduce sum 50 elements 27,863 → 29,463 +5.7% 20,451 → 21,788 +6.5%
forEach over 50 elements 22,194 → 24,532 🟢 +10.5% 25,617 → 27,855 🟢 +8.7%
find in 50 elements 35,823 → 38,547 🟢 +7.6% 29,857 → 32,937 🟢 +10.3%
sort 20 elements 12,256 → 13,372 🟢 +9.1% 11,959 → 12,451 +4.1%
flat nested array 115,519 → 131,417 🟢 +13.8% 368,586 → 476,570 🟢 +29.3%
flatMap 73,059 → 83,309 🟢 +14.0% 249,237 → 320,452 🟢 +28.6%
map inside map (5x5) 21,786 → 24,091 🟢 +10.6% 74,758 → 107,036 🟢 +43.2%
filter inside map (5x10) 16,212 → 17,891 🟢 +10.4% 14,231 → 15,221 +7.0%
reduce inside map (5x10) 19,648 → 21,297 🟢 +8.4% 14,726 → 15,159 +2.9%
forEach inside forEach (5x10) 16,813 → 17,692 +5.2% 16,335 → 17,171 +5.1%
find inside some (10x10) 13,984 → 15,253 🟢 +9.1% 11,459 → 12,232 +6.7%
map+filter chain nested (5x20) 5,570 → 6,021 🟢 +8.1% 4,740 → 5,112 🟢 +7.8%
reduce flatten (10x5) 39,057 → 43,603 🟢 +11.6% 6,102 → 6,317 +3.5%
async-await.js — Interp: 🟢 6 · avg +14.2% · Bytecode: 6 unch. · avg -0.4%
Benchmark Interpreted Δ Bytecode Δ
single await 372,912 → 428,075 🟢 +14.8% 290,821 → 284,770 -2.1%
multiple awaits 165,457 → 192,579 🟢 +16.4% 123,008 → 123,960 +0.8%
await non-Promise value 839,954 → 980,423 🟢 +16.7% 1,052,244 → 1,040,683 -1.1%
await with try/catch 365,615 → 418,399 🟢 +14.4% 280,655 → 281,781 +0.4%
await Promise.all 51,648 → 57,571 🟢 +11.5% 44,257 → 43,959 -0.7%
nested async function call 187,672 → 209,270 🟢 +11.5% 218,830 → 218,922 +0.0%
classes.js — Interp: 🟢 31 · avg +13.7% · Bytecode: 🟢 13, 🔴 2, 16 unch. · avg +4.7%
Benchmark Interpreted Δ Bytecode Δ
simple class new 114,878 → 132,052 🟢 +14.9% 361,703 → 385,852 +6.7%
class with defaults 92,659 → 105,485 🟢 +13.8% 257,876 → 269,994 +4.7%
50 instances via Array.from 5,501 → 6,394 🟢 +16.2% 6,087 → 6,847 🟢 +12.5%
instance method call 58,738 → 66,657 🟢 +13.5% 162,046 → 184,762 🟢 +14.0%
static method call 91,295 → 102,852 🟢 +12.7% 371,143 → 430,330 🟢 +15.9%
single-level inheritance 45,901 → 52,043 🟢 +13.4% 162,296 → 182,210 🟢 +12.3%
two-level inheritance 38,852 → 44,364 🟢 +14.2% 134,392 → 149,493 🟢 +11.2%
private field access 57,921 → 66,388 🟢 +14.6% 175,792 → 196,462 🟢 +11.8%
private methods 63,063 → 71,935 🟢 +14.1% 224,603 → 255,579 🟢 +13.8%
getter/setter access 65,534 → 73,704 🟢 +12.5% 173,780 → 189,951 🟢 +9.3%
class decorator (identity) 80,183 → 93,016 🟢 +16.0% 55,309 → 50,775 🔴 -8.2%
class decorator (wrapping) 46,876 → 53,441 🟢 +14.0% 39,296 → 38,040 -3.2%
identity method decorator 57,213 → 66,528 🟢 +16.3% 46,407 → 46,107 -0.6%
wrapping method decorator 47,390 → 53,796 🟢 +13.5% 39,755 → 42,350 +6.5%
stacked method decorators (x3) 33,607 → 38,077 🟢 +13.3% 28,400 → 26,599 -6.3%
identity field decorator 64,819 → 74,124 🟢 +14.4% 47,799 → 44,367 🔴 -7.2%
field initializer decorator 55,161 → 63,064 🟢 +14.3% 42,884 → 42,121 -1.8%
getter decorator (identity) 55,721 → 63,931 🟢 +14.7% 42,150 → 42,263 +0.3%
setter decorator (identity) 47,292 → 54,182 🟢 +14.6% 36,763 → 36,749 -0.0%
static method decorator 61,162 → 70,266 🟢 +14.9% 65,069 → 66,601 +2.4%
static field decorator 71,250 → 81,215 🟢 +14.0% 67,246 → 69,024 +2.6%
private method decorator 46,999 → 53,199 🟢 +13.2% 38,869 → 38,831 -0.1%
private field decorator 52,279 → 58,964 🟢 +12.8% 39,719 → 39,913 +0.5%
plain auto-accessor (no decorator) 87,053 → 102,515 🟢 +17.8% 56,042 → 55,925 -0.2%
auto-accessor with decorator 51,301 → 59,329 🟢 +15.6% 38,598 → 38,655 +0.1%
decorator writing metadata 42,552 → 47,970 🟢 +12.7% 44,166 → 44,081 -0.2%
static getter read 104,068 → 115,099 🟢 +10.6% 404,586 → 446,227 🟢 +10.3%
static getter/setter pair 78,046 → 84,158 🟢 +7.8% 217,079 → 238,808 🟢 +10.0%
inherited static getter 58,566 → 66,632 🟢 +13.8% 275,484 → 302,856 🟢 +9.9%
inherited static setter 63,266 → 70,937 🟢 +12.1% 215,147 → 235,310 🟢 +9.4%
inherited static getter with this binding 53,637 → 58,709 🟢 +9.5% 163,019 → 178,988 🟢 +9.8%
closures.js — Interp: 🟢 7, 4 unch. · avg +8.9% · Bytecode: 🟢 8, 3 unch. · avg +15.7%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 128,529 → 135,857 +5.7% 662,188 → 770,190 🟢 +16.3%
closure over multiple variables 118,737 → 126,486 +6.5% 400,210 → 472,883 🟢 +18.2%
nested closures 122,568 → 134,378 🟢 +9.6% 614,089 → 679,565 🟢 +10.7%
function as argument 95,779 → 102,264 +6.8% 554,512 → 693,080 🟢 +25.0%
function returning function 117,096 → 128,838 🟢 +10.0% 630,173 → 769,350 🟢 +22.1%
compose two functions 70,834 → 76,867 🟢 +8.5% 379,365 → 471,252 🟢 +24.2%
fn.call 149,432 → 163,059 🟢 +9.1% 142,190 → 142,433 +0.2%
fn.apply 109,240 → 124,337 🟢 +13.8% 96,570 → 99,694 +3.2%
fn.bind 135,737 → 148,278 🟢 +9.2% 150,038 → 151,831 +1.2%
recursive sum to 50 11,733 → 12,484 +6.4% 39,642 → 52,603 🟢 +32.7%
recursive tree traversal 19,094 → 21,434 🟢 +12.3% 63,302 → 75,339 🟢 +19.0%
collections.js — Interp: 🟢 6, 6 unch. · avg +7.5% · Bytecode: 12 unch. · avg -0.5%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 6,979 → 7,483 🟢 +7.2% 5,896 → 5,810 -1.5%
has lookup (50 elements) 88,888 → 93,217 +4.9% 88,833 → 88,163 -0.8%
delete elements 46,989 → 48,580 +3.4% 37,134 → 36,904 -0.6%
forEach iteration 15,121 → 16,668 🟢 +10.2% 16,671 → 17,397 +4.4%
spread to array 32,561 → 36,226 🟢 +11.3% 153,284 → 155,606 +1.5%
deduplicate array 41,157 → 44,708 🟢 +8.6% 48,442 → 48,998 +1.1%
set 50 entries 5,192 → 5,535 +6.6% 5,920 → 6,040 +2.0%
get lookup (50 entries) 84,648 → 88,743 +4.8% 96,402 → 90,913 -5.7%
has check 127,491 → 134,511 +5.5% 154,199 → 153,284 -0.6%
delete entries 44,837 → 46,635 +4.0% 35,362 → 34,921 -1.2%
forEach iteration 15,377 → 17,282 🟢 +12.4% 17,236 → 17,436 +1.2%
keys/values/entries 8,626 → 9,529 🟢 +10.5% 23,529 → 22,242 -5.5%
destructuring.js — Interp: 🟢 17, 5 unch. · avg +9.7% · Bytecode: 🟢 16, 6 unch. · avg +15.9%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 427,029 → 450,252 +5.4% 822,371 → 1,042,293 🟢 +26.7%
with rest element 282,448 → 309,191 🟢 +9.5% 605,746 → 789,889 🟢 +30.4%
with defaults 432,459 → 455,072 +5.2% 831,995 → 947,858 🟢 +13.9%
skip elements 431,816 → 486,059 🟢 +12.6% 994,974 → 1,204,725 🟢 +21.1%
nested array destructuring 184,055 → 191,997 +4.3% 449,516 → 498,705 🟢 +10.9%
swap variables 495,103 → 556,699 🟢 +12.4% 1,195,361 → 1,426,600 🟢 +19.3%
simple object destructuring 296,889 → 324,749 🟢 +9.4% 566,310 → 625,403 🟢 +10.4%
with defaults 356,972 → 409,935 🟢 +14.8% 319,699 → 322,998 +1.0%
with renaming 299,212 → 357,667 🟢 +19.5% 659,255 → 724,606 🟢 +9.9%
nested object destructuring 149,274 → 159,886 🟢 +7.1% 265,364 → 287,252 🟢 +8.2%
rest properties 179,446 → 209,807 🟢 +16.9% 232,315 → 242,231 +4.3%
object parameter 92,137 → 102,995 🟢 +11.8% 192,241 → 206,621 🟢 +7.5%
array parameter 123,416 → 139,717 🟢 +13.2% 351,450 → 425,591 🟢 +21.1%
mixed destructuring in map 36,241 → 38,946 🟢 +7.5% 35,728 → 39,591 🟢 +10.8%
forEach with array destructuring 66,540 → 71,631 🟢 +7.7% 158,531 → 205,632 🟢 +29.7%
map with array destructuring 69,522 → 74,952 🟢 +7.8% 192,436 → 262,205 🟢 +36.3%
filter with array destructuring 71,697 → 78,372 🟢 +9.3% 230,082 → 300,751 🟢 +30.7%
reduce with array destructuring 78,185 → 84,143 🟢 +7.6% 208,103 → 286,837 🟢 +37.8%
map with object destructuring 79,908 → 86,422 🟢 +8.2% 78,205 → 82,268 +5.2%
map with nested destructuring 65,553 → 70,003 +6.8% 66,175 → 69,773 +5.4%
map with rest in destructuring 39,548 → 42,115 +6.5% 22,879 → 23,958 +4.7%
map with defaults in destructuring 60,733 → 66,196 🟢 +9.0% 38,228 → 40,222 +5.2%
fibonacci.js — Interp: 🟢 2, 6 unch. · avg +5.8% · Bytecode: 🟢 6, 2 unch. · avg +20.4%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 328 → 341 +3.9% 1,105 → 1,473 🟢 +33.3%
recursive fib(20) 29 → 31 +3.7% 100 → 133 🟢 +33.1%
recursive fib(15) typed 325 → 341 +4.8% 1,475 → 1,966 🟢 +33.3%
recursive fib(20) typed 29 → 30 +3.3% 129 → 178 🟢 +38.0%
iterative fib(20) via reduce 12,427 → 12,828 +3.2% 8,842 → 9,001 +1.8%
iterator fib(20) 9,405 → 10,047 +6.8% 14,284 → 15,957 🟢 +11.7%
iterator fib(20) via Iterator.from + take 14,624 → 16,180 🟢 +10.6% 16,718 → 17,293 +3.4%
iterator fib(20) last value via reduce 11,101 → 12,178 🟢 +9.7% 12,695 → 13,770 🟢 +8.5%
for-of.js — Interp: 7 unch. · avg +3.5% · Bytecode: 🟢 7 · avg +12.3%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 48,132 → 50,615 +5.2% 150,308 → 166,799 🟢 +11.0%
for...of with 100-element array 5,435 → 5,616 +3.3% 21,364 → 24,834 🟢 +16.2%
for...of with string (10 chars) 34,038 → 36,191 +6.3% 121,372 → 137,775 🟢 +13.5%
for...of with Set (10 elements) 48,162 → 49,489 +2.8% 156,668 → 175,398 🟢 +12.0%
for...of with Map entries (10 entries) 30,669 → 31,408 +2.4% 52,573 → 57,814 🟢 +10.0%
for...of with destructuring 41,766 → 42,104 +0.8% 75,295 → 83,591 🟢 +11.0%
for-await-of with sync array 45,357 → 46,976 +3.6% 132,217 → 148,546 🟢 +12.3%
iterators.js — Interp: 🟢 3, 17 unch. · avg +4.7% · Bytecode: 🟢 1, 🔴 1, 18 unch. · avg +0.6%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 15,094 → 15,808 +4.7% 17,702 → 18,011 +1.7%
Iterator.from({next}).toArray() — 50 elements 6,541 → 6,808 +4.1% 7,975 → 8,226 +3.1%
spread pre-wrapped iterator — 20 elements 11,382 → 12,048 +5.9% 16,483 → 17,048 +3.4%
Iterator.from({next}).forEach — 50 elements 4,440 → 4,759 🟢 +7.2% 5,531 → 5,844 +5.6%
Iterator.from({next}).reduce — 50 elements 4,535 → 4,770 +5.2% 5,242 → 5,552 +5.9%
wrap array iterator 174,167 → 188,716 🟢 +8.4% 110,221 → 114,426 +3.8%
wrap plain {next()} object 10,426 → 10,714 +2.8% 11,546 → 12,265 +6.2%
map + toArray (50 elements) 4,587 → 4,870 +6.2% 5,734 → 5,698 -0.6%
filter + toArray (50 elements) 4,450 → 4,708 +5.8% 5,528 → 5,531 +0.1%
take(10) + toArray (50 element source) 26,683 → 27,766 +4.1% 27,503 → 28,116 +2.2%
drop(40) + toArray (50 element source) 6,546 → 6,666 +1.8% 8,349 → 8,059 -3.5%
chained map + filter + take (100 element source) 8,367 → 8,740 +4.5% 9,985 → 9,335 -6.5%
some + every (50 elements) 2,629 → 2,747 +4.5% 3,609 → 3,415 -5.4%
find (50 elements) 5,788 → 5,861 +1.3% 7,199 → 7,055 -2.0%
array.values().map().filter().toArray() 8,869 → 9,547 🟢 +7.6% 10,281 → 10,389 +1.1%
array.values().take(5).toArray() 210,354 → 222,262 +5.7% 163,056 → 151,617 🔴 -7.0%
array.values().drop(45).toArray() 198,854 → 208,419 +4.8% 138,445 → 148,573 🟢 +7.3%
map.entries() chained helpers 10,848 → 11,242 +3.6% 5,375 → 5,511 +2.5%
set.values() chained helpers 18,433 → 19,099 +3.6% 21,409 → 21,488 +0.4%
string iterator map + toArray 13,778 → 14,005 +1.7% 22,753 → 21,478 -5.6%
json.js — Interp: 🟢 4, 16 unch. · avg +5.2% · Bytecode: 🟢 2, 🔴 2, 16 unch. · avg +0.7%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 165,668 → 176,065 +6.3% 144,409 → 138,039 -4.4%
parse nested object 105,420 → 110,648 +5.0% 93,840 → 84,767 🔴 -9.7%
parse array of objects 56,125 → 59,269 +5.6% 50,075 → 45,491 🔴 -9.2%
parse large flat object 49,880 → 51,013 +2.3% 47,696 → 45,324 -5.0%
parse mixed types 71,224 → 74,564 +4.7% 62,659 → 62,779 +0.2%
stringify simple object 196,868 → 209,814 +6.6% 156,088 → 169,535 🟢 +8.6%
stringify nested object 110,741 → 115,972 +4.7% 85,048 → 93,220 🟢 +9.6%
stringify array of objects 63,478 → 61,315 -3.4% 51,366 → 52,628 +2.5%
stringify mixed types 87,940 → 88,956 +1.2% 72,238 → 76,184 +5.5%
reviver doubles numbers 44,188 → 48,390 🟢 +9.5% 45,662 → 47,190 +3.3%
reviver filters properties 38,253 → 42,327 🟢 +10.6% 47,780 → 47,319 -1.0%
reviver on nested object 49,999 → 54,539 🟢 +9.1% 53,475 → 53,377 -0.2%
reviver on array 29,194 → 31,746 🟢 +8.7% 30,091 → 30,652 +1.9%
replacer function doubles numbers 47,928 → 51,104 +6.6% 54,371 → 55,226 +1.6%
replacer function excludes properties 60,199 → 63,874 +6.1% 62,995 → 65,535 +4.0%
array replacer (allowlist) 113,648 → 117,745 +3.6% 102,654 → 104,809 +2.1%
stringify with 2-space indent 97,115 → 101,236 +4.2% 80,398 → 82,008 +2.0%
stringify with tab indent 98,690 → 101,647 +3.0% 81,741 → 83,939 +2.7%
parse then stringify 54,486 → 57,488 +5.5% 52,315 → 52,022 -0.6%
stringify then parse 32,685 → 34,240 +4.8% 31,174 → 31,048 -0.4%
jsx.jsx — Interp: 🟢 12, 9 unch. · avg +7.7% · Bytecode: 🟢 20, 1 unch. · avg +16.9%
Benchmark Interpreted Δ Bytecode Δ
simple element 189,974 → 218,641 🟢 +15.1% 657,306 → 788,451 🟢 +20.0%
self-closing element 207,342 → 219,159 +5.7% 695,438 → 807,862 🟢 +16.2%
element with string attribute 165,917 → 181,340 🟢 +9.3% 474,865 → 566,725 🟢 +19.3%
element with multiple attributes 146,012 → 152,663 +4.6% 416,307 → 463,210 🟢 +11.3%
element with expression attribute 161,275 → 173,544 🟢 +7.6% 469,102 → 546,012 🟢 +16.4%
text child 195,737 → 211,761 🟢 +8.2% 673,857 → 776,164 🟢 +15.2%
expression child 192,451 → 208,689 🟢 +8.4% 649,670 → 779,545 🟢 +20.0%
mixed text and expression 180,965 → 196,336 🟢 +8.5% 593,133 → 705,811 🟢 +19.0%
nested elements (3 levels) 72,433 → 81,927 🟢 +13.1% 258,598 → 316,089 🟢 +22.2%
sibling children 55,405 → 60,856 🟢 +9.8% 193,189 → 230,030 🟢 +19.1%
component element 144,549 → 152,586 +5.6% 463,023 → 534,842 🟢 +15.5%
component with children 86,769 → 95,108 🟢 +9.6% 292,589 → 339,679 🟢 +16.1%
dotted component 116,918 → 129,744 🟢 +11.0% 343,189 → 404,398 🟢 +17.8%
empty fragment 213,014 → 219,726 +3.2% 699,081 → 817,489 🟢 +16.9%
fragment with children 54,818 → 59,831 🟢 +9.1% 193,575 → 236,131 🟢 +22.0%
spread attributes 104,170 → 112,404 🟢 +7.9% 110,958 → 119,192 🟢 +7.4%
spread with overrides 92,526 → 98,588 +6.6% 82,203 → 88,525 🟢 +7.7%
shorthand props 154,368 → 165,007 +6.9% 447,119 → 527,627 🟢 +18.0%
nav bar structure 26,473 → 27,495 +3.9% 81,359 → 102,826 🟢 +26.4%
card component tree 31,231 → 32,134 +2.9% 90,796 → 110,187 🟢 +21.4%
10 list items via Array.from 14,204 → 14,997 +5.6% 24,512 → 26,032 +6.2%
numbers.js — Interp: 🟢 8, 3 unch. · avg +8.1% · Bytecode: 🟢 5, 6 unch. · avg +5.3%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 558,441 → 558,232 -0.0% 1,723,543 → 2,082,693 🟢 +20.8%
floating point arithmetic 575,953 → 625,183 🟢 +8.5% 1,882,398 → 2,247,537 🟢 +19.4%
number coercion 173,550 → 195,756 🟢 +12.8% 139,737 → 140,636 +0.6%
toFixed 99,633 → 109,686 🟢 +10.1% 221,505 → 224,952 +1.6%
toString 152,232 → 165,721 🟢 +8.9% 780,604 → 848,595 🟢 +8.7%
valueOf 215,257 → 241,594 🟢 +12.2% 1,127,743 → 1,304,670 🟢 +15.7%
toPrecision 140,677 → 144,353 +2.6% 435,451 → 466,638 🟢 +7.2%
Number.isNaN 294,345 → 327,234 🟢 +11.2% 183,756 → 178,840 -2.7%
Number.isFinite 295,419 → 319,326 🟢 +8.1% 177,626 → 170,078 -4.2%
Number.isInteger 286,457 → 315,888 🟢 +10.3% 193,070 → 187,552 -2.9%
Number.parseInt and parseFloat 238,706 → 248,809 +4.2% 163,785 → 154,939 -5.4%
objects.js — Interp: 🟢 1, 6 unch. · avg +2.1% · Bytecode: 🟢 4, 3 unch. · avg +7.6%
Benchmark Interpreted Δ Bytecode Δ
create simple object 452,604 → 451,426 -0.3% 907,086 → 1,022,635 🟢 +12.7%
create nested object 207,510 → 224,930 🟢 +8.4% 387,952 → 442,390 🟢 +14.0%
create 50 objects via Array.from 8,894 → 9,353 +5.2% 8,710 → 8,845 +1.5%
property read 617,561 → 590,240 -4.4% 762,539 → 920,498 🟢 +20.7%
Object.keys 285,698 → 298,349 +4.4% 213,029 → 202,011 -5.2%
Object.entries 107,024 → 105,474 -1.4% 64,275 → 64,393 +0.2%
spread operator 180,388 → 185,425 +2.8% 197,494 → 215,202 🟢 +9.0%
promises.js — Interp: 🔴 1, 11 unch. · avg -1.6% · Bytecode: 🟢 1, 🔴 3, 8 unch. · avg -5.2%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 542,391 → 568,046 +4.7% 374,932 → 369,399 -1.5%
new Promise(resolve => resolve(value)) 193,092 → 198,537 +2.8% 169,543 → 151,236 🔴 -10.8%
Promise.reject(reason) 536,046 → 571,624 +6.6% 352,162 → 338,547 -3.9%
resolve + then (1 handler) 170,277 → 168,643 -1.0% 161,075 → 150,979 -6.3%
resolve + then chain (3 deep) 63,979 → 61,740 -3.5% 72,068 → 65,810 🔴 -8.7%
resolve + then chain (10 deep) 20,912 → 19,021 🔴 -9.0% 21,457 → 23,297 🟢 +8.6%
reject + catch + then 95,237 → 92,570 -2.8% 98,005 → 84,831 🔴 -13.4%
resolve + finally + then 82,996 → 82,245 -0.9% 77,781 → 72,839 -6.4%
Promise.all (5 resolved) 31,619 → 30,497 -3.5% 25,775 → 24,155 -6.3%
Promise.race (5 resolved) 33,740 → 32,519 -3.6% 28,982 → 27,067 -6.6%
Promise.allSettled (5 mixed) 26,941 → 26,406 -2.0% 22,323 → 21,519 -3.6%
Promise.any (5 mixed) 32,260 → 30,150 -6.5% 26,550 → 25,614 -3.5%
strings.js — Interp: 🟢 3, 🔴 1, 7 unch. · avg +2.7% · Bytecode: 🟢 4, 🔴 3, 4 unch. · avg -1.6%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 412,491 → 388,539 -5.8% 432,927 → 369,797 🔴 -14.6%
template literal 701,207 → 594,674 🔴 -15.2% 710,564 → 692,997 -2.5%
string repeat 386,916 → 421,495 🟢 +8.9% 1,073,171 → 1,067,603 -0.5%
split and join 130,588 → 132,431 +1.4% 332,060 → 324,855 -2.2%
indexOf and includes 158,883 → 173,588 🟢 +9.3% 679,573 → 738,123 🟢 +8.6%
toUpperCase and toLowerCase 238,073 → 252,860 +6.2% 697,971 → 734,665 +5.3%
slice and substring 152,806 → 160,445 +5.0% 750,533 → 841,177 🟢 +12.1%
trim operations 179,109 → 182,931 +2.1% 858,043 → 658,172 🔴 -23.3%
replace and replaceAll 198,884 → 207,480 +4.3% 735,738 → 620,994 🔴 -15.6%
startsWith and endsWith 128,988 → 140,162 🟢 +8.7% 571,541 → 614,308 🟢 +7.5%
padStart and padEnd 189,025 → 198,306 +4.9% 633,648 → 684,908 🟢 +8.1%
typed-arrays.js — Interp: 🟢 6, 16 unch. · avg +4.6% · Bytecode: 🟢 3, 19 unch. · avg +2.4%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 322,635 → 312,067 -3.3% 129,190 → 130,819 +1.3%
new Int32Array(100) 297,419 → 294,693 -0.9% 123,303 → 126,200 +2.3%
new Int32Array(1000) 170,110 → 196,174 🟢 +15.3% 62,356 → 68,743 🟢 +10.2%
new Float64Array(100) 268,456 → 274,221 +2.1% 109,364 → 102,487 -6.3%
Int32Array.from([...]) 181,854 → 190,448 +4.7% 159,264 → 154,572 -2.9%
Int32Array.of(1, 2, 3, 4, 5) 317,333 → 318,422 +0.3% 247,535 → 255,899 +3.4%
sequential write 100 elements 3,592 → 3,572 -0.6% 13,984 → 14,337 +2.5%
sequential read 100 elements 3,700 → 3,623 -2.1% 10,688 → 11,373 +6.4%
Float64Array write 100 elements 3,336 → 3,410 +2.2% 13,237 → 13,837 +4.5%
fill(42) 47,545 → 54,427 🟢 +14.5% 46,357 → 43,353 -6.5%
slice() 207,348 → 219,164 +5.7% 190,715 → 186,047 -2.4%
map(x => x * 2) 7,817 → 8,405 🟢 +7.5% 8,101 → 8,458 +4.4%
filter(x => x > 50) 8,108 → 8,465 +4.4% 8,341 → 8,797 +5.5%
reduce (sum) 7,741 → 8,071 +4.3% 7,239 → 7,629 +5.4%
sort() 173,927 → 201,859 🟢 +16.1% 152,200 → 150,610 -1.0%
indexOf() 437,403 → 498,341 🟢 +13.9% 376,510 → 365,880 -2.8%
reverse() 330,780 → 371,013 🟢 +12.2% 279,421 → 276,111 -1.2%
create view over existing buffer 391,926 → 394,395 +0.6% 136,291 → 138,080 +1.3%
subarray() 448,853 → 463,553 +3.3% 369,622 → 359,172 -2.8%
set() from array 581,486 → 617,030 +6.1% 246,892 → 266,486 🟢 +7.9%
for-of loop 5,146 → 5,075 -1.4% 17,546 → 21,115 🟢 +20.3%
spread into array 18,576 → 18,018 -3.0% 55,459 → 56,800 +2.4%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

Covers ===, !==, <, > across empty, short (1-5 char), medium (6-13 char),
long (14+ char) and cross-boundary strings. Includes unicode tests for
BMP characters, emoji, CJK, mixed ASCII+unicode, and escape sequences.
Tests at the 13-char inline threshold boundary validate correctness when
strings transition from inline to heap-allocated representation.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@souffle/Souffle.Value.pas`:
- Line 11: SOUFFLE_INLINE_STRING_MAX is set to 13 but the experiment is scoped
to 15; update the constant SOUFFLE_INLINE_STRING_MAX to 15 so string
inlining/spill behavior matches the experimental configuration, ensuring
benchmarks use the intended 15-char threshold.

In `@tests/language/expressions/comparison/string-comparison.js`:
- Line 8: Replace self-comparisons like expect("" === "").toBe(true) with
comparisons between two distinct bindings holding the same value: declare two
separate constants (e.g., const left = ""; const right = ""; ) and update the
assertion to use those bindings (e.g., expect(left === right).toBe(true) or
expect(left).toBe(right)); apply this pattern to all flagged expect(... === ...)
occurrences so each side is a distinct binding rather than the identical
literal/variable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76b81bee-9f5b-4827-8562-c5ec4a86e5ce

📥 Commits

Reviewing files that changed from the base of the PR and between cd0ce40 and e5013a4.

📒 Files selected for processing (3)
  • souffle/Souffle.Value.pas
  • tests/language/expressions/comparison/string-comparison.js
  • units/Goccia.Runtime.Operations.pas

Comment thread souffle/Souffle.Value.pas
Comment thread tests/language/expressions/comparison/string-comparison.js Outdated
GreaterThanOrEqual and LessThanOrEqual coerced both operands to numbers
before checking for string-string comparison. Non-numeric strings like
"abc" became NaN, triggering an early false return. Add string-string
early exit paths matching the pattern already used in GreaterThan and
LessThan. Restore <= and >= assertions in string comparison tests.

Made-with: Cursor
…ions

Rename describe/test labels that referenced inline strings, heap
allocation, and character limit boundaries. Tests are unchanged.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
tests/language/expressions/comparison/string-comparison.js (1)

8-9: ⚠️ Potential issue | 🟠 Major

Resolve Biome noSelfCompare errors by avoiding literal/identical self-comparisons.

These assertions compare the same operand to itself, which trips lint/suspicious/noSelfCompare and can block CI. Rewrite them to compare distinct bindings containing equal values.

Suggested patch pattern
 test("empty string comparisons", () => {
-  expect("" === "").toBe(true);
-  expect("" !== "").toBe(false);
+  const emptyA = "";
+  const emptyB = `${""}`;
+  expect(emptyA === emptyB).toBe(true);
+  expect(emptyA !== emptyB).toBe(false);
   expect("" === "a").toBe(false);
   expect("" !== "a").toBe(true);
   expect("a" === "").toBe(false);
 });

 test("same content same length", () => {
-  expect("abc" === "abc").toBe(true);
+  const abcA = "abc";
+  const abcB = `ab${"c"}`;
+  expect(abcA === abcB).toBe(true);
-  expect("abc" !== "abc").toBe(false);
+  expect(abcA !== abcB).toBe(false);
 });

Also applies to: 16-18, 43-44, 60-61, 77-77, 85-85, 87-87, 158-158, 187-187, 193-193, 204-204, 210-210, 216-216

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/language/expressions/comparison/string-comparison.js` around lines 8 -
9, The failing tests use literal self-comparisons like expect("" ===
"").toBe(true) which trigger the noSelfCompare linter; instead create distinct
bindings and compare them (e.g. const a = ""; const b = ""; then use expect(a
=== b).toBe(true) or expect(a !== b).toBe(false)) so the same logical assertion
is preserved but the operands are different identifiers; update all similar
occurrences (uses of expect(... === ...) and expect(... !== ...) in this file)
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/language/expressions/comparison/string-comparison.js`:
- Around line 8-9: The failing tests use literal self-comparisons like expect(""
=== "").toBe(true) which trigger the noSelfCompare linter; instead create
distinct bindings and compare them (e.g. const a = ""; const b = ""; then use
expect(a === b).toBe(true) or expect(a !== b).toBe(false)) so the same logical
assertion is preserved but the operands are different identifiers; update all
similar occurrences (uses of expect(... === ...) and expect(... !== ...) in this
file) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a30f3bc4-6818-42aa-9181-e725a047c634

📥 Commits

Reviewing files that changed from the base of the PR and between e5013a4 and 8e71ebc.

📒 Files selected for processing (2)
  • tests/language/expressions/comparison/string-comparison.js
  • units/Goccia.Evaluator.Comparison.pas

@frostney frostney changed the title Experiment: SOUFFLE_INLINE_STRING_MAX = 15 Experiment: SOUFFLE_INLINE_STRING_MAX = 13 Mar 16, 2026
Replace identical literal operands on both sides of === / !== / <= / >=
with distinct const bindings to satisfy Biome's noSelfCompare rule.
Same logical assertions are preserved.

Made-with: Cursor
@frostney frostney merged commit f0257b1 into main Mar 16, 2026
9 checks passed
@frostney frostney deleted the exp/inline-string-15 branch March 16, 2026 17:13
@frostney frostney added the performance Performance improvement label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant