Skip to content

Add record/delegate fast path for OP_RT_GET_PROP in VM#83

Merged
frostney merged 3 commits intomainfrom
opt/rt-get-prop-fastpath
Mar 15, 2026
Merged

Add record/delegate fast path for OP_RT_GET_PROP in VM#83
frostney merged 3 commits intomainfrom
opt/rt-get-prop-fastpath

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Mar 12, 2026

Summary

  • Adds record/delegate fast path for `OP_RT_GET_PROP` in `ExecuteRuntimeOp`.
  • When the receiver is a `TSouffleRecord`, the property is looked up directly on the record and its delegate chain before falling through to `FRuntimeOps.GetProperty`.
  • Most dynamic property accesses hit records, so this avoids the virtual call + full runtime property resolution for the common case.

Test plan

  • All JS tests pass in interpreter mode
  • All JS tests pass in bytecode mode

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Improved property access resolution: record-backed properties now try direct lookup, then delegation, before falling back to the generic resolver — reducing missed or incorrect property reads in complex object chains.

Dynamic property access via OP_RT_GET_PROP now checks if the receiver
is a TSouffleRecord before calling FRuntimeOps.GetProperty. When it is,
the record is queried directly, then the delegate chain is walked. This
avoids the virtual method call + full runtime GetProperty (type switching,
bridge caching) for the common case of accessing properties on objects.

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

coderabbitai Bot commented Mar 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 743b0ff4-02d7-4966-93a9-d0e0f2f44c85

📥 Commits

Reviewing files that changed from the base of the PR and between 29d7823 and fea3d1e.

📒 Files selected for processing (1)
  • souffle/Souffle.VM.pas
🚧 Files skipped from review as they are similar to previous changes (1)
  • souffle/Souffle.VM.pas

📝 Walkthrough

Walkthrough

The VM's property-get opcode (OP_RT_GET_PROP) now tries a TSouffleRecord fast-path: direct Get, then DelegateGet, and only if both fail falls back to FRuntimeOps.GetProperty; non-record bases use the original runtime GetProperty path. (≤50 words)

Changes

Cohort / File(s) Summary
VM Property Resolution
souffle/Souffle.VM.pas
Updated OP_RT_GET_PROP handling: added local PropKey/PropVal; if base is a TSouffleRecord reference attempt Get(PropKey) → on miss try DelegateGet(PropKey) → on miss call FRuntimeOps.GetProperty(PropKey). Non-record bases use FRuntimeOps.GetProperty with constant key.

Sequence Diagram(s)

sequenceDiagram
    participant VM as VM (ExecuteRuntimeOp)
    participant Record as TSouffleRecord
    participant Delegate as Delegate (owner/parent)
    participant Runtime as FRuntimeOps

    VM->>Record: If base is record -> Get(PropKey)
    alt Get returns value
        Record-->>VM: PropVal (assign)
    else Get fails
        Record->>Delegate: DelegateGet(PropKey)
        alt Delegate returns value
            Delegate-->>VM: PropVal (assign)
        else Delegate fails
            VM->>Runtime: GetProperty(base, PropKey)
            Runtime-->>VM: PropVal (assign or error)
        end
    end
    alt base not a record
        VM->>Runtime: GetProperty(base, PropKey)
        Runtime-->>VM: PropVal (assign or error)
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐇 I hop through fields with keen delight,
I check the record, then ask its sight,
If neighbors know where secrets hide,
I borrow answers, swift and wide—
A rabbit's leap finds values right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a fast path optimization for OP_RT_GET_PROP that handles records and delegates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch opt/rt-get-prop-fastpath
📝 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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 3463 3463
Tests Passed 3422 ✅ 3463 ✅
Tests Skipped 41 0
Tests Execution 152.8ms 155.6ms
Tests Engine 311.2ms 642.7ms
Benchmarks Total 254 254
Benchmarks Duration 6.80min 11.00min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

Benchmark Results

254 benchmarks

Interpreted: 🟢 10 improved · 🔴 71 regressed · 173 unchanged · avg -3.8%
Bytecode: 🟢 7 improved · 🔴 3 regressed · 244 unchanged · avg +1.1%

arraybuffer.js — Interp: 🔴 10, 4 unch. · avg -9.7% · Bytecode: 14 unch. · avg +2.3%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 492,095 → 435,351 🔴 -11.5% 143,762 → 144,356 +0.4%
create ArrayBuffer(64) 484,754 → 426,173 🔴 -12.1% 140,282 → 143,206 +2.1%
create ArrayBuffer(1024) 383,187 → 324,243 🔴 -15.4% 128,217 → 131,444 +2.5%
create ArrayBuffer(8192) 172,803 → 139,487 🔴 -19.3% 80,804 → 80,495 -0.4%
slice full buffer (64 bytes) 576,117 → 526,468 🔴 -8.6% 387,252 → 398,243 +2.8%
slice half buffer (512 of 1024 bytes) 496,403 → 453,432 🔴 -8.7% 336,067 → 349,600 +4.0%
slice with negative indices 501,178 → 458,749 🔴 -8.5% 364,380 → 379,982 +4.3%
slice empty range 564,068 → 524,733 -7.0% 378,338 → 394,582 +4.3%
byteLength access 1,662,153 → 1,584,832 -4.7% 1,202,102 → 1,211,883 +0.8%
Symbol.toStringTag access 1,139,123 → 1,181,262 +3.7% 591,322 → 597,278 +1.0%
ArrayBuffer.isView 809,791 → 773,519 -4.5% 486,692 → 513,016 +5.4%
clone ArrayBuffer(64) 438,815 → 362,905 🔴 -17.3% 327,955 → 330,043 +0.6%
clone ArrayBuffer(1024) 353,812 → 308,248 🔴 -12.9% 252,601 → 255,990 +1.3%
clone ArrayBuffer inside object 299,371 → 271,702 🔴 -9.2% 153,200 → 158,083 +3.2%
arrays.js — Interp: 🔴 8, 11 unch. · avg -6.5% · Bytecode: 🟢 1, 18 unch. · avg +1.4%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 14,896 → 13,965 -6.3% 13,723 → 13,767 +0.3%
Array.from 10 elements 251,673 → 231,444 🔴 -8.0% 160,582 → 165,568 +3.1%
Array.of 10 elements 343,275 → 311,988 🔴 -9.1% 225,478 → 230,390 +2.2%
spread into new array 362,475 → 338,674 -6.6% 683,353 → 692,321 +1.3%
map over 50 elements 30,661 → 28,965 -5.5% 24,025 → 23,852 -0.7%
filter over 50 elements 25,882 → 23,995 🔴 -7.3% 22,606 → 22,946 +1.5%
reduce sum 50 elements 29,268 → 27,743 -5.2% 20,575 → 19,923 -3.2%
forEach over 50 elements 24,438 → 23,281 -4.7% 25,494 → 25,617 +0.5%
find in 50 elements 37,986 → 35,816 -5.7% 30,164 → 30,209 +0.2%
sort 20 elements 13,065 → 12,396 -5.1% 11,949 → 12,028 +0.7%
flat nested array 125,572 → 120,704 -3.9% 361,463 → 391,202 🟢 +8.2%
flatMap 81,912 → 74,426 🔴 -9.1% 246,632 → 252,877 +2.5%
map inside map (5x5) 23,708 → 21,955 🔴 -7.4% 74,949 → 74,915 -0.0%
filter inside map (5x10) 17,491 → 16,143 🔴 -7.7% 14,246 → 14,485 +1.7%
reduce inside map (5x10) 21,125 → 19,662 -6.9% 14,520 → 14,718 +1.4%
forEach inside forEach (5x10) 17,535 → 16,967 -3.2% 16,369 → 16,306 -0.4%
find inside some (10x10) 15,081 → 13,996 🔴 -7.2% 11,208 → 11,511 +2.7%
map+filter chain nested (5x20) 5,895 → 5,511 -6.5% 4,743 → 4,822 +1.7%
reduce flatten (10x5) 42,981 → 39,242 🔴 -8.7% 6,030 → 6,198 +2.8%
async-await.js — Interp: 🔴 5, 1 unch. · avg -10.5% · Bytecode: 6 unch. · avg +2.3%
Benchmark Interpreted Δ Bytecode Δ
single await 422,158 → 376,590 🔴 -10.8% 281,334 → 288,843 +2.7%
multiple awaits 188,860 → 168,929 🔴 -10.6% 118,125 → 123,450 +4.5%
await non-Promise value 969,161 → 823,831 🔴 -15.0% 1,021,161 → 1,052,886 +3.1%
await with try/catch 409,820 → 364,457 🔴 -11.1% 277,650 → 280,286 +0.9%
await Promise.all 56,353 → 53,099 -5.8% 45,076 → 45,123 +0.1%
nested async function call 206,255 → 186,449 🔴 -9.6% 216,118 → 222,084 +2.8%
classes.js — Interp: 🔴 28, 3 unch. · avg -9.6% · Bytecode: 31 unch. · avg +0.3%
Benchmark Interpreted Δ Bytecode Δ
simple class new 131,235 → 115,794 🔴 -11.8% 362,637 → 369,962 +2.0%
class with defaults 105,936 → 93,494 🔴 -11.7% 252,253 → 255,632 +1.3%
50 instances via Array.from 6,241 → 5,627 🔴 -9.8% 6,476 → 6,536 +0.9%
instance method call 67,032 → 59,911 🔴 -10.6% 166,091 → 165,263 -0.5%
static method call 103,217 → 92,366 🔴 -10.5% 372,954 → 367,025 -1.6%
single-level inheritance 52,322 → 46,978 🔴 -10.2% 164,815 → 163,886 -0.6%
two-level inheritance 44,569 → 39,102 🔴 -12.3% 133,963 → 134,032 +0.1%
private field access 65,355 → 58,581 🔴 -10.4% 177,870 → 178,067 +0.1%
private methods 71,476 → 64,038 🔴 -10.4% 223,234 → 225,172 +0.9%
getter/setter access 73,748 → 66,257 🔴 -10.2% 175,774 → 173,011 -1.6%
class decorator (identity) 92,228 → 82,972 🔴 -10.0% 54,397 → 54,831 +0.8%
class decorator (wrapping) 53,042 → 47,507 🔴 -10.4% 38,119 → 38,764 +1.7%
identity method decorator 65,671 → 58,533 🔴 -10.9% 45,712 → 46,016 +0.7%
wrapping method decorator 53,385 → 48,580 🔴 -9.0% 39,044 → 39,243 +0.5%
stacked method decorators (x3) 37,639 → 34,466 🔴 -8.4% 28,749 → 28,538 -0.7%
identity field decorator 73,566 → 66,638 🔴 -9.4% 47,227 → 47,434 +0.4%
field initializer decorator 62,354 → 56,870 🔴 -8.8% 42,095 → 42,458 +0.9%
getter decorator (identity) 63,046 → 56,934 🔴 -9.7% 42,305 → 41,752 -1.3%
setter decorator (identity) 53,283 → 48,212 🔴 -9.5% 36,420 → 36,043 -1.0%
static method decorator 68,990 → 62,432 🔴 -9.5% 64,687 → 65,688 +1.5%
static field decorator 80,365 → 72,199 🔴 -10.2% 67,864 → 68,598 +1.1%
private method decorator 52,871 → 48,169 🔴 -8.9% 38,544 → 38,938 +1.0%
private field decorator 59,316 → 52,641 🔴 -11.3% 40,210 → 40,041 -0.4%
plain auto-accessor (no decorator) 100,578 → 90,590 🔴 -9.9% 55,332 → 55,594 +0.5%
auto-accessor with decorator 58,526 → 52,953 🔴 -9.5% 38,223 → 38,455 +0.6%
decorator writing metadata 46,578 → 43,529 -6.5% 43,789 → 44,048 +0.6%
static getter read 114,088 → 104,771 🔴 -8.2% 407,832 → 411,051 +0.8%
static getter/setter pair 84,296 → 79,711 -5.4% 213,805 → 215,527 +0.8%
inherited static getter 66,781 → 60,017 🔴 -10.1% 277,504 → 274,705 -1.0%
inherited static setter 70,670 → 64,724 🔴 -8.4% 214,256 → 213,601 -0.3%
inherited static getter with this binding 58,169 → 54,287 -6.7% 161,630 → 162,653 +0.6%
closures.js — Interp: 🔴 1, 10 unch. · avg -5.0% · Bytecode: 11 unch. · avg +0.1%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 136,921 → 128,985 -5.8% 657,175 → 651,873 -0.8%
closure over multiple variables 125,054 → 120,913 -3.3% 398,668 → 403,216 +1.1%
nested closures 132,187 → 127,291 -3.7% 599,708 → 613,650 +2.3%
function as argument 101,742 → 96,629 -5.0% 552,879 → 558,131 +0.9%
function returning function 126,142 → 119,342 -5.4% 635,498 → 633,074 -0.4%
compose two functions 75,577 → 72,791 -3.7% 380,889 → 385,976 +1.3%
fn.call 158,401 → 152,068 -4.0% 142,136 → 143,212 +0.8%
fn.apply 122,126 → 112,584 🔴 -7.8% 99,050 → 97,478 -1.6%
fn.bind 145,195 → 137,356 -5.4% 150,054 → 148,438 -1.1%
recursive sum to 50 12,461 → 11,984 -3.8% 40,195 → 40,237 +0.1%
recursive tree traversal 20,725 → 19,340 -6.7% 64,153 → 63,122 -1.6%
collections.js — Interp: 🔴 1, 11 unch. · avg -4.4% · Bytecode: 12 unch. · avg -0.0%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 7,546 → 7,210 -4.5% 5,833 → 5,873 +0.7%
has lookup (50 elements) 95,626 → 91,722 -4.1% 88,429 → 89,050 +0.7%
delete elements 50,385 → 48,596 -3.6% 37,136 → 37,312 +0.5%
forEach iteration 16,307 → 16,045 -1.6% 17,012 → 16,978 -0.2%
spread to array 35,313 → 31,596 🔴 -10.5% 151,615 → 152,576 +0.6%
deduplicate array 44,199 → 42,211 -4.5% 48,059 → 47,927 -0.3%
set 50 entries 5,532 → 5,212 -5.8% 5,912 → 5,908 -0.1%
get lookup (50 entries) 87,572 → 86,284 -1.5% 97,992 → 96,989 -1.0%
has check 133,930 → 130,551 -2.5% 154,798 → 155,170 +0.2%
delete entries 46,213 → 45,305 -2.0% 35,380 → 35,668 +0.8%
forEach iteration 17,213 → 16,018 -6.9% 16,780 → 16,972 +1.1%
keys/values/entries 9,144 → 8,619 -5.7% 23,183 → 22,404 -3.4%
destructuring.js — Interp: 🔴 13, 9 unch. · avg -7.3% · Bytecode: 🟢 2, 20 unch. · avg +1.9%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 446,487 → 409,958 🔴 -8.2% 857,899 → 859,938 +0.2%
with rest element 292,260 → 295,525 +1.1% 584,356 → 594,117 +1.7%
with defaults 449,456 → 418,897 -6.8% 852,064 → 836,241 -1.9%
skip elements 463,134 → 441,446 -4.7% 976,784 → 978,512 +0.2%
nested array destructuring 184,890 → 182,655 -1.2% 440,910 → 447,455 +1.5%
swap variables 540,145 → 514,015 -4.8% 1,196,824 → 1,180,549 -1.4%
simple object destructuring 330,198 → 295,317 🔴 -10.6% 540,408 → 564,480 +4.5%
with defaults 414,566 → 357,751 🔴 -13.7% 327,171 → 316,446 -3.3%
with renaming 351,818 → 318,847 🔴 -9.4% 625,540 → 657,628 +5.1%
nested object destructuring 162,482 → 149,024 🔴 -8.3% 259,676 → 264,521 +1.9%
rest properties 205,511 → 187,522 🔴 -8.8% 231,129 → 238,365 +3.1%
object parameter 103,540 → 92,008 🔴 -11.1% 182,758 → 196,525 🟢 +7.5%
array parameter 136,681 → 125,963 🔴 -7.8% 352,831 → 362,728 +2.8%
mixed destructuring in map 39,583 → 36,558 🔴 -7.6% 34,952 → 37,678 🟢 +7.8%
forEach with array destructuring 71,159 → 67,636 -5.0% 156,944 → 161,160 +2.7%
map with array destructuring 74,170 → 69,844 -5.8% 186,731 → 191,324 +2.5%
filter with array destructuring 76,806 → 72,165 -6.0% 230,868 → 233,247 +1.0%
reduce with array destructuring 82,822 → 75,728 🔴 -8.6% 207,438 → 210,862 +1.7%
map with object destructuring 86,402 → 79,291 🔴 -8.2% 76,951 → 79,640 +3.5%
map with nested destructuring 70,694 → 65,632 🔴 -7.2% 63,858 → 65,267 +2.2%
map with rest in destructuring 42,256 → 39,752 -5.9% 23,406 → 23,417 +0.0%
map with defaults in destructuring 66,872 → 59,416 🔴 -11.1% 38,830 → 38,617 -0.5%
fibonacci.js — Interp: 8 unch. · avg -2.9% · Bytecode: 8 unch. · avg -1.2%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 346 → 330 -4.5% 1,128 → 1,131 +0.2%
recursive fib(20) 31 → 30 -2.8% 103 → 102 -1.4%
recursive fib(15) typed 334 → 335 +0.3% 1,498 → 1,468 -2.0%
recursive fib(20) typed 31 → 30 -4.0% 136 → 133 -2.1%
iterative fib(20) via reduce 12,878 → 12,318 -4.4% 8,892 → 8,651 -2.7%
iterator fib(20) 9,839 → 9,513 -3.3% 14,981 → 14,741 -1.6%
iterator fib(20) via Iterator.from + take 15,299 → 14,717 -3.8% 16,592 → 16,760 +1.0%
iterator fib(20) last value via reduce 11,534 → 11,495 -0.3% 12,791 → 12,708 -0.6%
for-of.js — Interp: 7 unch. · avg -0.9% · Bytecode: 7 unch. · avg +0.6%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 48,998 → 48,160 -1.7% 151,903 → 152,978 +0.7%
for...of with 100-element array 5,599 → 5,522 -1.4% 21,625 → 21,427 -0.9%
for...of with string (10 chars) 35,842 → 35,289 -1.5% 118,969 → 119,964 +0.8%
for...of with Set (10 elements) 49,442 → 49,002 -0.9% 159,569 → 159,285 -0.2%
for...of with Map entries (10 entries) 30,927 → 31,309 +1.2% 47,611 → 48,976 +2.9%
for...of with destructuring 42,121 → 41,725 -0.9% 71,700 → 73,071 +1.9%
for-await-of with sync array 46,490 → 46,094 -0.9% 130,360 → 128,640 -1.3%
iterators.js — Interp: 20 unch. · avg -0.4% · Bytecode: 🔴 1, 19 unch. · avg +0.6%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 15,399 → 14,561 -5.4% 17,300 → 16,860 -2.5%
Iterator.from({next}).toArray() — 50 elements 6,688 → 6,387 -4.5% 7,662 → 7,630 -0.4%
spread pre-wrapped iterator — 20 elements 11,838 → 11,402 -3.7% 16,355 → 16,179 -1.1%
Iterator.from({next}).forEach — 50 elements 4,647 → 4,649 +0.1% 5,386 → 5,527 +2.6%
Iterator.from({next}).reduce — 50 elements 4,709 → 4,647 -1.3% 5,134 → 5,202 +1.3%
wrap array iterator 187,070 → 177,812 -4.9% 112,725 → 111,022 -1.5%
wrap plain {next()} object 10,562 → 10,464 -0.9% 13,100 → 12,955 -1.1%
map + toArray (50 elements) 4,756 → 4,810 +1.1% 5,666 → 5,933 +4.7%
filter + toArray (50 elements) 4,590 → 4,632 +0.9% 5,423 → 5,760 +6.2%
take(10) + toArray (50 element source) 27,318 → 26,936 -1.4% 29,051 → 29,875 +2.8%
drop(40) + toArray (50 element source) 6,583 → 6,479 -1.6% 8,110 → 8,621 +6.3%
chained map + filter + take (100 element source) 8,533 → 8,549 +0.2% 9,873 → 10,240 +3.7%
some + every (50 elements) 2,651 → 2,681 +1.2% 3,323 → 3,388 +2.0%
find (50 elements) 5,751 → 5,883 +2.3% 7,331 → 7,403 +1.0%
array.values().map().filter().toArray() 9,045 → 9,427 +4.2% 10,247 → 10,107 -1.4%
array.values().take(5).toArray() 220,441 → 216,828 -1.6% 158,739 → 146,106 🔴 -8.0%
array.values().drop(45).toArray() 207,549 → 202,282 -2.5% 145,806 → 147,235 +1.0%
map.entries() chained helpers 11,142 → 11,398 +2.3% 5,489 → 5,473 -0.3%
set.values() chained helpers 18,918 → 19,266 +1.8% 21,622 → 21,147 -2.2%
string iterator map + toArray 13,625 → 14,496 +6.4% 22,437 → 21,974 -2.1%
json.js — Interp: 20 unch. · avg -3.2% · Bytecode: 🟢 1, 19 unch. · avg +1.8%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 177,679 → 170,462 -4.1% 145,370 → 137,384 -5.5%
parse nested object 110,241 → 108,341 -1.7% 90,988 → 90,135 -0.9%
parse array of objects 58,870 → 56,544 -4.0% 51,028 → 49,920 -2.2%
parse large flat object 50,592 → 49,542 -2.1% 45,953 → 46,015 +0.1%
parse mixed types 74,679 → 71,822 -3.8% 62,390 → 64,611 +3.6%
stringify simple object 206,353 → 195,245 -5.4% 152,952 → 158,988 +3.9%
stringify nested object 112,786 → 112,358 -0.4% 83,777 → 86,463 +3.2%
stringify array of objects 61,107 → 63,500 +3.9% 49,476 → 52,951 🟢 +7.0%
stringify mixed types 87,687 → 87,566 -0.1% 69,177 → 71,915 +4.0%
reviver doubles numbers 47,958 → 45,221 -5.7% 40,536 → 42,749 +5.5%
reviver filters properties 41,905 → 39,138 -6.6% 46,241 → 47,075 +1.8%
reviver on nested object 53,867 → 51,267 -4.8% 52,887 → 53,731 +1.6%
reviver on array 30,594 → 29,664 -3.0% 29,165 → 30,163 +3.4%
replacer function doubles numbers 50,987 → 48,617 -4.6% 52,143 → 52,983 +1.6%
replacer function excludes properties 63,150 → 61,202 -3.1% 62,683 → 63,026 +0.5%
array replacer (allowlist) 116,907 → 115,032 -1.6% 98,501 → 99,913 +1.4%
stringify with 2-space indent 102,055 → 96,979 -5.0% 78,964 → 80,700 +2.2%
stringify with tab indent 103,396 → 97,431 -5.8% 79,607 → 80,721 +1.4%
parse then stringify 56,598 → 54,608 -3.5% 51,102 → 52,261 +2.3%
stringify then parse 33,788 → 32,868 -2.7% 30,921 → 31,315 +1.3%
jsx.jsx — Interp: 🔴 1, 20 unch. · avg -4.2% · Bytecode: 21 unch. · avg +0.9%
Benchmark Interpreted Δ Bytecode Δ
simple element 220,775 → 206,071 -6.7% 641,003 → 652,304 +1.8%
self-closing element 220,208 → 211,596 -3.9% 676,498 → 685,671 +1.4%
element with string attribute 182,283 → 173,058 -5.1% 466,593 → 473,841 +1.6%
element with multiple attributes 153,235 → 147,813 -3.5% 405,224 → 407,621 +0.6%
element with expression attribute 175,137 → 161,817 🔴 -7.6% 465,319 → 458,293 -1.5%
text child 213,156 → 204,327 -4.1% 652,676 → 657,820 +0.8%
expression child 209,310 → 198,707 -5.1% 637,288 → 647,055 +1.5%
mixed text and expression 193,808 → 195,131 +0.7% 578,274 → 586,608 +1.4%
nested elements (3 levels) 80,476 → 77,859 -3.3% 256,877 → 257,052 +0.1%
sibling children 60,963 → 57,504 -5.7% 191,435 → 191,309 -0.1%
component element 153,252 → 144,861 -5.5% 449,838 → 453,924 +0.9%
component with children 95,518 → 90,588 -5.2% 288,466 → 290,997 +0.9%
dotted component 129,421 → 122,300 -5.5% 331,387 → 338,243 +2.1%
empty fragment 222,277 → 212,719 -4.3% 694,151 → 689,560 -0.7%
fragment with children 59,474 → 57,412 -3.5% 189,235 → 190,711 +0.8%
spread attributes 113,554 → 108,437 -4.5% 108,146 → 111,895 +3.5%
spread with overrides 99,144 → 94,536 -4.6% 80,912 → 83,217 +2.8%
shorthand props 163,041 → 155,509 -4.6% 433,200 → 440,955 +1.8%
nav bar structure 27,754 → 27,326 -1.5% 81,390 → 81,998 +0.7%
card component tree 32,598 → 31,590 -3.1% 89,791 → 88,654 -1.3%
10 list items via Array.from 15,036 → 14,859 -1.2% 24,307 → 24,449 +0.6%
numbers.js — Interp: 🔴 1, 10 unch. · avg -3.3% · Bytecode: 11 unch. · avg +2.1%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 556,156 → 548,742 -1.3% 1,682,106 → 1,734,404 +3.1%
floating point arithmetic 577,798 → 603,746 +4.5% 1,878,489 → 1,907,269 +1.5%
number coercion 192,696 → 189,093 -1.9% 135,889 → 139,217 +2.4%
toFixed 108,923 → 103,390 -5.1% 219,686 → 223,647 +1.8%
toString 162,747 → 159,469 -2.0% 768,063 → 777,885 +1.3%
valueOf 240,331 → 227,507 -5.3% 1,113,307 → 1,132,001 +1.7%
toPrecision 145,637 → 146,976 +0.9% 432,897 → 439,481 +1.5%
Number.isNaN 324,631 → 300,639 🔴 -7.4% 179,758 → 182,602 +1.6%
Number.isFinite 310,003 → 294,126 -5.1% 174,893 → 176,230 +0.8%
Number.isInteger 311,094 → 289,726 -6.9% 189,612 → 193,931 +2.3%
Number.parseInt and parseFloat 255,878 → 238,230 -6.9% 154,053 → 161,214 +4.6%
objects.js — Interp: 🟢 1, 6 unch. · avg +1.9% · Bytecode: 7 unch. · avg +1.0%
Benchmark Interpreted Δ Bytecode Δ
create simple object 458,224 → 455,556 -0.6% 877,829 → 875,175 -0.3%
create nested object 215,881 → 216,039 +0.1% 386,733 → 397,642 +2.8%
create 50 objects via Array.from 9,405 → 9,168 -2.5% 8,742 → 8,785 +0.5%
property read 595,301 → 618,734 +3.9% 760,264 → 766,326 +0.8%
Object.keys 285,836 → 297,084 +3.9% 204,244 → 207,501 +1.6%
Object.entries 100,962 → 109,389 🟢 +8.3% 63,412 → 63,355 -0.1%
spread operator 185,708 → 186,097 +0.2% 187,965 → 190,933 +1.6%
promises.js — Interp: 🟢 6, 6 unch. · avg +7.7% · Bytecode: 🔴 1, 11 unch. · avg +0.4%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 542,926 → 550,759 +1.4% 342,201 → 351,586 +2.7%
new Promise(resolve => resolve(value)) 193,581 → 198,826 +2.7% 162,269 → 170,613 +5.1%
Promise.reject(reason) 531,509 → 546,118 +2.7% 341,315 → 356,980 +4.6%
resolve + then (1 handler) 163,659 → 174,139 +6.4% 158,572 → 163,930 +3.4%
resolve + then chain (3 deep) 60,453 → 68,186 🟢 +12.8% 70,736 → 71,657 +1.3%
resolve + then chain (10 deep) 20,271 → 22,210 🟢 +9.6% 21,791 → 22,124 +1.5%
reject + catch + then 93,755 → 99,934 +6.6% 90,899 → 93,304 +2.6%
resolve + finally + then 81,707 → 85,814 +5.0% 77,007 → 78,311 +1.7%
Promise.all (5 resolved) 29,963 → 33,370 🟢 +11.4% 26,547 → 26,513 -0.1%
Promise.race (5 resolved) 32,068 → 35,583 🟢 +11.0% 29,024 → 28,040 -3.4%
Promise.allSettled (5 mixed) 25,508 → 27,993 🟢 +9.7% 24,161 → 22,831 -5.5%
Promise.any (5 mixed) 29,436 → 33,330 🟢 +13.2% 28,622 → 25,822 🔴 -9.8%
strings.js — Interp: 🟢 1, 10 unch. · avg +4.5% · Bytecode: 🔴 1, 10 unch. · avg -0.7%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 349,879 → 419,143 🟢 +19.8% 436,360 → 434,372 -0.5%
template literal 635,493 → 643,967 +1.3% 701,546 → 663,104 -5.5%
string repeat 415,382 → 414,143 -0.3% 1,111,534 → 1,068,933 -3.8%
split and join 131,998 → 137,035 +3.8% 328,456 → 330,173 +0.5%
indexOf and includes 170,417 → 171,847 +0.8% 676,560 → 685,238 +1.3%
toUpperCase and toLowerCase 248,071 → 257,166 +3.7% 745,809 → 688,993 🔴 -7.6%
slice and substring 155,056 → 164,410 +6.0% 750,107 → 761,262 +1.5%
trim operations 183,963 → 191,822 +4.3% 857,533 → 863,895 +0.7%
replace and replaceAll 200,776 → 213,577 +6.4% 731,170 → 734,882 +0.5%
startsWith and endsWith 139,376 → 140,143 +0.6% 569,851 → 587,555 +3.1%
padStart and padEnd 197,849 → 203,673 +2.9% 617,887 → 627,593 +1.6%
typed-arrays.js — Interp: 🟢 2, 🔴 3, 17 unch. · avg -0.6% · Bytecode: 🟢 3, 19 unch. · avg +2.9%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 311,367 → 322,440 +3.6% 125,858 → 127,724 +1.5%
new Int32Array(100) 290,435 → 296,492 +2.1% 121,025 → 122,399 +1.1%
new Int32Array(1000) 193,011 → 170,754 🔴 -11.5% 66,080 → 65,727 -0.5%
new Float64Array(100) 266,353 → 267,589 +0.5% 107,796 → 109,748 +1.8%
Int32Array.from([...]) 189,518 → 183,579 -3.1% 158,303 → 160,557 +1.4%
Int32Array.of(1, 2, 3, 4, 5) 300,735 → 318,979 +6.1% 237,366 → 241,027 +1.5%
sequential write 100 elements 3,470 → 3,660 +5.5% 12,647 → 13,752 🟢 +8.7%
sequential read 100 elements 3,591 → 3,785 +5.4% 10,133 → 10,594 +4.5%
Float64Array write 100 elements 3,384 → 3,363 -0.6% 12,403 → 13,288 🟢 +7.1%
fill(42) 54,442 → 47,850 🔴 -12.1% 46,082 → 46,272 +0.4%
slice() 216,285 → 205,151 -5.1% 186,587 → 188,175 +0.9%
map(x => x * 2) 8,231 → 8,337 +1.3% 7,723 → 7,699 -0.3%
filter(x => x > 50) 8,166 → 8,517 +4.3% 8,140 → 8,162 +0.3%
reduce (sum) 7,766 → 8,057 +3.7% 6,881 → 7,052 +2.5%
sort() 197,987 → 170,107 🔴 -14.1% 156,526 → 157,147 +0.4%
indexOf() 488,404 → 454,773 -6.9% 359,305 → 373,194 +3.9%
reverse() 360,919 → 339,236 -6.0% 276,774 → 278,970 +0.8%
create view over existing buffer 406,268 → 401,764 -1.1% 134,735 → 136,453 +1.3%
subarray() 443,810 → 457,114 +3.0% 348,849 → 363,822 +4.3%
set() from array 628,149 → 603,968 -3.8% 239,539 → 248,334 +3.7%
for-of loop 4,738 → 5,178 🟢 +9.3% 17,626 → 17,800 +1.0%
spread into array 17,235 → 18,549 🟢 +7.6% 46,524 → 54,601 🟢 +17.4%

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

@frostney
Copy link
Copy Markdown
Owner Author

TODO: Let's re-review this after #81

@frostney frostney merged commit cd0ce40 into main Mar 15, 2026
9 checks passed
@frostney frostney deleted the opt/rt-get-prop-fastpath branch March 15, 2026 22:47
frostney added a commit that referenced this pull request Mar 18, 2026
- Rewrite optimization-log.md with correct PR numbers for all 22 PRs
  since #73 (previous version had multiple wrong PR-to-description
  mappings)
- Add missing PRs: #73, #74, #75, #76, #77, #79, #80, #81, #82, #83,
  #85, #88, #89, plus infrastructure PRs #90, #92, #98
- Fix GC section: Collect-after-each-file is deliberate, not a
  workaround; any GC revisit needs fresh analysis
- Fix minor items: link to actual issues #103 and #104, correct naming
  convention context (shared units should NOT use Goccia.* prefix)
- Create docs/decision-log.md with 1-3 sentence summaries linking to
  detailed documentation
- Link fpc-generics-performance.md and fpc-dispatch-performance.md
  from AGENTS.md, code-style.md, design-decisions.md, and
  optimization-log.md

Made-with: Cursor
frostney added a commit that referenced this pull request Mar 18, 2026
…105)

* Convert spike PDFs to Markdown, update VM docs, add optimization log

- Convert all 4 PDF spike documents (string performance, hashmap
  performance, generics performance, dispatch performance) to Markdown
  for better AI agent parsing
- Fix stale TSouffleValue documentation: 26 bytes → 16 bytes,
  string[23] → string[13] across souffle-vm.md, design-decisions.md
- Update test count to 3,501 across all docs (AGENTS.md, README.md,
  souffle-vm.md, design-decisions.md)
- Add svkString to TSouffleValue kinds list in AGENTS.md
- Add THashMap optimization details to AGENTS.md component table
- Add docs/optimization-log.md with full VM optimization history,
  experiment results, remaining work, and key insights for fresh
  context pickup
- Add hashmap spike update note documenting TScopeMap removal,
  TOrderedStringMap refactoring, and THashMap hash optimization

Made-with: Cursor

* Fix optimization log PR numbers, add decision log, link all spikes

- Rewrite optimization-log.md with correct PR numbers for all 22 PRs
  since #73 (previous version had multiple wrong PR-to-description
  mappings)
- Add missing PRs: #73, #74, #75, #76, #77, #79, #80, #81, #82, #83,
  #85, #88, #89, plus infrastructure PRs #90, #92, #98
- Fix GC section: Collect-after-each-file is deliberate, not a
  workaround; any GC revisit needs fresh analysis
- Fix minor items: link to actual issues #103 and #104, correct naming
  convention context (shared units should NOT use Goccia.* prefix)
- Create docs/decision-log.md with 1-3 sentence summaries linking to
  detailed documentation
- Link fpc-generics-performance.md and fpc-dispatch-performance.md
  from AGENTS.md, code-style.md, design-decisions.md, and
  optimization-log.md

Made-with: Cursor

* Address PR review comments: fix ratios, bytes/chars, stale refs

- Fix generics spike ratio values (arithmetic errors from original PDF)
- Change "13 characters" to "13 bytes" in souffle-vm.md (FPC string[N]
  is byte-counted), add UTF-8 note
- Mark TScopeMap as removed/historical in hashmap spike document
- Fix Key Insight #4 PR reference (was #88 async/await, not boolean)
- Remove stale GOCCIA_EXT_EVAL_CLASS / FPendingClasses / decorator
  bridge references from AGENTS.md, souffle-vm.md, design-decisions.md
  (decorators are now compiled natively via BEGIN/APPLY/FINISH opcodes)
- Fix "44 second" → "44-second" hyphenation in string spike

Made-with: Cursor

* Expand TScopeMap history in hashmap spike with full timeline

The spike originally recommended TScopeMap for scope bindings based on
micro-benchmarks at N≤10. After implementation (PR #66), real-world
profiling showed the assumption about scope sizes was incorrect — global
and bridged scopes far exceed 20 bindings, causing a 2.7× regression.
The update section now tells the complete timeline: original
recommendation → implementation → profiling discovery → revert.

Made-with: Cursor

* Add grill-me interview skill

Introduce a new 'grill-me' agent skill that relentlessly interviews users about a plan or design, exploring each branch of the decision tree until a shared understanding is reached and dependencies are resolved. The skill prefers to inspect the codebase when that can answer a question. Updated skills lock to include the new skill.
frostney added a commit that referenced this pull request Mar 23, 2026
Mirror PR #83's GET fast path: when the receiver is a TSouffleRecord,
call PutChecked() directly instead of bridging to FRuntimeOps.SetProperty().
Non-record types (blueprints, setters, private fields) still fall through
to the bridge for full property resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
frostney added a commit that referenced this pull request Mar 23, 2026
* Add record fast path for OP_RT_SET_PROP in VM dispatch

Mirror PR #83's GET fast path: when the receiver is a TSouffleRecord,
call PutChecked() directly instead of bridging to FRuntimeOps.SetProperty().
Non-record types (blueprints, setters, private fields) still fall through
to the bridge for full property resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix SET_PROP fast path: skip records with blueprints (setters/frozen)

Records with a Blueprint may have setters, non-writable properties, or
frozen/sealed state that PutChecked() does not handle. Restrict the fast
path to plain records (no blueprint) and fall through to the bridge for
class instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert SET_PROP fast path — PutChecked misses accessor/freeze/seal semantics

PutChecked does not handle accessor properties (getters/setters defined
via Object.defineProperty), does not throw on non-writable/frozen/sealed
violations, and does not cover Object.preventExtensions. The blueprint
check alone was insufficient — plain objects modified with defineProperty,
freeze, or seal also break.

OP_RT_SET_PROP is only emitted for destructuring patterns, not a hot
enough path to warrant a partial fast path that breaks correctness.
Revert to always delegating to FRuntimeOps.SetProperty which handles
the full property descriptor protocol.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Correct SET_PROP fast path: guard all property descriptor cases

The fast path now only fires when ALL conditions are met:
- Receiver is TSouffleRecord (not blueprint, wrapped value, etc.)
- Record has no instance setters (HasSetters = false)
- Record has no blueprint (no class setter hierarchy)
- Key is not a private property (does not start with #)
- PutChecked succeeds (property is writable AND record is extensible)

If any condition fails, falls through to FRuntimeOps.SetProperty which
handles the full property descriptor protocol: setter invocation,
blueprint setter hierarchy, private property flags, TypeError on
non-writable/frozen/sealed violations.

The previous attempts failed because:
- v1: No guards at all — broke setters, frozen, sealed, defineProperty
- v2: Blueprint-only guard — missed Object.defineProperty accessor
  properties, Object.freeze, Object.seal on plain records
- v3 (reverted): Gave up entirely

This version correctly handles all cases by checking HasSetters,
Blueprint, private keys, and PutChecked return value before committing
to the fast path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Handle full record SetProperty protocol natively in VM

Instead of a partial fast path that falls through to the bridge, handle
the complete TSouffleRecord SetProperty protocol in the VM dispatch:

1. Instance setters: check Rec.HasSetters, look up Rec.Setters.Get(key),
   invoke the setter closure via ExecuteFunction — all Souffle-native
2. Blueprint setter hierarchy: walk Bp.Setters chain via new
   SetPropertyViaBlueprintSetter helper — all Souffle-native
3. Normal put: Rec.PutChecked handles writable/extensible checks
4. Write violation: on PutChecked failure, call
   FRuntimeOps.PropertyWriteViolation for the language-level TypeError

Add PropertyWriteViolation as a virtual method on the base
TSouffleRuntimeOperations contract (default no-op) so the VM can
request the error without knowing about Goccia types.

The bridge (FRuntimeOps.SetProperty) is now only reached for
non-record types (TSouffleBlueprint static fields, TGocciaWrappedValue).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@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