Skip to content

Identify DotvvmProperty with systematic 32-bit IDs#1867

Open
exyi wants to merge 11 commits intomainfrom
property-dict-refactor
Open

Identify DotvvmProperty with systematic 32-bit IDs#1867
exyi wants to merge 11 commits intomainfrom
property-dict-refactor

Conversation

@exyi
Copy link
Member

@exyi exyi commented Oct 4, 2024

All DotvvmProperties now get assigned 32-bit IDs, which can be used for more efficient lookup and identification. The has a ID 16b + 16b structure to allow optimizing certain common operations and make the assignment consistent even when we initialize controls on multiple threads.

The ID format is (bit 0 is (id >> 31)&1, bit 31 is id&1)

  • bit 0
    • =1 - is property group
      • bits 16-30: Identifies the property declaring type.
      • bits 0-15: Identifies the property in the declaring type.
    • =0 - any other DotvvmProperty
      • bits 16-30: Identifies the property group
      • bits 0-15: Identifies a string - the property group member

All IDs are assigned sequentially, with reserved blocks at the start for the most important types which we might want to address directly in a compile-time constant.

IDs put a limit on the number of properties in a type (>= 32k), the number of property groups (32k), and the number of property group members (64k). All property groups share the same name dictionary, which allows for significant memory savings, but it might be limiting in obscure cases.

As property groups share the name-ID mapping, we do not to keep the GroupedDotvvmProperty instances in memory after the compilation is done. VirtualPropertyGroupDictionary will map strings directly to the IDs and back, without going to GroupedDotvvmProperty.

Shorter unmanaged IDs allows for efficient lookup in unorganized arrays using SIMD – 8 property IDs fit into a single vector register. Since controls with more than 8 properties are not common, the brute force will actually be faster than anything. The custom perfect-hashing algorithm is therefore removed. Possible states of the DotvvmControlProperties dictionary are:

  • Empty: keys and values are both null (this is default struct state)
  • Array8: keys and values are 8 element arrays without any ordering guarantees (keys is 256 bits)
  • Array16: keys and values are 16 element arrays without any ordering guarantees (keys is 512 bits)
  • Dictionary: keys is null, values is Dictionary<DotvvmPropertyId, object?>. This is mainly fallback for the rare case someone puts this many properties onto a single control.

Moreover, the dictionary remembers two booleans: ownsValues and ownsKeys which are used for copy-on-write control cloning.

  • When new arrays are initialized, both are set to true
  • When control is cloned, both are set to false, and the dictionary is copied by reference.
    • Exception being if any property contains an instance of DotvvmBindableObject
      • the object must be cloned recursively, and we therefore clone the values array, while still copying keys by reference
      • result is: ownsValues = true, ownsKeys = false
  • When existing property is updated:
    • If !ownsValues, we clone the values array and set ownsValues = true (ownsKeys is untouched)
  • When new property is added / When property is removed:
    • If !ownsKeys, we clone the keys array and set ownsKeys = true
    • If !ownsValues, we clone the values array and set ownsValues = true

Number of other places in the framework were adjusted to address properties directly using the IDs to take advantage of the performance gains.

@exyi exyi marked this pull request as draft October 4, 2024 11:44
@exyi exyi added this to the Version 5.0 milestone Oct 4, 2024
@exyi exyi force-pushed the property-dict-refactor branch from a2f0889 to 4745066 Compare November 3, 2024 17:33
@exyi exyi force-pushed the property-dict-refactor branch from ddd5fa4 to fcf2744 Compare December 8, 2024 09:25
@exyi exyi requested a review from Copilot April 26, 2025 13:37

This comment was marked as resolved.

@exyi exyi force-pushed the property-dict-refactor branch from 5a41705 to 72ec420 Compare August 7, 2025 20:36
exyi added 6 commits August 13, 2025 21:42
All DotvvmProperties are now assigned 32-bit ids, which can be used for more
efficient lookups and identifications. The ID is formatted to allow optimizing
certain common operations and make the assignment consistent even when
we initialize controls on multiple threads.

The ID format is (bit 0 is (id >> 31)&1, bit 31 is id&1)
* bit 0
  - =1 - is property group
    * bits 16-30: Identifies the property declaring type.
    * bits 0-15: Identifies the property in the declaring type.
  - =0 - any other DotvvmProperty
    * bits 16-30: Identifies the property group
    * bits 0-15: Identifies a string - the property group member

All IDs are assigned sequentially, with reserved blocks at the
start for the most important types which we might want to
adress directly in a compile-time constant.

IDs put a limit on the number of properties in a type (64k),
the number of property groups (32k), and the number of property
group members. All property groups share the same name dictionary,
which allows for significant memory savings, but it might
be limiting in obscure cases.

As property groups share the name-ID mapping, we do not to keep
the GroupedDotvvmProperty instances in memory after the compilation
is done. VirtualPropertyGroupDictionary will map strings directly
to the IDs and back.

Shorter unmanaged IDs allows for efficient lookups in unorganized
arrays using SIMD. 8 property IDs fit into a single vector register.
Since, controls with more than 8 properties are not common, we
can eliminate hashing with this "brute force".
We should evaluate whether it makes sense to keep the custom
small table--optimized hashtable. This patch keeps that in place.
The standard Dictionary`2 is also faster when indexed with integer
compared to a reference type.

Number of other places in the framework were adjusted to adress
properties directly using the IDs.
…rtual GetValue

This is now frequently needed when working only
with the ID instead of working with DotvvmProperty.
Without this information, we pay not only for virtual dispatch,
but also the (more expensive) cost of looking up the
DotvvmProperty instance.

Since most properties use plain DotvvmProperty
and don't need the polymorphism, we now avoid
this cost by using the last bit of the property ID
to indicate whether the property can use direct access.
@exyi exyi force-pushed the property-dict-refactor branch from 72ec420 to caa736d Compare August 13, 2025 19:42
exyi added 2 commits August 13, 2025 21:50
* Optimize relevant property accessed using the new compile-time constants
* Optimize PropertyGroup.Clear
* Cover DotvvmPropertyDictionary with more tests (and fix some bugs)
@exyi exyi force-pushed the property-dict-refactor branch from caa736d to ddf5945 Compare August 15, 2025 15:05
@exyi exyi requested a review from Copilot August 18, 2025 18:51
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements systematic 32-bit IDs for DotvvmProperty to enable more efficient lookup and identification. All DotvvmProperties now receive sequential IDs with a 16-bit type ID + 16-bit property ID structure, allowing for optimized operations and consistent assignment even during multi-threaded control initialization.

Key changes include:

  • Introduction of DotvvmPropertyId struct for efficient property identification using 32-bit IDs
  • Replacement of perfect hashing with SIMD-optimized linear search for small property collections (8-16 elements)
  • Implementation of copy-on-write semantics for control cloning to improve memory efficiency

Reviewed Changes

Copilot reviewed 52 out of 53 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Framework/Framework/Controls/PropertyImmutableHashtable.cs Replaced with PropertyDictionaryImpl containing SIMD-optimized property lookup algorithms
src/Framework/Framework/Controls/DotvvmControlProperties.cs Complete rewrite to support efficient array-based storage with copy-on-write semantics
src/Framework/Framework/scripts/generate-property-ids.mjs New script to generate consistent property ID assignments
src/Framework/Framework/Controls/DotvvmBindableObject.cs Updated to use new property ID system and optimized property access patterns
src/Framework/Framework/Controls/HtmlGenericControl.cs Modified to use property IDs directly for performance-critical rendering paths
Comments suppressed due to low confidence (1)

src/Framework/Framework/Controls/DotvvmControlProperties.cs:478

  • [nitpick] The struct name 'PropertyGroupEnumerable' is ambiguous. Consider renaming to 'PropertyGroupCollection' or 'PropertyGroupView' to better indicate its purpose.
                var slot = Impl.FindSlotOrFree8(keys, p, out var exists);

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@exyi exyi force-pushed the property-dict-refactor branch 2 times, most recently from 9c7ec4d to 50d3239 Compare August 18, 2025 23:05
@exyi exyi force-pushed the property-dict-refactor branch from 50d3239 to 065a219 Compare August 18, 2025 23:14
@exyi exyi marked this pull request as ready for review August 20, 2025 14:49
@exyi
Copy link
Member Author

exyi commented Oct 30, 2025

I'm attaching benchmark results for this PR. TL;DR: it's faster by about 50% in targeted benchmarks.

It's subset of the dotvvm-benchmarks suite run across .NET 8/9/10 with TieredPGO on/off.
Each benchmark has two original runs (1A, 1B) and two runs with the changes (2A, 2B).
The “ratio” is new/original (lower is better).

Improvement on the test plain-dotvvm page is smaller (~10%), since that benchmark does not mutate any DotvvmControl properties, and the old hashtable implementation served this workload well (it's not measured in this set)

tmp-benchmark-comparison.csv

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