Skip to content
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

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

Closed
id0001 opened this issue Aug 27, 2021 · 9 comments
Closed

Comments

@id0001
Copy link

id0001 commented Aug 27, 2021

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.

@id0001 id0001 added the bug label Aug 27, 2021
@ebfortin
Copy link
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.

@ebfortin
Copy link
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.

@lipchev
Copy link
Collaborator

lipchev commented Aug 28, 2021

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

@lipchev
Copy link
Collaborator

lipchev commented Aug 28, 2021

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

@ebfortin
Copy link
Contributor

Ah the static constructor. I forgot about that one.

@angularsen
Copy link
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

@stale
Copy link

stale bot commented Oct 30, 2021

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.

@stale stale bot added the wontfix label Oct 30, 2021
@stale stale bot closed this as completed Nov 9, 2021
@Sibusten
Copy link

Sibusten commented Dec 1, 2021

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);

@jl-mobitech
Copy link

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
Projects
None yet
Development

No branches or pull requests

6 participants