Skip to content

Spannified internals of BigInteger - #35565

Merged
jeffhandley merged 99 commits into
dotnet:mainfrom
sakno:issue-22609
Oct 8, 2021
Merged

Spannified internals of BigInteger#35565
jeffhandley merged 99 commits into
dotnet:mainfrom
sakno:issue-22609

Conversation

@sakno

@saknosakno commented Apr 28, 2020

Copy link
Copy Markdown
Contributor

Proposed refactoring according with issue: #22609. The implementation plan consists of the following steps:

  • Replace unmanaged pointers with managed ones as well as remove unsafe code and pinning
  • Beautify stack allocation
  • Use spans wherever possible, especially for memory slicing
  • Simplify (or probably remove at all) BitsBuffer value type
  • Spannify FastReducer value type
  • Attempt to replace some array allocations with span slicing
  • Square, Multiply and bitwise operations use heap-based allocation of arrays if their length is greater than or equal to stack allocation threshold. Maybe replace it with array pooling using shared ArrayPool<T>?
  • BMI intrinsics (moved to separated issue BigInteger performance improvements #41495)

Spannified versions of internal and private static methods look pretty nice. However, I'm not sure about performance of passing span to the method. If RyuJIT uses scalar replacement then it's good news. Otherwise, maybe pass length and managed pointer to the first element as separate arguments to ensure that they passed through registers. This version was implemented in the first commit. I need advice here as well as preliminary code review because further work fully based on signatures of spannified methods.

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @tannergooding
Notify danmosemsft if you want to be subscribed.

@gfoidlgfoidl left a comment

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.

You can get rid of some Unsafe.Add usage by letting the JIT do the work and have the safety back.
(Didn't comment on all places, please do one pass of clean-up.)

Plus some nits.

Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
@sakno

Copy link
Copy Markdown
ContributorAuthor

@gfoidl , could you please take a look at the next iteration (commit a1e5006). I tried to reduce memory allocation for bitwise operations. But I'm not satisfied with necessity of delegate type that is introduced to reduce code duplication. Moreover, this approach leads to delegate instance allocation on every call. AFAIK, the current version of C# still doesn't support method pointers.

ToUInt32Array is renamed to CopyTo and now working with the memory allocated by the immediate caller.

Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
Comment threadsrc/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Outdated
@danmoseley

Copy link
Copy Markdown
Contributor

@GrabYourPitchforks sounds good

@danmoseley

Copy link
Copy Markdown
Contributor

@sakno I haven't used it myself, but I believe you can run this on the before and after results to get a better comparison:
https://github.com/dotnet/performance/blob/main/src/tools/ResultsComparer/README.md
Hopefully it looks at allocations as well.

@stephentoub

stephentoub commented Aug 26, 2021

Copy link
Copy Markdown
Member

I believe you can run this on the before and after results to get a better comparison

I just add this to my dotnet run benchmarkdotnet cmd line:

--corerun d:\coreclrtest\main\corerun.exe d:\coreclrtest\pr\corerun.exe

where those main and pr folders contain a copy of the contents of D:\repos\runtime\artifacts\bin\testhost\net6.0-windows-Release-x64\shared\Microsoft.NETCore.App\7.0.0 before and after the change.

@danmoseley

Copy link
Copy Markdown
Contributor

Just checking in on this one since it's so old, seems like the next action is @GrabYourPitchforks offered to have another look through. And @sakno if you are interested you could try the trick above to get a single table.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

I had something come up which will make me busy for the next few days, but I can continue driving this once my schedule frees up. Should we un-milestone from 6.0 then?

@danmoseleydanmoseley modified the milestones: 6.0.0, 7.0.0Aug 30, 2021
@danmoseley

Copy link
Copy Markdown
Contributor

Yes, they won't take this change into 6.0.

@sakno

sakno commented Sep 6, 2021

Copy link
Copy Markdown
ContributorAuthor
MethodBranchnumberStringargumentsMeanErrorStdDevMedianMinMaxRatioRatioSDGen 0Allocated
Ctor_ByteArraymain-2147483648?13.954 ns0.1019 ns0.0851 ns13.941 ns13.819 ns14.118 ns1.000.00--
Ctor_ByteArrayPR-2147483648?14.761 ns0.0901 ns0.0799 ns14.760 ns14.580 ns14.865 ns1.060.01--
ToByteArraymain-2147483648?20.727 ns0.7729 ns0.8900 ns20.618 ns19.637 ns22.860 ns1.000.000.010132 B
ToByteArrayPR-2147483648?20.204 ns0.3181 ns0.2975 ns20.061 ns19.861 ns20.859 ns0.990.040.010232 B
Parsemain-2147483648?194.382 ns4.7403 ns5.4589 ns192.446 ns189.305 ns209.048 ns1.000.000.0427136 B
ParsePR-2147483648?185.266 ns0.9011 ns0.7525 ns184.929 ns184.252 ns186.515 ns0.940.030.0426136 B
ToStringXmain-2147483648?80.635 ns3.3895 ns3.7674 ns78.761 ns75.194 ns88.298 ns1.000.000.012640 B
ToStringXPR-2147483648?72.292 ns0.6056 ns0.5369 ns72.084 ns71.747 ns73.678 ns0.890.050.012640 B
ToStringDmain-2147483648?76.453 ns0.8156 ns0.7230 ns76.179 ns75.101 ns77.658 ns1.000.000.0481152 B
ToStringDPR-2147483648?71.113 ns0.2899 ns0.2712 ns71.067 ns70.690 ns71.631 ns0.930.010.0484152 B
Addmain?1024,1024 bits56.347 ns1.1633 ns1.1946 ns55.965 ns55.397 ns59.876 ns1.000.000.0510160 B
AddPR?1024,1024 bits56.439 ns0.2406 ns0.2009 ns56.427 ns56.043 ns56.775 ns1.000.020.0509160 B
Subtractmain?1024,1024 bits57.248 ns0.3050 ns0.2853 ns57.254 ns56.701 ns57.831 ns1.000.000.0482152 B
SubtractPR?1024,1024 bits57.372 ns0.5303 ns0.4960 ns57.226 ns56.800 ns58.195 ns1.000.010.0484152 B
Multiplymain?1024,1024 bits1,018.982 ns5.7862 ns5.4124 ns1,017.372 ns1,010.925 ns1,029.580 ns1.000.000.0857280 B
MultiplyPR?1024,1024 bits1,070.928 ns4.9263 ns4.6081 ns1,070.226 ns1,064.082 ns1,078.841 ns1.050.010.0859280 B
GreatestCommonDivisormain?1024,1024 bits10,890.242 ns76.4818 ns67.7992 ns10,885.738 ns10,799.565 ns10,999.333 ns1.000.000.0870304 B
GreatestCommonDivisorPR?1024,1024 bits11,022.761 ns86.6214 ns76.7876 ns10,992.526 ns10,948.845 ns11,209.706 ns1.010.01--
ModPowmain?1024,1024,64 bits176,924.900 ns1,493.7503 ns1,247.3492 ns177,199.311 ns174,384.102 ns178,660.212 ns1.000.00-305 B
ModPowPR?1024,1024,64 bits167,633.288 ns803.3346 ns627.1908 ns167,786.488 ns166,646.389 ns168,750.669 ns0.950.01-33 B
Dividemain?1024,512 bits642.343 ns4.2045 ns3.7272 ns642.788 ns636.328 ns648.605 ns1.000.000.028696 B
DividePR?1024,512 bits662.570 ns7.4316 ns6.2057 ns661.077 ns656.037 ns678.460 ns1.030.010.029196 B
Remaindermain?1024,512 bits701.984 ns3.9658 ns3.7096 ns701.717 ns696.460 ns708.885 ns1.000.000.0748240 B
RemainderPR?1024,512 bits657.863 ns3.2204 ns3.0123 ns657.702 ns652.353 ns663.535 ns0.940.000.026488 B
Ctor_ByteArraymain123?9.109 ns0.1340 ns0.1253 ns9.082 ns8.944 ns9.307 ns1.000.00--
Ctor_ByteArrayPR123?8.949 ns0.0736 ns0.0688 ns8.951 ns8.853 ns9.075 ns0.980.02--
ToByteArraymain123?16.474 ns0.1133 ns0.1060 ns16.497 ns16.320 ns16.688 ns1.000.000.010132 B
ToByteArrayPR123?16.809 ns0.2037 ns0.1905 ns16.770 ns16.528 ns17.156 ns1.020.010.010232 B
Parsemain123?125.084 ns1.7400 ns1.5425 ns125.059 ns123.403 ns129.047 ns1.000.000.0328104 B
ParsePR123?125.753 ns1.0728 ns0.9510 ns125.476 ns124.542 ns127.676 ns1.010.020.0328104 B
ToStringXmain123?60.686 ns0.4545 ns0.4251 ns60.796 ns59.957 ns61.458 ns1.000.000.010032 B
ToStringXPR123?61.624 ns0.3370 ns0.3153 ns61.519 ns61.216 ns62.294 ns1.020.010.010032 B
ToStringDmain123?43.681 ns0.1716 ns0.1521 ns43.717 ns43.313 ns43.854 ns1.000.000.010132 B
ToStringDPR123?45.596 ns0.3587 ns0.3180 ns45.640 ns45.087 ns46.047 ns1.040.010.010132 B
Ctor_ByteArraymain123456789012(...)901234567890 [200]?144.698 ns1.3993 ns1.3089 ns144.616 ns142.887 ns147.656 ns1.000.000.0352112 B
Ctor_ByteArrayPR123456789012(...)901234567890 [200]?143.912 ns0.6425 ns0.6010 ns144.048 ns143.003 ns144.906 ns0.990.010.0355112 B
ToByteArraymain123456789012(...)901234567890 [200]?69.283 ns0.5334 ns0.4454 ns69.176 ns68.703 ns70.031 ns1.000.000.0356112 B
ToByteArrayPR123456789012(...)901234567890 [200]?70.821 ns0.3920 ns0.3667 ns70.853 ns70.167 ns71.335 ns1.020.010.0357112 B
Parsemain123456789012(...)901234567890 [200]?1,743.566 ns7.1283 ns5.9525 ns1,742.759 ns1,734.400 ns1,755.052 ns1.000.000.3071984 B
ParsePR123456789012(...)901234567890 [200]?1,779.592 ns8.3089 ns7.3656 ns1,779.426 ns1,770.266 ns1,794.882 ns1.020.000.3131984 B
ToStringXmain123456789012(...)901234567890 [200]?464.817 ns2.9414 ns2.6075 ns464.115 ns461.699 ns469.537 ns1.000.000.1137360 B
ToStringXPR123456789012(...)901234567890 [200]?456.938 ns2.6321 ns2.4621 ns456.267 ns453.956 ns461.539 ns0.980.010.1135360 B
ToStringDmain123456789012(...)901234567890 [200]?1,083.860 ns6.4113 ns5.6835 ns1,083.582 ns1,072.836 ns1,094.347 ns1.000.000.3151992 B
ToStringDPR123456789012(...)901234567890 [200]?1,041.487 ns9.7771 ns8.6671 ns1,038.352 ns1,032.884 ns1,058.836 ns0.960.010.3155992 B
Addmain?16,16 bits11.229 ns0.0470 ns0.0393 ns11.229 ns11.162 ns11.305 ns1.000.00--
AddPR?16,16 bits7.047 ns0.0570 ns0.0506 ns7.045 ns6.973 ns7.149 ns0.630.00--
Subtractmain?16,16 bits11.216 ns0.0828 ns0.0774 ns11.204 ns11.092 ns11.341 ns1.000.00--
SubtractPR?16,16 bits6.494 ns0.0715 ns0.0634 ns6.489 ns6.424 ns6.664 ns0.580.01--
Multiplymain?16,16 bits10.973 ns0.1227 ns0.1147 ns10.972 ns10.802 ns11.209 ns1.000.00--
MultiplyPR?16,16 bits7.742 ns0.1315 ns0.1230 ns7.731 ns7.602 ns8.052 ns0.710.01--
GreatestCommonDivisormain?16,16 bits76.718 ns0.4069 ns0.3607 ns76.750 ns76.264 ns77.468 ns1.000.00--
GreatestCommonDivisorPR?16,16 bits76.764 ns0.4106 ns0.3640 ns76.640 ns76.164 ns77.423 ns1.000.01--
ModPowmain?16,16,16 bits179.341 ns0.7725 ns0.7226 ns179.333 ns177.896 ns180.873 ns1.000.00--
ModPowPR?16,16,16 bits200.235 ns1.0669 ns0.9458 ns200.308 ns198.739 ns202.131 ns1.120.01--
Dividemain?16,8 bits9.324 ns0.0411 ns0.0343 ns9.323 ns9.277 ns9.385 ns1.000.00--
DividePR?16,8 bits10.364 ns0.0747 ns0.0662 ns10.370 ns10.247 ns10.496 ns1.110.01--
Remaindermain?16,8 bits9.433 ns0.0795 ns0.0705 ns9.420 ns9.358 ns9.602 ns1.000.00--
RemainderPR?16,8 bits10.091 ns0.0647 ns0.0605 ns10.081 ns9.978 ns10.178 ns1.070.01--
ModPowmain?16384,16384,64 bits2,982,284.055 ns20,304.5000 ns18,992.8419 ns2,981,140.681 ns2,951,121.619 ns3,024,813.569 ns1.000.00-2,236 B
ModPowPR?16384,16384,64 bits3,149,539.852 ns52,327.1029 ns48,946.8045 ns3,144,966.418 ns3,064,405.190 ns3,231,942.696 ns1.060.02-47 B
Dividemain?65536,32768 bits5,211,368.132 ns20,965.3161 ns18,585.2067 ns5,207,763.708 ns5,186,675.948 ns5,253,423.531 ns1.000.00-12,363 B
DividePR?65536,32768 bits5,469,820.721 ns23,984.5013 ns22,435.1174 ns5,474,290.333 ns5,426,964.667 ns5,501,347.646 ns1.050.01-4,160 B
Remaindermain?65536,32768 bits5,274,423.949 ns40,466.6923 ns35,872.6687 ns5,260,983.656 ns5,228,020.708 ns5,351,705.146 ns1.000.00-12,355 B
RemainderPR?65536,32768 bits5,327,585.356 ns21,686.8215 ns20,285.8663 ns5,329,391.406 ns5,293,440.948 ns5,360,807.240 ns1.010.01-4,146 B
Addmain?65536,65536 bits2,261.169 ns17.0800 ns15.1409 ns2,261.419 ns2,236.812 ns2,289.504 ns1.000.002.61328,224 B
AddPR?65536,65536 bits2,474.616 ns12.8627 ns11.4024 ns2,476.329 ns2,454.378 ns2,490.396 ns1.090.012.61638,224 B
Subtractmain?65536,65536 bits2,298.021 ns25.6683 ns22.7543 ns2,294.305 ns2,268.096 ns2,338.891 ns1.000.002.61418,216 B
SubtractPR?65536,65536 bits2,523.440 ns28.7476 ns24.0056 ns2,519.823 ns2,496.089 ns2,586.340 ns1.100.012.61458,216 B
Multiplymain?65536,65536 bits927,272.318 ns18,900.2201 ns21,765.5216 ns928,409.371 ns902,183.483 ns974,650.134 ns1.000.0047.7941153,523 B
MultiplyPR?65536,65536 bits1,004,700.576 ns3,988.3908 ns3,730.7432 ns1,004,817.846 ns998,809.113 ns1,011,070.479 ns1.080.024.166716,414 B
GreatestCommonDivisormain?65536,65536 bits5,591,038.422 ns34,612.6535 ns32,376.6975 ns5,589,624.021 ns5,530,768.417 ns5,649,730.417 ns1.000.00-16,451 B
GreatestCommonDivisorPR?65536,65536 bits6,993,940.330 ns78,301.4450 ns73,243.2202 ns6,972,483.861 ns6,905,103.250 ns7,155,806.472 ns1.250.02-32 B

@jeffhandley

Copy link
Copy Markdown
Member

@sakno Thanks for sharing those performance numbers. We're finally down to the final stages of getting this merged!

@GrabYourPitchforks, @bartonjs, @tannergooding, and I all spent time reviewing the performance results, looking over the changes again, and discussing with each other what the risks and benefits are with these changes. With such a large refactoring, I needed to ensure we're looking at this from all angles, including potential future improvements. We've concluded that the improvements you've made here do indeed put us in a better position overall. Thank you!

I've asked @GrabYourPitchforks to resolve the 2 conflicts that exist right now; he'll do that later this week and push directly to the PR's branch. After that, assuming the CI shows up as green, we'll finally merge this in. These changes will then be part of .NET 7.0 Preview 1.

Thank you so much for your persistence and all of your effort on this, @sakno! This is a significant contribution to .NET.

@sakno

sakno commented Oct 6, 2021

Copy link
Copy Markdown
ContributorAuthor

@jeffhandley , @GrabYourPitchforks , thanks for the feedback! I can resolve the conflict by myself without any problem if you want. However, I see the usage of index expression (xd[^1]) that was previously proposed by Tanner and then rejected by Stephen. If we decided to use index expressions then we need to use it everywhere across BigInteger code base. Otherwise, it should be transformed to xd[xd.Length - 1].

@stephentoub

Copy link
Copy Markdown
Member

However, I see the usage of index expression (xd[^1]) that was previously proposed by Tanner and then rejected by Stephen.

Where did I reject it?

@sakno

sakno commented Oct 6, 2021

Copy link
Copy Markdown
ContributorAuthor

@stephentoub , here is reverting PR: #57297. It was also applied to this PR.
Comment by @tannergooding

Did some brief additional investigation and it looks like the only changes that really need to be reverted are the ones using C# indexing expressions.

The motivation was:

But to your specific question: yes, consider removing range expressions. We don't use them frequently within the base libraries because they tend to interfere with assembly trimming.

@stephentoub

stephentoub commented Oct 6, 2021

Copy link
Copy Markdown
Member

Right, that wasn't me rejecting their usage, that was their usage in those cases showing to regress performance.

@jeffhandley

Copy link
Copy Markdown
Member

@sakno Thanks! We'd happily take you up on the offer to resolve the 2 conflicts. And good catch on the range expressions having snuckmade it in. In your conflict resolution, let's change those instances back to xd[xd.Length - 1].

@sakno

sakno commented Oct 7, 2021

Copy link
Copy Markdown
ContributorAuthor

@jeffhandley , done.

@GrabYourPitchforksGrabYourPitchforks left a comment

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.

We missed one thing in earlier reviews, but otherwise commit 65485fa LGTM! 🥳

uint divLo = right.Length > 1 ? right[right.Length - 2] : 0;

// We measure the leading zeros of the divisor
int shift = LeadingZeros(divHi);

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.

Cleanup opportunity we didn't spot earlier: get rid of this LeadingZeros method and prefer BitOperations.LeadingZeroCount instead.

@GrabYourPitchforks

Copy link
Copy Markdown
Member

@sakno We're at the final step! If you want to address #35565 (comment) (it's just deleting some dead code) as part of this PR, please feel free to do so, otherwise we can tackle it in a follow-up PR. Whatever you decide, I think we're good to merge right after we kick CI a bit.

@jeffhandley

Copy link
Copy Markdown
Member

The test failure is #60119, which was fixed in #60140.

@sakno

sakno commented Oct 8, 2021

Copy link
Copy Markdown
ContributorAuthor

@GrabYourPitchforks , we have one more potential cleanup in BigIntegerCalculator.Utils file. Here is public static int Compare(ReadOnlySpan<uint> left, ReadOnlySpan<uint> right) method. It can be replaced with MemoryExtensions.SequenceCompareTo method. It uses IComparable<T>.CompareTo constrained call to compare elements. However, the span element is of type unit but I expect that JIT will be able to do inlining for CompareTo implementation

Oh sorry, not applicable. SequenceCompareTo starts from zero element, which is not suitable for BigInteger purposes.

Also, ActualLength can be replaced with MemoryExtensions.TrimEnd everywhere.

@jeffhandleyjeffhandley left a comment

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.

Thank you, @sakno! 💯

@jeffhandley
jeffhandley merged commit 22b516c into dotnet:mainOct 8, 2021
@ghostghost locked as resolved and limited conversation to collaborators Nov 7, 2021
@dakersnar

Copy link
Copy Markdown
Contributor

@sakno I'm skimming these changes trying to determine if this PR is the cause of #70330. If you happen to have any insight, let me know.

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

12 participants

@sakno@danmoseley@stephentoub@tannergooding@jkotas@jeffhandley@GrabYourPitchforks@dakersnar@IDisposable@gfoidl@terrajobst@Dotnet-GitSync-Bot