From 68e0f0e0b0363827a6cd416938cb42e13139a616 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Sun, 29 Mar 2026 01:28:30 +0900 Subject: [PATCH 1/6] Use previous cache --- .../src/System/Number.BigInteger.cs | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index 5d5acec3f395bd..519a696c14e965 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -346,7 +346,7 @@ private static ParsingStatus NumberToBigInteger(ref NumberBuffer number, out Big // since the D&C algorithm reuses Multiply/Square/Divide on these spans. scoped Span base1E9; - ReadOnlySpan intDigits= number.Digits.Slice(0, Math.Min(number.Scale, number.DigitsCount)); + ReadOnlySpan intDigits = number.Digits.Slice(0, Math.Min(number.Scale, number.DigitsCount)); int intDigitsEnd = intDigits.IndexOf(0); if (intDigitsEnd < 0) { @@ -1111,38 +1111,39 @@ internal readonly ref struct PowersOf1e9 public const nuint TenPowMaxPartial = 1000000000; public const int MaxPartialDigits = 9; - private PowersOf1e9(nuint[] pow1E9) + private PowersOf1e9(ReadOnlySpan pow1E9) { this.pow1E9 = pow1E9; } public static PowersOf1e9 GetCached(int bufferLength) { + // Only cache buffers large enough to contain computed powers. + // Small buffers (≤ LeadingPowers1E9.Length) aren't populated by + // the constructor — it uses the static LeadingPowers1E9 directly. + if (bufferLength <= LeadingPowers1E9.Length) + { + return new PowersOf1e9(LeadingPowers1E9); + } + nuint[]? cached = s_cachedPowersOf1e9; if (cached is not null && cached.Length >= bufferLength) { return new PowersOf1e9(cached); } + Debug.Assert(cached is null || bufferLength > cached.Length); nuint[] buffer = new nuint[bufferLength]; - PowersOf1e9 result = new((Span)buffer); + Build(buffer, cached ?? LeadingPowers1E9); - // Only cache buffers large enough to contain computed powers. - // Small buffers (≤ LeadingPowers1E9.Length) aren't populated by - // the constructor — it uses the static LeadingPowers1E9 directly. - if (buffer.Length > LeadingPowers1E9.Length && - (cached is null || buffer.Length > cached.Length)) - { - // The write is safe without explicit memory barriers because: - // 1. The array is fully initialized before being stored. - // 2. On ARM64, the .NET GC write barrier uses stlr (store-release), - // providing release semantics for reference-type stores. - // 3. Readers have a data dependency (load reference -> access elements), - // providing natural acquire ordering on all architectures. - s_cachedPowersOf1e9 = buffer; - } - - return result; + // The write is safe without explicit memory barriers because: + // 1. The array is fully initialized before being stored. + // 2. On ARM64, the .NET GC write barrier uses stlr (store-release), + // providing release semantics for reference-type stores. + // 3. Readers have a data dependency (load reference -> access elements), + // providing natural acquire ordering on all architectures. + s_cachedPowersOf1e9 = buffer; + return new(buffer); } /// @@ -1319,30 +1320,32 @@ public static PowersOf1e9 GetCached(int bufferLength) 1892883497866839537, ]; - public PowersOf1e9(Span pow1E9) + private static void Build(Span buffer, ReadOnlySpan cached) { - Debug.Assert(pow1E9.Length >= 1); + Debug.Assert(buffer.Length > cached.Length); + Debug.Assert(cached.Length >= LeadingPowers1E9.Length); Debug.Assert(Indexes[6] == LeadingPowers1E9.Length); - if (pow1E9.Length <= LeadingPowers1E9.Length) + Debug.Assert(Indexes.Contains(buffer.Length - 1)); + + cached.CopyTo(buffer); + + int start = 6; + for (int i = 7; i < Indexes.Length && Indexes[i] <= cached.Length; i++) { - this.pow1E9 = LeadingPowers1E9; - return; + start = i; } - LeadingPowers1E9.CopyTo(pow1E9.Slice(0, LeadingPowers1E9.Length)); - this.pow1E9 = pow1E9; - - ReadOnlySpan src = pow1E9.Slice(Indexes[5], Indexes[6] - Indexes[5]); - int toExclusive = Indexes[6]; - for (int i = 6; i + 1 < Indexes.Length; i++) + ReadOnlySpan src = buffer.Slice(Indexes[start - 1], Indexes[start] - Indexes[start - 1]); + int toExclusive = Indexes[start]; + for (int i = start; i + 1 < Indexes.Length; i++) { Debug.Assert(2 * src.Length - (Indexes[i + 1] - Indexes[i]) is 0 or 1); - if (pow1E9.Length - toExclusive < (src.Length << 1)) + if (buffer.Length - toExclusive < (src.Length << 1)) { break; } - Span dst = pow1E9.Slice(toExclusive, src.Length << 1); + Span dst = buffer.Slice(toExclusive, src.Length << 1); BigIntegerCalculator.Square(src, dst); // When 9*(1<<(i-1)) is not evenly divisible by BitsPerLimb, the stored @@ -1358,8 +1361,8 @@ public PowersOf1e9(Span pow1E9) int from = toExclusive; toExclusive = Indexes[i + 1]; - src = pow1E9.Slice(from, toExclusive - from); - Debug.Assert(toExclusive == pow1E9.Length || pow1E9[toExclusive] == 0); + src = buffer.Slice(from, toExclusive - from); + Debug.Assert(toExclusive == buffer.Length || buffer[toExclusive] == 0); } } From 08a619385758e69bed82585ee50e66e2022439a7 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Sun, 29 Mar 2026 02:52:28 +0900 Subject: [PATCH 2/6] Fix comments --- .../src/System/Number.BigInteger.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index 519a696c14e965..f22cb30725f1ff 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -1136,20 +1136,18 @@ public static PowersOf1e9 GetCached(int bufferLength) nuint[] buffer = new nuint[bufferLength]; Build(buffer, cached ?? LeadingPowers1E9); - // The write is safe without explicit memory barriers because: - // 1. The array is fully initialized before being stored. - // 2. On ARM64, the .NET GC write barrier uses stlr (store-release), - // providing release semantics for reference-type stores. - // 3. Readers have a data dependency (load reference -> access elements), - // providing natural acquire ordering on all architectures. + // reftype field assignments have Store-Release semantics in .NET, so no volatile is needed. s_cachedPowersOf1e9 = buffer; return new(buffer); } /// /// Pre-calculated cumulative lengths into . - /// pow1E9[Indexes[i-1]..Indexes[i]] equals 1000000000^(1<<i). + /// pow1E9[Indexes[i]..Indexes[i+1]]<<(32*OmittedLength(i)) equals 1000000000^(1<<i). /// + /// + /// Satisfies the following relationship Indexes[i+1] - Indexes[i] == Math.Ceiling(Math.Log2(1000000000) * (1u<<i) / 32 - OmittedLength(i)). + /// private static ReadOnlySpan Indexes => nint.Size == 8 ? Indexes64 : Indexes32; private static ReadOnlySpan Indexes32 => @@ -1225,8 +1223,7 @@ public static PowersOf1e9 GetCached(int bufferLength) ]; /// - /// Pre-computed leading powers of 10^9 for small exponents. Entries up to - /// 1000000000^(1<<5) are stored directly because their low limb is never zero. + /// Pre-computed leading powers of 10^9 for small exponents. /// private static ReadOnlySpan LeadingPowers1E9 => nint.Size == 8 ? MemoryMarshal.Cast(LeadingPowers1E9_64) From 8551844c4d363437e84556933ccbc39a6ae3952b Mon Sep 17 00:00:00 2001 From: kzrnm Date: Sun, 29 Mar 2026 10:51:17 +0900 Subject: [PATCH 3/6] Fix comments --- .../System.Runtime.Numerics/src/System/Number.BigInteger.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index f22cb30725f1ff..1567bbd76a4552 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -1143,10 +1143,10 @@ public static PowersOf1e9 GetCached(int bufferLength) /// /// Pre-calculated cumulative lengths into . - /// pow1E9[Indexes[i]..Indexes[i+1]]<<(32*OmittedLength(i)) equals 1000000000^(1<<i). + /// pow1E9[Indexes[i]..Indexes[i+1]]<<(BitsPerLimb*OmittedLength(i)) equals 1000000000^(1<<i). /// /// - /// Satisfies the following relationship Indexes[i+1] - Indexes[i] == Math.Ceiling(Math.Log2(1000000000) * (1u<<i) / 32 - OmittedLength(i)). + /// Satisfies the following relationship Indexes[i+1] - Indexes[i] == Math.Ceiling(Math.Log2(1000000000) * (1u<<i) / BitsPerLimb - OmittedLength(i)). /// private static ReadOnlySpan Indexes => nint.Size == 8 ? Indexes64 : Indexes32; From 563cb9f782eef7744e73d7e4c79d292647b8458f Mon Sep 17 00:00:00 2001 From: kzrnm Date: Thu, 2 Apr 2026 04:56:10 +0900 Subject: [PATCH 4/6] Add cache test --- .../BigInteger/BigIntegerToStringTests.cs | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs index d54300593da263..318558505f166a 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Reflection; using System.Tests; using System.Text; using Xunit; @@ -874,7 +875,8 @@ private static string ExponentialFormatter(string input, int precision, NumberFo { temp[i] = '0'; i--; - }; + } + ; if (i > -1) { temp[i]++; @@ -1417,7 +1419,8 @@ private static string ScientificFormatter(string input, int precision, NumberFor { temp[i] = '0'; i--; - }; + } + ; if (i > -1) { temp[i]++; @@ -1492,7 +1495,8 @@ private static string SignedScientificFormatter(string input, int precision, Num { temp[i] = '0'; i--; - }; + } + ; if (i > -1) { temp[i]++; @@ -1913,7 +1917,8 @@ private static string ConvertToExp(string input, int places) { temp[i] = '0'; i--; - }; + } + ; if (i > -1) { temp[i]++; @@ -2133,6 +2138,33 @@ private static NumberFormatInfo MarkUp(NumberFormatInfo nfi) [Collection(nameof(DisableParallelization))] public class ToStringTestThreshold { + [Fact] + public static void PowerOfTenCacheTests() + { + FieldInfo field = typeof(BigInteger).Assembly.GetType("System.Number") + .GetField("s_cachedPowersOf1e9", BindingFlags.NonPublic | BindingFlags.Static); + + field.SetValue(null, null); + + (BigInteger.One << 99991).ToString(); + nuint[] cache99991 = (nuint[])field.GetValue(null); + Assert.NotNull(cache99991); + + (BigInteger.One << 20000).ToString(); + nuint[] cache20000 = (nuint[])field.GetValue(null); + Assert.Same(cache99991, cache20000); + + field.SetValue(null, null); + (BigInteger.One << 20000).ToString(); + cache20000 = (nuint[])field.GetValue(null); + Assert.True(cache20000.Length < cache99991.Length); + + (BigInteger.One << 99989).ToString(); + nuint[] cache99989 = (nuint[])field.GetValue(null); + Assert.Equal(cache99991, cache99989); + Assert.NotSame(cache99991, cache99989); + } + [Fact] public static void RunSimpleToStringTests() { From d39611b3e4a81ab3e5589c9e74ae8964cd1b50d4 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Sat, 4 Apr 2026 02:20:13 +0900 Subject: [PATCH 5/6] Remove unnecessary semicolons --- .../tests/BigInteger/BigIntegerToStringTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs index 318558505f166a..19c4c540cddab1 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/BigIntegerToStringTests.cs @@ -876,7 +876,7 @@ private static string ExponentialFormatter(string input, int precision, NumberFo temp[i] = '0'; i--; } - ; + if (i > -1) { temp[i]++; @@ -1420,7 +1420,7 @@ private static string ScientificFormatter(string input, int precision, NumberFor temp[i] = '0'; i--; } - ; + if (i > -1) { temp[i]++; @@ -1496,7 +1496,7 @@ private static string SignedScientificFormatter(string input, int precision, Num temp[i] = '0'; i--; } - ; + if (i > -1) { temp[i]++; @@ -1918,7 +1918,7 @@ private static string ConvertToExp(string input, int places) temp[i] = '0'; i--; } - ; + if (i > -1) { temp[i]++; From 6106d3c504561366654e54164f63af8b4654dd10 Mon Sep 17 00:00:00 2001 From: kzrnm Date: Sat, 4 Apr 2026 02:23:44 +0900 Subject: [PATCH 6/6] Copilot remarks --- .../System.Runtime.Numerics/src/System/Number.BigInteger.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index 1567bbd76a4552..2651b79a5d06e3 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -1146,7 +1146,8 @@ public static PowersOf1e9 GetCached(int bufferLength) /// pow1E9[Indexes[i]..Indexes[i+1]]<<(BitsPerLimb*OmittedLength(i)) equals 1000000000^(1<<i). /// /// - /// Satisfies the following relationship Indexes[i+1] - Indexes[i] == Math.Ceiling(Math.Log2(1000000000) * (1u<<i) / BitsPerLimb - OmittedLength(i)). + /// Satisfies the following relationship Indexes[i+1] - Indexes[i] == Math.Ceiling(Math.Log2(1000000000) * (1u<<i) / BitsPerLimb - OmittedLength(i)), + /// where BitsPerLimb is the number of bits in a single limb (sizeof(nuint) * 8, i.e. 32 on 32-bit platforms and 64 on 64-bit platforms). /// private static ReadOnlySpan Indexes => nint.Size == 8 ? Indexes64 : Indexes32;