Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Some System.Decimal performance improvements - #99212

Merged
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc
Feb 3, 2025
Merged

Some System.Decimal performance improvements#99212
tannergooding merged 28 commits into
dotnet:mainfrom
Daniel-Svensson:deciaml_calc

Conversation

@Daniel-Svensson

@Daniel-SvenssonDaniel-Svensson commented Mar 3, 2024

Copy link
Copy Markdown
Contributor

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

Overview

  • x64
    • Division time is 60-80% of old time (~25-66% speedup)
    • Multiplication worst case down to 65% (~53% speedup)
  • x86
    • Division time is 70-86% of old time (~16-42% speedup)
    • Multiplication - might be a few % slower in worst case (not more than 8%)

Divide x64


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New131/3 32bit division26.42 ns0.115 ns0.102 ns0.71
Main131/3 32bit division37.40 ns0.038 ns0.032 ns1.00
New107374182.393.334bit / 32bit24.36 ns0.488 ns0.407 ns0.78
Main107374182.393.334bit / 32bit31.26 ns0.469 ns0.439 ns1.00
New10145(...)50239 [21]396bit / 32bit18.11 ns0.126 ns0.112 ns0.61
Main10145(...)50239 [21]396bit / 32bit29.63 ns0.118 ns0.098 ns1.00
New39291(...)21183 [22]1288490192096bit / 64bit30.19 ns0.157 ns0.147 ns0.86
Main39291(...)21183 [22]1288490192096bit / 64bit35.06 ns0.135 ns0.126 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit45.50 ns0.184 ns0.172 ns0.78
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.17 ns0.249 ns0.233 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.537 ns0.0281 ns0.0263 ns1.01
Main15564.650930.0000000332bit * 32bit5.503 ns0.0311 ns0.0275 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.299 ns0.1154 ns0.0964 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit23.860 ns0.1578 ns0.1399 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit13.066 ns0.0910 ns0.0851 ns0.89
Main21702051861934.7501351240935.5381666264it * 64bit14.759 ns0.1160 ns0.1085 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.228 ns0.0496 ns0.0464 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.584 ns0.0786 ns0.0735 ns1.00

x86 (32-bit)

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
Ole32131/3 32bit division31.68 ns0.052 ns0.049 ns0.51
New131/3 32bit division44.65 ns0.094 ns0.083 ns0.72
Main131/3 32bit division62.36 ns0.533 ns0.472 ns1.00
Ole32107374182.393.334bit / 32bit27.64 ns0.032 ns0.027 ns0.49
New107374182.393.334bit / 32bit39.30 ns0.090 ns0.084 ns0.70
Main107374182.393.334bit / 32bit56.35 ns0.057 ns0.048 ns1.00
Ole3210145(...)50239 [21]396bit / 32bit24.04 ns0.018 ns0.016 ns0.70
New10145(...)50239 [21]396bit / 32bit29.66 ns0.041 ns0.039 ns0.86
Main10145(...)50239 [21]396bit / 32bit34.52 ns0.077 ns0.068 ns1.00
Ole3239291(...)21183 [22]1288490192096bit / 64bit43.04 ns0.036 ns0.028 ns0.50
New39291(...)21183 [22]1288490192096bit / 64bit65.67 ns0.187 ns0.166 ns0.77
Main39291(...)21183 [22]1288490192096bit / 64bit85.67 ns0.112 ns0.105 ns1.00
Ole3239291(...)21183 [22]1844674421114850512096bit / 96bit48.31 ns0.045 ns0.038 ns0.43
New39291(...)21183 [22]1844674421114850512096bit / 96bit79.02 ns0.514 ns0.456 ns0.71
Main39291(...)21183 [22]1844674421114850512096bit / 96bit112.06 ns0.549 ns0.486 ns1.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.200
[Host] : .NET 8.0.2 (8.0.224.6711), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New15564.650930.0000000332bit * 32bit12.28 ns0.040 ns0.037 ns1.030.00
Main15564.650930.0000000332bit * 32bit11.92 ns0.037 ns0.033 ns1.000.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.89 ns0.160 ns0.142 ns1.030.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit63.14 ns0.161 ns0.134 ns1.000.00
New21702051861934.7501351240935.5381666264it * 64bit40.29 ns0.807 ns0.863 ns0.980.02
Main21702051861934.7501351240935.5381666264it * 64bit41.18 ns0.293 ns0.274 ns1.000.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.50 ns0.079 ns0.070 ns1.080.01
Main57510(...)29861 [21]0.0119304696bit * 32bit17.08 ns0.087 ns0.082 ns1.000.00

…se code than
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | a | b | Mean | Error | StdDev | Allocated |
|--------------------------- |-- |----------- |---------:|----------:|----------:|----------:|
| Mul64By32_New | 3 | 4294967295 | 2.068 ns | 0.0459 ns | 0.0383 ns | - |
| Mul64By32_Ori | 3 | 4294967295 | 2.916 ns | 0.0231 ns | 0.0193 ns | - |
- Add comment to BigMul64By32 and make it return nunit to avoid clearing upper 32 bits
- Simplify IncreaseScale
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@Daniel-SvenssonDaniel-Svensson changed the title Some Decimal performance improvementsSome System.Decimal performance improvementsMar 3, 2024
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Decimal.DecCalc.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

Cc @jkotas for the questions about relative prioritization of 32bit performance.

@huoyaoyuan

Copy link
Copy Markdown
Member

It's similar to #99196 (and what I did in https://github.com/huoyaoyuan/runtime/tree/decimal-divrem)

@jkotasjkotas added area-System.Numerics and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 4, 2024
@ghost

ghost commented Mar 4, 2024

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

Issue Details

Speed up decimal Multiplication and Division (mostly on x64, but some minor improvements on x86).

It does mostly 3 tricks:

  • Use new X86 unstrincts for DivRem to reduce number of divisions, an to avoid doing expensive 64bit divisions (via helper call) in 32bit mode
  • Add a helper for 64 * 32 bit multiplication (which on 64bit platforms becomes a single call to BigMul)
  • Changes ulong Math.BigMul(uint, uint) to not useBMI2.MultiplyNoFlags for better generated code (it gets rid of the memory write)

This is small port of the code from #7778 now that DivRem is accessible.
There is some more fixes that can be done at a later time (such as using 64 by 32 multiply in more places), but many of the original changes are not relevant due either missing low lewel primitives (such as carry) or worse code generated code (big mul)

Questions

  • I saw code currently uses both checks on IntPtr.Size and #if TARGET_32BIT for target conditional code, what is the preferred style ?
  • How do you prioritize performance of 64bit vs 32bit runtime ?
    • I could make a go and try and rewrite multiply to the fully optimized C++ version, but such a C# version would probably have some loss of performance for the 32bit version

Remarks for better performance

Benchmarks

Full benchmark code can be found at https://github.com/Daniel-Svensson/ClrExperiments/tree/master/ClrDecimal/Benchmarks

64bit Divide


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New92233(...)63.19 [21]3.334bit / 32bit23.09 ns0.145 ns0.121 ns0.54
Main92233(...)63.19 [21]3.334bit / 32bit42.75 ns0.195 ns0.183 ns1.00
New10145(...)02.39 [22]0.396bit / 32bit14.21 ns0.086 ns0.072 ns0.48
Main10145(...)02.39 [22]0.396bit / 32bit29.83 ns0.074 ns0.066 ns1.00
New39291(...)11.83 [23]12884901.92096bit / 64bit30.57 ns0.045 ns0.040 ns0.86
Main39291(...)11.83 [23]12884901.92096bit / 64bit35.45 ns0.190 ns0.159 ns1.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit47.87 ns0.129 ns0.114 ns0.82
Main39291(...)21183 [22]1844674421114850512096bit / 96bit58.72 ns0.106 ns0.094 ns1.00

64bit Multiply


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit5.580 ns0.0320 ns0.0300 ns1.00
Main15564.650930.0000000332bit * 32bit5.578 ns0.0096 ns0.0085 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit22.684 ns0.2357 ns0.2089 ns0.94
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit24.079 ns0.0712 ns0.0666 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit12.748 ns0.0281 ns0.0249 ns0.91
Main21702051861934.7501351240935.5381666264it * 64bit13.954 ns0.0561 ns0.0525 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit6.270 ns0.0156 ns0.0130 ns0.65
Main57510(...)29861 [21]0.0119304696bit * 32bit9.707 ns0.1240 ns0.1160 ns1.00

32bit

32bit benchmarks results

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatioRatioSD
New92233(...)63.19 [21]3.334bit / 32bit60.96 ns0.172 ns0.161 ns1.010.01
Main92233(...)63.19 [21]3.334bit / 32bit60.42 ns0.564 ns0.500 ns1.000.00
New10145(...)02.39 [22]0.396bit / 32bit36.85 ns0.072 ns0.063 ns1.010.00
Main10145(...)02.39 [22]0.396bit / 32bit36.49 ns0.090 ns0.085 ns1.000.00
New39291(...)11.83 [23]12884901.92096bit / 64bit78.43 ns0.244 ns0.217 ns0.820.01
Main39291(...)11.83 [23]12884901.92096bit / 64bit95.56 ns0.663 ns0.620 ns1.000.00
New39291(...)21183 [22]1844674421114850512096bit / 96bit81.67 ns1.663 ns2.489 ns0.620.03
Main39291(...)21183 [22]1844674421114850512096bit / 96bit132.38 ns2.643 ns4.559 ns1.000.00

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3)
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X86 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain 
MethodabdescrMeanErrorStdDevRatio
New15564.650930.0000000332bit * 32bit10.65 ns0.047 ns0.042 ns0.86
Main15564.650930.0000000332bit * 32bit12.36 ns0.062 ns0.052 ns1.00
New18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit65.15 ns0.159 ns0.141 ns1.00
Main18446(...)19935 [21]38738(...)51235 [22]96bit * 96bit64.84 ns0.178 ns0.149 ns1.00
New21702051861934.7501351240935.5381666264it * 64bit40.23 ns0.119 ns0.112 ns0.97
Main21702051861934.7501351240935.5381666264it * 64bit41.45 ns0.534 ns0.500 ns1.00
New57510(...)29861 [21]0.0119304696bit * 32bit18.69 ns0.082 ns0.077 ns1.10
Main57510(...)29861 [21]0.0119304696bit * 32bit16.96 ns0.051 ns0.048 ns1.00
Author:Daniel-Svensson
Assignees:-
Labels:

area-System.Numerics, needs-area-label

Milestone:-

@jkotas

Copy link
Copy Markdown
Member

relative prioritization of 32bit performance.

We do not actively invest into improving 32bit performance specifically. At the same, we avoid regressing 32bit performance unless there is a very good reason.

// TODO: https://github.com/dotnet/runtime/issues/5213
ulong tmp, div;
if (bufNum.U2 != 0)
if (X86.X86Base.X64.IsSupported)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a lot of extra code for a relatively small performance increase on modern CPUs.

We currently use Skylake as the baseline for a lot of our perf score numbers and 3ns savings for 13 lines of new code (+23 more for x86) doesn't really seem worth it.

Ideally any improvements would be shared across all 3 platforms or be significant enough to make the additional complexity worthwhile.

@Daniel-SvenssonDaniel-SvenssonMar 13, 2024

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I removed the x64 specific code and just kept the x86 since it was just a few ns faster than the x86 specific code (as long as there are no branch misspredictions) and the x86 should be faster on Skylake even with a misspredicted branch.

It stills gives around ~10ns faster division for 96/32 case

I can change it to x64 only code insted (2 if statements and 2 DivRem calls) if you rather like it.

/// <returns>hi bits of the result</returns>
/// <remarks>returns nuint instead of uint to skip clearing upper 32bits on 64bit platforms</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static nuint BigMul64By32(ulong a, uint b, out ulong low)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same general comment. This is another case where this should really just be a general purpose opt in the JIT, recognizing ulong * uint and optimizing it accordingly, rather than requiring a full ulong * ulong, just opting to do whichever is most efficient.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good if the JIT could recognice it and optimize the code.
Since the currently most popular approach to the get more than 64bit result is to call BigMul I hope that that method can be optimized in the future.

I created an internal overload of BigMul in the time beeing, which can easily be removed when the JIT can optimize 32*64 bit => 128bit multiplications

Comment on lines -156 to -171
#if TARGET_32BIT
if (Bmi2.IsSupported)
{
uint low;
uint high = Bmi2.MultiplyNoFlags(a, b, &low);
return ((ulong)high << 32) | low;
}
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AFAIR, the intent was for us to have an overload that returns a tuple and uses the JIT multi-reg return hookups, but we haven't exposed that yet.

We ideally aren't removing opts like this, especially when even with the memory access it is potentially faster or close enough in most cases.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Ok I added it back, but it contributed to more than 20% slower division (more than 10ns for 1/3) so I removed a of calls and replaced it with the fallback path (ulong)(uint)a * (uint)b which the JIT recognize and generates optmized code for.

Since the JIT already has specialized codegen for 32 * 32 => 64bit division would it not make sense to just emit mulx there (or for normal multiplication as well) if it could lead to more efficient code (maybe when writing to memory or when the register usage gets better).

It seems clang uses mulx for 32 * 32 => 64bit in 32bit mode and 64 * 64 => 128bit in 64bit mode.

@tannergooding

Copy link
Copy Markdown
Member

I'm, personally, not a huge fan of a lot of the changes here. It's a lot of new code for what appears to be some relatively minor perf increases overall.

Where the perf increase is more measurable (specifically n-bit / 32-bit), it looks like places where we're missing some more general purpose JIT optimizations which would likely be a better overall investment, working across multiple hardware configurations and for more than just System.Decimal.

@Daniel-Svensson

Copy link
Copy Markdown
ContributorAuthor

@tannergooding I've opted into preview features for the file, hopefully it should compile again.

Hope it is ok to opt into preview features for the whole file instead of doing 3 pairs of suppress/restore

@tannergooding

Copy link
Copy Markdown
Member

Rerunning CI. Going to finish reviewing this today after CI finishes

@xtqqczze

Copy link
Copy Markdown
Contributor

@MihuBot

Comment on lines +228 to +233
Div3Word:
(bufNum.U2, remainder) = X86.X86Base.DivRem(bufNum.U2, remainder, den);
Div2Word:
(bufNum.U1, remainder) = X86.X86Base.DivRem(bufNum.U1, remainder, den);
Div1Word:
(bufNum.U0, remainder) = X86.X86Base.DivRem(bufNum.U0, remainder, den);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: we should really use a switch so a jump table can be generated rather than a series of labels

@tannergooding
tannergooding merged commit ca99c80 into dotnet:mainFeb 3, 2025
grendello added a commit to grendello/runtime that referenced this pull request Feb 4, 2025
* main: (30 commits)
JIT: Optimize bit-wise AND with a constant mask in combination with a left shift in a compare (dotnet#111979)
Change how we build the cross-OS DAC to support building in the VMR (dotnet#111927)
Add Windows Server 2025 to test configurations (dotnet#111938)
[PERF] Move performance testing YAML from dotnet/runtime to dotnet/performance (dotnet#111454)
arm64: Add support for Bitwise OR NOT & XOR NOT (dotnet#111893)
JIT: Fix cross crossgen comparison failures (dotnet#112078)
Bump `StyleCop.Analyzers` to `1.2.0-beta.556` (dotnet#111278)
Remove `RequiresProcessIsolation` on InterfaceFolding tests (dotnet#112098)
Use hardlinks in helixpublishwitharcade (dotnet#112091)
Update breaking change rules regarding byref/objref fields. (dotnet#112087)
[daccess] Do not use USE_DAC_TABLE_RVA on Apple platforms (dotnet#112076)
use collection syntax in illink (dotnet#108458)
Include PDB for all TfmRuntimeSpecificPackageFile (dotnet#111879)
[main] Update dependencies from dotnet/emsdk (dotnet#111690)
Enable Mono tests (dotnet#111981)
Let the debugger knows DATAS is on (dotnet#107115)
Tests ran counter (dotnet#111145)
Some System.Decimal performance improvements (dotnet#99212)
[mono][mini] Remove support for the Xamarin.iOS and Xamarin.Mac assemblies in the AOT compiler. (dotnet#108886)
Remove one usage of `Unsafe.AsPointer`. (dotnet#112079)
...
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Mar 6, 2025
@Daniel-Svensson
Daniel-Svensson deleted the deciaml_calc branch May 28, 2026 19:36
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants

@Daniel-Svensson@danmoseley@huoyaoyuan@jkotas@tannergooding@xtqqczze@EgorBo@hamarb123@MichalPetryka