Skip to content

First call of Quantity.From is extremely slow compared to subsequent calls #965

Closed

Description

@id0001

As stated in the title the first call to Quantity.From is noticeably slow, which impacts the user experience when used for a textbox.

To reproduce I basically run the following code:

Stopwatch sw = Stopwatch.StartNew();
Ratio v = (Ratio)Quantity.From(50, RatioUnit.Percent);   
sw.Stop();   
Console.WriteLine($"Time: {sw.Elapsed.TotalMilliseconds}");   

Result:

Time: 1204.4786ms
Time: 0.0041ms
Time: 0.0029ms
Time: 0.0022ms
Time: 0.0011ms
Time: 0.002ms
Time: 0.002ms
Time: 0.0021ms
Time: 0.0015ms
Time: 0.0019ms
Time: 0.0022ms
Time: 0.0015ms
Time: 0.0026ms
Time: 0.0017ms
Time: 0.0011ms
Time: 0.0021ms
Time: 0.0014ms

It's only the first call to Quantity.From(). Doesn't matter which unit and changing units after does not produce longer times.

Activity

  1. ebfortin commented on Aug 28, 2021

    @ebfortin
    Contributor

    Although the first one seems a bit high in reality I think it's related to something normal where the Jit for the first calls will emit un optimized machine code and will optimize in subsequent calls only. It's just the way the runtime is designed.

    For a better benchmarking I suggest you use BenchmarkDotNet. It has some facilities to evaluate different scenarios and situations.

    It would regardless be interesting to go look at the code for Quantity.From to see if reflection is involved for the first call.

  2. ebfortin commented on Aug 28, 2021

    @ebfortin
    Contributor

    Went to see the code and there's a kickass big switch case involved in the Quantity.From method using pattern matching. Pretty sure my first explanation is the right one : first call is executed with the "lazy" code the Jit emitted which is not optimized.

    You could resolve this by prr compiling your assemblies with the AOT (ahead of time compiler). It was called ngen before but now I don't know.

  3. lipchev commented on Aug 28, 2021

    @lipchev
    Collaborator

    Hey guys,
    I've started work (way back when..) on a V2 of the Benchmarks PR. I haven't yet committed the customized benchmarks page (as there are a couple more things to fix) but you can browse through the updated results on my branch.

    Here is the relevant extract for the unit construction:

    BenchmarkDotNet=v0.13.0, OS=Windows 10.0.17763.1935 (1809/October2018Update/Redstone5)
    Intel Xeon Platinum 8171M CPU 2.60GHz, 1 CPU, 2 logical and 2 physical cores
    .NET SDK=5.0.203
      [Host]     : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
      Job-BMHMKH : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
    
    Runtime=.NET 5.0  Toolchain=netcoreapp50  
    
    Method Categories Mean Error StdDev StdErr Min Max Median Ratio MannWhitney(5%) RatioSD Gen 0 Gen 1 Gen 2 Allocated
    new(value,unit) Unit,Micro,Construction,Value 14.99 ns 0.289 ns 0.296 ns 0.072 ns 14.38 ns 15.58 ns 14.97 ns 1.00 Base 0.00 - - - -
    FromUnit(quantityValue) Unit,Micro,Construction,Value 31.31 ns 0.510 ns 0.452 ns 0.121 ns 30.58 ns 32.34 ns 31.34 ns 2.09 Slower 0.04 - - - -
    FromQuantityInfo(randomInfo,value) Quantity,Micro,Construction,Value 64.98 ns 0.991 ns 0.927 ns 0.239 ns 63.36 ns 66.47 ns 64.99 ns 4.33 Slower 0.09 0.0017 - - 32 B
    Quantity.From(value,unit) Unit,Micro,Construction,Value 83.88 ns 1.133 ns 1.059 ns 0.274 ns 82.42 ns 86.34 ns 83.82 ns 5.58 Slower 0.12 0.0017 - - 32 B
    Quantity<Q,U>.From(value,unit) Unit,Micro,Construction,Value 92.46 ns 1.089 ns 1.019 ns 0.263 ns 91.03 ns 94.43 ns 92.37 ns 6.16 Slower 0.14 0.0029 - - 56 B
    Quantity.From(value,randomUnit) Unit,Micro,Construction,Value 109.56 ns 1.692 ns 1.500 ns 0.401 ns 106.96 ns 112.46 ns 109.47 ns 7.31 Slower 0.15 0.0017 - - 32 B
    new(value,unitSystem) UnitSystem,Micro,Construction,Value 393.85 ns 7.595 ns 8.746 ns 1.956 ns 377.97 ns 414.86 ns 393.32 ns 26.29 Slower 0.64 0.0099 - - 192 B
  4. lipchev commented on Aug 28, 2021

    @lipchev
    Collaborator

    The first call to Quantity triggers the static constructor, which goes about constructing all the QuantityInfos (partly lazy). I don't exactly remember the order of things, but Quantity.From does have a one-time initialization cost (however I could not figure out how to measure it). There are also these one-time initialization costs that you might also like to know about:

    BenchmarkDotNet=v0.13.0, OS=Windows 10.0.17763.1935 (1809/October2018Update/Redstone5)
    Intel Xeon Platinum 8171M CPU 2.60GHz, 1 CPU, 2 logical and 2 physical cores
    .NET SDK=5.0.203
      [Host]     : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
      Job-OYANID : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT
    
    Runtime=.NET 5.0  Toolchain=netcoreapp50  
    
    Method Mean Error StdDev StdErr Min Max Median Gen 0 Gen 1 Gen 2 Allocated
    InitUnitAbbreviationsCache 1,933.2 μs 37.46 μs 35.04 μs 9.05 μs 1,876.7 μs 1,985.7 μs 1,934.0 μs 66.4063 33.2031 - 1,235 KB
    InitUnitConverter 575.1 μs 10.25 μs 17.13 μs 2.85 μs 536.9 μs 603.9 μs 573.5 μs - - - 719 KB
  5. ebfortin commented on Aug 28, 2021

    @ebfortin
    Contributor

    Ah the static constructor. I forgot about that one.

  6. angularsen commented on Aug 30, 2021

    @angularsen
    Owner

    I expect some JIT performance hit on the first usage of Units.NET, due to the crazy number of types we have plus some relatively heavy initial setup of abbreviation cache and unit converters.

    I'm sure this can all be optimized, so if someone feels the itch to hack at it, please go ahead :-D

  7. stale commented on Oct 30, 2021

    @stale

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

  8. Sibusten commented on Dec 1, 2021

    @Sibusten

    For anyone finding this later, I was able to move the cost to app bootup by adding the following to Main:

    RuntimeHelpers.RunClassConstructor(typeof(Quantity).TypeHandle);
  9. jl-mobitech commented on Sep 21, 2022

    @jl-mobitech

    For anyone finding this later, I was able to move the cost to app bootup by adding the following to Main:

    RuntimeHelpers.RunClassConstructor(typeof(Quantity).TypeHandle);

    Thanks for this. To further improve UX, you can do the following:

    await Task.Run(() => RuntimeHelpers.RunClassConstructor(typeof(Quantity).TypeHandle)).ConfigureAwait(false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions