Skip to content

QuantityInfo: internalizing the UnitInfo construction #1555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

lipchev
Copy link
Collaborator

@lipchev lipchev commented Apr 21, 2025

  • QuantityInfo: internalizing the UnitInfo construction
  • QuantityInfo: introducing a delegate for constructing the quantity (required only for net standard)
  • QuantityInfo: introducing an optional ResourceDictionary
  • QuantityInfo: replacing the TUnit[] with an IReadOnlyCollection<TUnit>
  • UnitInfo: introducing a back-reference to the QuantityInfo (making the QuantityName [Obsolete])
  • IQuantity: added the QuantityInfo<TQuantity, TUnit>, the From(double, TUnit) method and default implementations for the non-generic properties
  • QuantityInfoLookup: added another collection for the quantity by type mapping (replacing the generated code in Quantity.g.s).
  • UnitAbbreviationsCache: ReadAbbreviationsFromResourceFile implemented using the provided ResourceManager (if available)
  • updating the QuantityInfo definitions for all quantities (introducing a concrete class, such as MassInfo) with helpers for creating a derived configuration
  • HowMuch upgraded to IQuantity<HowMuch, HowMuchUnit> (the original QuantityInfo is now abstract)
  • Quantity refactored the Parse / From* methods using the default QuantityParser / QuantityInfoLookup
  • Quantity replaced the ByName dictionary with an IReadOnlyDictionary
  • UnitsNet.csproj / UnitsNet.Tests.csproj: added some (specific) implicit usings

lipchev added 2 commits April 21, 2025 08:32
- QuantityInfo: introducing a delegate for constructing the quantity (required only for net standard)
- QuantityInfo: introducing an optional ResourceDictionary
- UnitInfo: introducing a back-reference to the QuantityInfo (making the QuantityName [Obsolete])
- IQuantity: added the QuantityInfo<TQuantity, TUnit>, the From(double, TUnit) method and default implementations for the non-generic properties
- UnitAbbreviationsCache: ReadAbbreviationsFromResourceFile implemented using the provided ResourceDictionary (if available)
- updating the QuantityInfo definitions for all quantities (introducing a concrete class, such as MassInfo) with helpers for creating a derried configuration
- HowMuch upgraded to IQuantity<HowMuch, HowMuchUnit> (the original QuantityInfo is now abstract)
- UnitsNet.csproj / UnitsNet.Tests.csproj: added some (specific) implicit usings
@lipchev lipchev changed the title QuantityInfo: internalizing the UnitInfo construction (WIP) QuantityInfo: internalizing the UnitInfo construction Apr 21, 2025
@lipchev
Copy link
Collaborator Author

lipchev commented Apr 21, 2025

@angularsen The most breaking thing here should be the change from MassUnit[] to IReadOnlyCollection<MassUnit>: we still have the IReadOnlyList<UnitInfo>, so if anybody want to do a for-loop that's still an option, but in my opinion, having the indexer (backed by a dictionary) is more interesting than having 2 read-only lists, but if you want I could switch it.

Providing a delegate and resource dictionary in the QuantityInfo does look like we're mixing the domain with the infrastructure, but since we want to support nestandard2.0 this was the simplest way to have an all in one definition, that can support both internal and external quantities.

PS The UnitInfo is still missing the conversion expressions, that should be used by the UnitConverter, and we still don't have a way to configure/customize the UnitsNetSetup.Default, but for the rest of the classes (UnitParser, UnitAbbreviationsCache etc)- we're pretty close to my final version.

PS2 The new size is:

2.53 MB (2 661 376 bytes)

which is up from

2.18 MB (2 287 616 bytes)

but that would go down again, when I remove all the RegisterDefaultConversions and the switch expressions:

1.78 MB (1 867 776 bytes)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here's how caching the ResourceManager instance affects the cold start of the UnitAbbreviationsCache (note that this affects the performance for every call to ReadAbbreviationsFromResourceFile, which happens for every unit and every culture):

Before:

Method Job Runtime Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Default .NET 9.0 .NET 9.0 3.233 μs 0.0755 μs 0.2130 μs 1.00 0.09 0.2594 - 4.28 KB 1.00
EmptyWithCustomMapping .NET 9.0 .NET 9.0 3.050 μs 0.0250 μs 0.0234 μs 0.95 0.06 0.2747 - 4.53 KB 1.06
WithSpecificQuantity .NET 9.0 .NET 9.0 3.743 μs 0.0410 μs 0.0383 μs 1.16 0.08 0.4501 - 7.44 KB 1.74
WithSpecificQuantityAndCustomMapping .NET 9.0 .NET 9.0 3.767 μs 0.0521 μs 0.0487 μs 1.17 0.08 0.4578 - 7.69 KB 1.80
Default .NET Framework 4.8 .NET Framework 4.8 10.512 μs 0.0382 μs 0.0319 μs 1.00 0.00 1.3580 0.0153 8.36 KB 1.00
EmptyWithCustomMapping .NET Framework 4.8 .NET Framework 4.8 10.837 μs 0.0324 μs 0.0271 μs 1.03 0.00 1.3885 0.0153 8.56 KB 1.02
WithSpecificQuantity .NET Framework 4.8 .NET Framework 4.8 11.704 μs 0.1035 μs 0.0968 μs 1.11 0.01 1.8768 0.0305 11.54 KB 1.38
WithSpecificQuantityAndCustomMapping .NET Framework 4.8 .NET Framework 4.8 11.719 μs 0.0276 μs 0.0258 μs 1.11 0.00 1.9073 0.0305 11.74 KB 1.40

After:

Method Job Runtime Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Default .NET 9.0 .NET 9.0 194.2 ns 2.92 ns 2.73 ns 1.00 0.02 0.0904 0.0005 1.48 KB 1.00
EmptyWithCustomMapping .NET 9.0 .NET 9.0 248.0 ns 2.78 ns 2.32 ns 1.28 0.02 0.1054 0.0005 1.73 KB 1.17
WithSpecificQuantity .NET 9.0 .NET 9.0 1,495.2 ns 25.54 ns 23.89 ns 7.70 0.16 0.4501 0.0076 7.36 KB 4.98
WithSpecificQuantityAndCustomMapping .NET 9.0 .NET 9.0 1,627.0 ns 19.65 ns 18.38 ns 8.38 0.15 0.4654 0.0076 7.61 KB 5.15
Default .NET Framework 4.8 .NET Framework 4.8 385.5 ns 1.09 ns 0.97 ns 1.00 0.00 0.2728 0.0014 1.68 KB 1.00
EmptyWithCustomMapping .NET Framework 4.8 .NET Framework 4.8 500.3 ns 0.84 ns 0.79 ns 1.30 0.00 0.3042 0.0010 1.87 KB 1.12
WithSpecificQuantity .NET Framework 4.8 .NET Framework 4.8 1,342.3 ns 5.49 ns 5.13 ns 3.48 0.02 0.8106 0.0095 4.99 KB 2.98
WithSpecificQuantityAndCustomMapping .NET Framework 4.8 .NET Framework 4.8 1,432.3 ns 10.34 ns 9.67 ns 3.72 0.03 0.8430 0.0114 5.19 KB 3.09

There is of course some added cost to the constructor of the QuantityInfo (I've got a benchmark which I think I forgot to add), but that's just a one time cost (per selected quantity) which is generally incurred on start instead of on the first request for a given unit and culture.

My initial thought was to use some sort of a struct representing the Tuple<Assembly, string> (that is required for the ReadAbbreviationsFromResourceFile to work with external units), but ultimately decided against it. Here's what this looks like in the sample project:

        public static readonly QuantityInfo<HowMuch, HowMuchUnit> Info = new(
            HowMuchUnit.Some,
            new UnitDefinition<HowMuchUnit>[]
            {
                new(HowMuchUnit.Some, "Some", BaseUnits.Undefined),
                new(HowMuchUnit.ATon, "Tons", new BaseUnits(mass: MassUnit.Tonne), QuantityValue.FromTerms(1, 10)),
                new(HowMuchUnit.AShitTon, "ShitTons", BaseUnits.Undefined, QuantityValue.FromTerms(1, 100))
            },
            new BaseDimensions(0, 1, 0, 0, 0, 0, 0),
            // providing a resource manager for the unit abbreviations (optional)
            Properties.CustomQuantities_HowMuch.ResourceManager); 

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here's the before (still using Enum.GetValues(typeof(MassUnit)).Cast<MassUnit>().ToArray() on both targets):

Method Job Runtime NbIterations Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
CreateDefaultMass .NET 9.0 .NET 9.0 1000 804.5 μs 16.03 μs 27.22 μs 1.00 0.05 351.5625 6.8359 5.62 MB 1.00
CreateDefaultVolume .NET 9.0 .NET 9.0 1000 1,384.4 μs 21.80 μs 18.21 μs 1.72 0.06 494.1406 13.6719 7.9 MB 1.41
CreateDefaultVolumeFlow .NET 9.0 .NET 9.0 1000 1,938.2 μs 36.43 μs 35.78 μs 2.41 0.09 634.7656 25.3906 10.15 MB 1.81
CreateDefaultMass .NET Framework 4.8 .NET Framework 4.8 1000 7,751.9 μs 59.94 μs 56.07 μs 1.00 0.01 1250.0000 15.6250 7.56 MB 1.00
CreateDefaultVolume .NET Framework 4.8 .NET Framework 4.8 1000 14,789.7 μs 85.69 μs 80.16 μs 1.91 0.02 1937.5000 46.8750 11.71 MB 1.55
CreateDefaultVolumeFlow .NET Framework 4.8 .NET Framework 4.8 1000 20,617.8 μs 94.10 μs 88.02 μs 2.66 0.02 2562.5000 93.7500 15.42 MB 2.04

After:

Method Job Runtime NbIterations Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
CreateDefaultMass .NET 9.0 .NET 9.0 1000 2.351 ms 0.0185 ms 0.0173 ms 1.00 0.01 429.6875 7.8125 6.86 MB 1.00
CreateDefaultVolume .NET 9.0 .NET 9.0 1000 3.175 ms 0.0333 ms 0.0311 ms 1.35 0.02 609.3750 - 9.86 MB 1.44
CreateDefaultVolumeFlow .NET 9.0 .NET 9.0 1000 3.750 ms 0.0368 ms 0.0326 ms 1.60 0.02 789.0625 11.7188 12.63 MB 1.84
CreateDefaultMass .NET Framework 4.8 .NET Framework 4.8 1000 2.578 ms 0.0153 ms 0.0143 ms 1.00 0.01 1265.6250 27.3438 7.6 MB 1.00
CreateDefaultVolume .NET Framework 4.8 .NET Framework 4.8 1000 3.652 ms 0.0270 ms 0.0253 ms 1.42 0.01 2035.1563 62.5000 12.24 MB 1.61
CreateDefaultVolumeFlow .NET Framework 4.8 .NET Framework 4.8 1000 4.486 ms 0.0216 ms 0.0202 ms 1.74 0.01 2367.1875 78.1250 14.21 MB 1.87

This is quite remarkable really, just for the fact that for the first time we've got a benchmark in which .NET Framework 4.8 is neck-a-neck with .NET 9.0 🤣

Comment on lines +107 to +119
/// <summary>
/// Creates a new instance of the <see cref="MassInfo"/> class with the default settings for the Mass quantity and a callback for customizing the default unit mappings.
/// </summary>
/// <param name="customizeUnits">
/// A callback function for customizing the default unit mappings.
/// </param>
/// <returns>
/// A new instance of the <see cref="MassInfo"/> class with the default settings.
/// </returns>
public static MassInfo CreateDefault(Func<IEnumerable<UnitDefinition<MassUnit>>, IEnumerable<IUnitDefinition<MassUnit>>> customizeUnits)
{
return new MassInfo(nameof(Mass), DefaultBaseUnit, customizeUnits(GetDefaultMappings()), new Mass(0, DefaultBaseUnit), DefaultBaseDimensions);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, you might be wondering about this thing here (currently uncovered by tests). It allows us to include, exclude or re-configure certain units (not very useful In the current version, where the conversion expressions are still not available).

Here's how this is covered in #1544 : MassTestsBase.g.cs.

The problem is that this it uses these extensions, (tested here) which, I just realized, are using two generic type parameters, making them susceptible to the bug with ReSharper.

If you think we can Ignore and Continue (and you agree with the rest of the changes so far), then I can add them here.

PS Note that in #1544 I went to even further and (with the use of some dazzling re-directions) was able to make the default Mass.Info configurable from the UnitsNetSetup.DefaultConfigurationBuilder, but that's still ahead of us (should you decide to take the red pill 🤣 ).

@angularsen
Copy link
Owner

angularsen commented Apr 24, 2025

@lipchev just a heads up that this week is crazy for me and I'm traveling this weekend, not sure I'll get around to much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants