Skip to content

Use FrozenDictionary for OperatorInfo table#327

Draft
BCSharp wants to merge 2 commits intoIronLanguages:mainfrom
BCSharp:operator_dict
Draft

Use FrozenDictionary for OperatorInfo table#327
BCSharp wants to merge 2 commits intoIronLanguages:mainfrom
BCSharp:operator_dict

Conversation

@BCSharp
Copy link
Copy Markdown
Member

@BCSharp BCSharp commented Apr 9, 2026

From the documentation:

FrozenDictionary<TKey,TValue> is immutable and is optimized for situations where a dictionary is created infrequently but is used frequently at run time. It has a relatively high cost to create but provides excellent lookup performance. Thus, it is ideal for cases where a dictionary is created once, potentially at the startup of an application, and is used throughout the remainder of the life of the application.

This perfectly describes the case of OperatorInfo. Unfortunately, FrozenDictionary exists only in .NET 8.0 and above. It does not exist in .NET Framework, unless an external NuGet package is added as dependency and adding dependencies is not something I would do for a localized performance gain.

Furthermore, the really efficient (both in terms of performance as well as in heap stress) way of creating it is available only since .NET 10.0 (actually 9.0, but the syntax is easier since 10.0). This creates three ways how the lookup of OperatorInfo is performed, and it is reflected in the new code.

For .NET 10.0 and up:

It uses FrozenDictionary initialized from a blob on the stack; no temporary heap objects that have to be garbage-collected.

For .NET 8.0 up to .NET 10

It uses FrozenDictionary initialized from an array of key-value pairs constructed on the heap. It provides all performance benefits of FrozenDictionary at the cost of one temporary object on the heap and of significant size.

For .NET Framework (and .NET Standard 2.0 I suppose)

It uses generic Dictionary, just like the old code, no unnecessary objects on the heap. The construction is the fastest (which happens only once at startup) but the lookup is significantly slower.


There are some methods that do simple linear search by string of the operator table, but they all are on the obsolete path. I have added ObsoleteAttribute to make it more visible.

/// </summary>
internal sealed class OperatorInfo {
private static readonly Dictionary<ExpressionType, OperatorInfo> _infos = MakeOperatorTable(); // table of ExpressionType, names, and alt names for looking up methods.
private static readonly OperatorDictionary _infos = MakeOperatorTable(); // table of ExpressionType, names, and alt names for looking up methods.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there an advantage to having OperatorDictionary here instead of something like IReadOnlyDictionary<ExpressionType, OperatorInfo>? If not we could eliminate OperatorDictionary and simplify a bit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A call on a concrete type is faster than calling through an interface. For .NET 8+ the compiler may apply some tricks to speed up the interface call, but I am not sure when they kick in.

#if NET10_0_OR_GREATER
ReadOnlySpan<KeyValuePair<ExpressionType, OperatorInfo>> data = [
#elif NET8_0_OR_GREATER
var data = new KeyValuePair<ExpressionType, OperatorInfo>[] {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe a bit nicer?

KeyValuePair<ExpressionType, OperatorInfo>[] data = [

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It is nicer but not the same. It creates a temporary array on the heap. The .NET 10 version creates a span on the stack; no heap pressure,

Unless you refer only to line 73 — fine with me. I simply have a habit of putting the type on the RHS if possible. If you prefer to put it before the variable name (instead of var) I would do it also for the .NET Framework version, for consistency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah I was refering to just line 73. I guess I prefer the look of the new square bracket syntax to the curly brackets for building an array.

@BCSharp
Copy link
Copy Markdown
Member Author

BCSharp commented Apr 10, 2026

I am marking this PR as draft. Some benchmarks on .NET 10 show FrozenDictionary of 100 elements being ~8% slower than a generic Dictionary. The discrepancy gets bigger the more elements there are. I got this result on both macOS Tahoe and Windows 11. If it is true, it is disturbing and obviously not worth the change. Perhaps my tests are flawed or inaccurate. I need more time to investigate.

@BCSharp BCSharp marked this pull request as draft April 10, 2026 05:58
@slozier
Copy link
Copy Markdown
Contributor

slozier commented Apr 10, 2026

I guess since the point of this PR is performance, how does it compare to a plain array lookup with bounds check (cast the enum to int)?

@BCSharp
Copy link
Copy Markdown
Member Author

BCSharp commented Apr 14, 2026

Benchmarks published on the internet on .NET 8 show FrozenDictionary can be up to 70% faster than a generic Dictionary for reads. I have noticed the Dictionary on .NET 10 is noticeably better than on .NET 8. The construction of FrozenDictionary is significantly slower than Dictionary, which means it takes a number of (faster) reads to break even. For a collection of about 35 operators, the break-even is at about 700 reads (my estimate, assuming the numbers on the Internet are accurate). This is easily achieved even for medium-sized scripts, and for shorter scripts the time is maybe longer but practically insignificant.

Now I need to verify the actual speed of reads, and compare them to `Dictionary reads and an array lookup.

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.

2 participants