Skip to content

Commit 5892ef2

Browse files
authored
Improve performance of integer formatting (#76726)
* Improve performance of integer formatting 1. The DivRem(..., 10) for each digit in the number ends up being the most expensive aspect of formatting. This employs a trick other formatting libraries use, which is to have a table for all the formatted values between 00 and 99, and to then DivRem(..., 100) to cut the number of operations in half, which for longer values is meaningful. 2. Avoids going through the digit counting path when we know at the call site it won't be needed. 3. Employs a branch-free, table-based lookup for CountDigits(ulong) to go with a similar approach added for uint. * Address PR feedback (move license notice)
1 parent daa88f6 commit 5892ef2

File tree

4 files changed

+324
-141
lines changed

4 files changed

+324
-141
lines changed

THIRD-PARTY-NOTICES.TXT

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,3 +1143,35 @@ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABIL
11431143
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
11441144
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
11451145
POSSIBILITY OF SUCH DAMAGE.
1146+
1147+
License notice for fmtlib/fmt
1148+
-------------------------------
1149+
1150+
Formatting library for C++
1151+
1152+
Copyright (c) 2012 - present, Victor Zverovich
1153+
1154+
Permission is hereby granted, free of charge, to any person obtaining
1155+
a copy of this software and associated documentation files (the
1156+
"Software"), to deal in the Software without restriction, including
1157+
without limitation the rights to use, copy, modify, merge, publish,
1158+
distribute, sublicense, and/or sell copies of the Software, and to
1159+
permit persons to whom the Software is furnished to do so, subject to
1160+
the following conditions:
1161+
1162+
The above copyright notice and this permission notice shall be
1163+
included in all copies or substantial portions of the Software.
1164+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
1165+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1166+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
1167+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
1168+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
1169+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
1170+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1171+
1172+
--- Optional exception to the license ---
1173+
1174+
As an exception, if, as a result of your compiling your source code, portions
1175+
of this Software are embedded into a machine-executable object form of such
1176+
source code, you may redistribute such embedded portions in such object form
1177+
without including the above copyright and permission notices.

src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,60 +52,58 @@ public static int CountDigits(UInt128 value)
5252
return digits;
5353
}
5454

55+
// Based on do_count_digits from https://github.com/fmtlib/fmt/blob/662adf4f33346ba9aba8b072194e319869ede54a/include/fmt/format.h#L1124
5556
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5657
public static int CountDigits(ulong value)
5758
{
58-
int digits = 1;
59-
uint part;
60-
if (value >= 10000000)
59+
// Map the log2(value) to a power of 10.
60+
ReadOnlySpan<byte> log2ToPow10 = new byte[]
6161
{
62-
if (value >= 100000000000000)
63-
{
64-
part = (uint)(value / 100000000000000);
65-
digits += 14;
66-
}
67-
else
68-
{
69-
part = (uint)(value / 10000000);
70-
digits += 7;
71-
}
72-
}
73-
else
74-
{
75-
part = (uint)value;
76-
}
62+
1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
63+
6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
64+
10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
65+
15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20
66+
};
67+
Debug.Assert(log2ToPow10.Length == 64);
68+
uint index = Unsafe.Add(ref MemoryMarshal.GetReference(log2ToPow10), BitOperations.Log2(value));
7769

78-
if (part < 10)
79-
{
80-
// no-op
81-
}
82-
else if (part < 100)
70+
// TODO https://github.com/dotnet/runtime/issues/60948: Use ReadOnlySpan<ulong> instead of ReadOnlySpan<byte>.
71+
// Read the associated power of 10.
72+
ReadOnlySpan<byte> powersOf10 = new byte[]
8373
{
84-
digits++;
85-
}
86-
else if (part < 1000)
87-
{
88-
digits += 2;
89-
}
90-
else if (part < 10000)
91-
{
92-
digits += 3;
93-
}
94-
else if (part < 100000)
95-
{
96-
digits += 4;
97-
}
98-
else if (part < 1000000)
99-
{
100-
digits += 5;
101-
}
102-
else
74+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // unused entry to avoid needing to subtract
75+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0
76+
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10
77+
0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 100
78+
0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1000
79+
0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10000
80+
0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // 100000
81+
0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, // 1000000
82+
0x80, 0x96, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, // 10000000
83+
0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00, // 100000000
84+
0x00, 0xCA, 0x9A, 0x3B, 0x00, 0x00, 0x00, 0x00, // 1000000000
85+
0x00, 0xE4, 0x0B, 0x54, 0x02, 0x00, 0x00, 0x00, // 10000000000
86+
0x00, 0xE8, 0x76, 0x48, 0x17, 0x00, 0x00, 0x00, // 100000000000
87+
0x00, 0x10, 0xA5, 0xD4, 0xE8, 0x00, 0x00, 0x00, // 1000000000000
88+
0x00, 0xA0, 0x72, 0x4E, 0x18, 0x09, 0x00, 0x00, // 10000000000000
89+
0x00, 0x40, 0x7A, 0x10, 0xF3, 0x5A, 0x00, 0x00, // 100000000000000
90+
0x00, 0x80, 0xC6, 0xA4, 0x7E, 0x8D, 0x03, 0x00, // 1000000000000000
91+
0x00, 0x00, 0xC1, 0x6F, 0xF2, 0x86, 0x23, 0x00, // 10000000000000000
92+
0x00, 0x00, 0x8A, 0x5D, 0x78, 0x45, 0x63, 0x01, // 100000000000000000
93+
0x00, 0x00, 0x64, 0xA7, 0xB3, 0xB6, 0xE0, 0x0D, // 1000000000000000000
94+
0x00, 0x00, 0xE8, 0x89, 0x04, 0x23, 0xC7, 0x8A, // 10000000000000000000
95+
};
96+
Debug.Assert((index + 1) * sizeof(ulong) <= powersOf10.Length);
97+
ulong powerOf10 = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref MemoryMarshal.GetReference(powersOf10), index * sizeof(ulong)));
98+
if (!BitConverter.IsLittleEndian)
10399
{
104-
Debug.Assert(part < 10000000);
105-
digits += 6;
100+
powerOf10 = BinaryPrimitives.ReverseEndianness(powerOf10);
106101
}
107102

108-
return digits;
103+
// Return the number of digits based on the power of 10, shifted by 1
104+
// if it falls below the threshold.
105+
bool lessThan = value < powerOf10;
106+
return (int)(index - Unsafe.As<bool, byte>(ref lessThan)); // while arbitrary bools may be non-0/1, comparison operators are expected to return 0/1
109107
}
110108

111109
[MethodImpl(MethodImplOptions.AggressiveInlining)]

0 commit comments

Comments
 (0)