Skip to content

feat: static property descriptors, zero heap allocation#720

Merged
ptondereau merged 1 commit intoextphprs:masterfrom
ptondereau:feat/static-property-descriptors
Apr 14, 2026
Merged

feat: static property descriptors, zero heap allocation#720
ptondereau merged 1 commit intoextphprs:masterfrom
ptondereau:feat/static-property-descriptors

Conversation

@ptondereau
Copy link
Copy Markdown
Member

Description

Closes #70

The property system used to heap-allocate closures (Box<dyn Fn>) and a HashMap for every PHP class at runtime. All of this metadata is known at compile time, so there's no reason for it to live on the heap.

This replaces the whole thing with static function pointers and compile-time arrays. Property lookup becomes a linear scan over two small slices (field props first, then getter/setter props), which beats hashing for the typical class size.

What changed:

  • New PropertyDescriptor<T> type with fn(&T, &mut Zval) / fn(&mut T, &Zval) pointers
  • ClassMetadata holds &'static [PropertyDescriptor<T>] instead of OnceCell<HashMap<...>>
  • Macros generate concrete getter/setter functions and static arrays
  • Removed Property enum, Prop trait, PropertyInfo struct, and all the boxing machinery
  • Added callgrind-based property read/write benchmarks

Benchmark results (callgrind, 100k iterations x 4 properties):

Operation Before (instructions) After Improvement
Read 235,203,424 157,800,000 -33%
Write 220,397,869 139,994,445 -36%
LL cache hits ~210 ~50 -76%

Cold-start (single access) is 3.7-4.2x faster due to no heap allocation on first use.

Breaking changes for anyone manually implementing RegisteredClass or PhpClassImpl (rare, macros handle this). get_properties() is gone from the trait. ClassMetadata::new() now takes &'static [PropertyDescriptor<T>]. The Property/Prop/PropertyInfo types no longer exist.

Net diff: -204 lines.

Checklist

@ptondereau
Copy link
Copy Markdown
Member Author

#310 for references

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

🐰 Bencher Report

Branchfeat/static-property-descriptors
TestbedPHP 8.4.19 (cli) (built: Mar 13 2026 01:28:58) (NTS)

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkEstimated Cyclescycles x 1e3InstructionsinstructionsL1 HitshitsLL HitshitsRAM HitshitsTotal read+writereads/writes
binary_bench::callback::callback_calls lots_of_callback_calls:("callback_call.php", 100_000) -> php '-dextension...📈 view plot
⚠️ NO THRESHOLD
575,835.94 x 1e3📈 view plot
⚠️ NO THRESHOLD
397,181,192.00📈 view plot
⚠️ NO THRESHOLD
567,817,385.00📈 view plot
⚠️ NO THRESHOLD
1,602,478.00📈 view plot
⚠️ NO THRESHOLD
176.00📈 view plot
⚠️ NO THRESHOLD
569,420,039.00
binary_bench::callback::callback_calls multiple_callback_calls:("callback_call.php", 10) -> php '-dextension=/hom...📈 view plot
⚠️ NO THRESHOLD
69.74 x 1e3📈 view plot
⚠️ NO THRESHOLD
43,157.00📈 view plot
⚠️ NO THRESHOLD
61,377.00📈 view plot
⚠️ NO THRESHOLD
441.00📈 view plot
⚠️ NO THRESHOLD
176.00📈 view plot
⚠️ NO THRESHOLD
61,994.00
binary_bench::callback::callback_calls single_callback_call:("callback_call.php", 1) -> php '-dextension=/home...📈 view plot
⚠️ NO THRESHOLD
17.73 x 1e3📈 view plot
⚠️ NO THRESHOLD
7,418.00📈 view plot
⚠️ NO THRESHOLD
10,333.00📈 view plot
⚠️ NO THRESHOLD
248.00📈 view plot
⚠️ NO THRESHOLD
176.00📈 view plot
⚠️ NO THRESHOLD
10,757.00
binary_bench::function::function_calls lots_of_function_calls:("function_call.php", 100_000) -> php '-dextension...📈 view plot
⚠️ NO THRESHOLD
59,501.37 x 1e3📈 view plot
⚠️ NO THRESHOLD
43,100,145.00📈 view plot
⚠️ NO THRESHOLD
59,500,103.00📈 view plot
⚠️ NO THRESHOLD
50.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
59,500,182.00
binary_bench::function::function_calls multiple_function_calls:("function_call.php", 10) -> php '-dextension=/hom...📈 view plot
⚠️ NO THRESHOLD
7.30 x 1e3📈 view plot
⚠️ NO THRESHOLD
4,455.00📈 view plot
⚠️ NO THRESHOLD
6,057.00📈 view plot
⚠️ NO THRESHOLD
46.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
6,132.00
binary_bench::function::function_calls single_function_call:("function_call.php", 1) -> php '-dextension=/home...📈 view plot
⚠️ NO THRESHOLD
1.94 x 1e3📈 view plot
⚠️ NO THRESHOLD
576.00📈 view plot
⚠️ NO THRESHOLD
703.00📈 view plot
⚠️ NO THRESHOLD
45.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
777.00
binary_bench::method::method_calls lots_of_method_calls:("method_call.php", 100_000) -> php '-dextension=/...📈 view plot
⚠️ NO THRESHOLD
66,032.22 x 1e3📈 view plot
⚠️ NO THRESHOLD
48,100,000.00📈 view plot
⚠️ NO THRESHOLD
65,992,125.00📈 view plot
⚠️ NO THRESHOLD
7,851.00📈 view plot
⚠️ NO THRESHOLD
24.00📈 view plot
⚠️ NO THRESHOLD
66,000,000.00
binary_bench::method::method_calls multiple_method_calls:("method_call.php", 10) -> php '-dextension=/home/...📈 view plot
⚠️ NO THRESHOLD
7.46 x 1e3📈 view plot
⚠️ NO THRESHOLD
4,810.00📈 view plot
⚠️ NO THRESHOLD
6,566.00📈 view plot
⚠️ NO THRESHOLD
10.00📈 view plot
⚠️ NO THRESHOLD
24.00📈 view plot
⚠️ NO THRESHOLD
6,600.00
binary_bench::method::method_calls single_method_call:("method_call.php", 1) -> php '-dextension=/home/r...📈 view plot
⚠️ NO THRESHOLD
1.51 x 1e3📈 view plot
⚠️ NO THRESHOLD
481.00📈 view plot
⚠️ NO THRESHOLD
627.00📈 view plot
⚠️ NO THRESHOLD
9.00📈 view plot
⚠️ NO THRESHOLD
24.00📈 view plot
⚠️ NO THRESHOLD
660.00
binary_bench::property_read::property_reads lots_of_property_reads:("property_read.php", 100_000) -> php '-dextension...📈 view plot
⚠️ NO THRESHOLD
217,301.76 x 1e3📈 view plot
⚠️ NO THRESHOLD
160,300,000.00📈 view plot
⚠️ NO THRESHOLD
216,299,898.00📈 view plot
⚠️ NO THRESHOLD
200,057.00📈 view plot
⚠️ NO THRESHOLD
45.00📈 view plot
⚠️ NO THRESHOLD
216,500,000.00
binary_bench::property_read::property_reads multiple_property_reads:("property_read.php", 10) -> php '-dextension=/hom...📈 view plot
⚠️ NO THRESHOLD
23.36 x 1e3📈 view plot
⚠️ NO THRESHOLD
16,030.00📈 view plot
⚠️ NO THRESHOLD
21,561.00📈 view plot
⚠️ NO THRESHOLD
44.00📈 view plot
⚠️ NO THRESHOLD
45.00📈 view plot
⚠️ NO THRESHOLD
21,650.00
binary_bench::property_read::property_reads single_property_read:("property_read.php", 1) -> php '-dextension=/home...📈 view plot
⚠️ NO THRESHOLD
3.82 x 1e3📈 view plot
⚠️ NO THRESHOLD
1,603.00📈 view plot
⚠️ NO THRESHOLD
2,089.00📈 view plot
⚠️ NO THRESHOLD
31.00📈 view plot
⚠️ NO THRESHOLD
45.00📈 view plot
⚠️ NO THRESHOLD
2,165.00
binary_bench::property_write::property_writes lots_of_property_writes:("property_write.php", 100_000) -> php '-dextensio...📈 view plot
⚠️ NO THRESHOLD
197,084.21 x 1e3📈 view plot
⚠️ NO THRESHOLD
144,094,106.00📈 view plot
⚠️ NO THRESHOLD
196,582,877.00📈 view plot
⚠️ NO THRESHOLD
100,050.00📈 view plot
⚠️ NO THRESHOLD
31.00📈 view plot
⚠️ NO THRESHOLD
196,682,958.00
binary_bench::property_write::property_writes multiple_property_writes:("property_write.php", 10) -> php '-dextension=/ho...📈 view plot
⚠️ NO THRESHOLD
20.47 x 1e3📈 view plot
⚠️ NO THRESHOLD
14,118.00📈 view plot
⚠️ NO THRESHOLD
19,236.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
31.00📈 view plot
⚠️ NO THRESHOLD
19,296.00
binary_bench::property_write::property_writes single_property_write:("property_write.php", 1) -> php '-dextension=/hom...📈 view plot
⚠️ NO THRESHOLD
2.90 x 1e3📈 view plot
⚠️ NO THRESHOLD
1,332.00📈 view plot
⚠️ NO THRESHOLD
1,763.00📈 view plot
⚠️ NO THRESHOLD
18.00📈 view plot
⚠️ NO THRESHOLD
30.00📈 view plot
⚠️ NO THRESHOLD
1,811.00
binary_bench::static_method::static_method_calls lots_of_static_calls:("static_method_call.php", 100_000) -> php '-dexte...📈 view plot
⚠️ NO THRESHOLD
59,501.38 x 1e3📈 view plot
⚠️ NO THRESHOLD
43,100,145.00📈 view plot
⚠️ NO THRESHOLD
59,500,101.00📈 view plot
⚠️ NO THRESHOLD
52.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
59,500,182.00
binary_bench::static_method::static_method_calls multiple_static_calls:("static_method_call.php", 10) -> php '-dextension...📈 view plot
⚠️ NO THRESHOLD
7.31 x 1e3📈 view plot
⚠️ NO THRESHOLD
4,455.00📈 view plot
⚠️ NO THRESHOLD
6,056.00📈 view plot
⚠️ NO THRESHOLD
47.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
6,132.00
binary_bench::static_method::static_method_calls single_static_call:("static_method_call.php", 1) -> php '-dextension=...📈 view plot
⚠️ NO THRESHOLD
1.95 x 1e3📈 view plot
⚠️ NO THRESHOLD
576.00📈 view plot
⚠️ NO THRESHOLD
702.00📈 view plot
⚠️ NO THRESHOLD
46.00📈 view plot
⚠️ NO THRESHOLD
29.00📈 view plot
⚠️ NO THRESHOLD
777.00
🐰 View full continuous benchmarking report in Bencher

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 14, 2026

Coverage Report for CI Build 24395157578

Coverage increased (+0.4%) to 66.276%

Details

  • Coverage increased (+0.4%) from the base build.
  • Patch coverage: 75 uncovered changes across 5 files (69 of 144 lines covered, 47.92%).
  • 12 coverage regressions across 6 files.

Uncovered Changes

File Changed Covered %
src/zend/handlers.rs 24 0 0.0%
src/class.rs 23 0 0.0%
crates/macros/src/impl_.rs 62 44 70.97%
src/builders/module.rs 8 0 0.0%
src/internal/class.rs 2 0 0.0%

Coverage Regressions

12 previously-covered lines in 6 files lost coverage.

File Lines Losing Coverage Coverage
src/class.rs 3 15.19%
src/internal/mod.rs 3 0.0%
crates/macros/src/impl_.rs 2 88.71%
src/zend/handlers.rs 2 0.0%
src/builders/module.rs 1 71.03%
src/internal/class.rs 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 12700
Covered Lines: 8417
Line Coverage: 66.28%
Coverage Strength: 33.43 hits per line

💛 - Coveralls

@ptondereau ptondereau force-pushed the feat/static-property-descriptors branch 2 times, most recently from 3611e36 to d239175 Compare April 14, 2026 10:49
…ap allocations

Previously, each PHP class property required boxing closures (Box<dyn Fn>)
and building a HashMap at runtime. This happened once per class but was
unnecessary since all property metadata is known at compile time.

Now the macros generate plain function pointers and static arrays that
live entirely in read-only memory. Property lookup uses a simple linear
scan over two small slices (field props, then getter/setter props),
which is faster than hashing for the typical class size (< 20 properties).

This removes the Property enum, Prop trait, and PropertyInfo struct,
replacing them with a single PropertyDescriptor<T> that holds:
- fn(&T, &mut Zval) -> PhpResult for getters
- fn(&mut T, &Zval) -> PhpResult for setters
- visibility flags, type info, docs

Also adds callgrind-based property read/write benchmarks.

Closes extphprs#70
@ptondereau ptondereau force-pushed the feat/static-property-descriptors branch from d239175 to 32d2c43 Compare April 14, 2026 10:56
@ptondereau ptondereau marked this pull request as ready for review April 14, 2026 11:25
@ptondereau ptondereau merged commit 3171734 into extphprs:master Apr 14, 2026
128 of 130 checks passed
@ptondereau ptondereau deleted the feat/static-property-descriptors branch April 14, 2026 11:27
@ptondereau ptondereau mentioned this pull request Apr 14, 2026
@Xenira Xenira mentioned this pull request Apr 14, 2026
paragonie-security added a commit to paragonie/ext-pqcrypto that referenced this pull request Apr 14, 2026
The upstream fix extphprs/ext-php-rs#720 should improve performance significantly
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.

Store properties in ClassMetadata

2 participants