Skip to content

Improve StringBuilder.Equals - #133167

Open
prozolic wants to merge 2 commits into
dotnet:mainfrom
prozolic:stringbuilder-equals
Open

Improve StringBuilder.Equals#133167
prozolic wants to merge 2 commits into
dotnet:mainfrom
prozolic:stringbuilder-equals

Conversation

@prozolic

@prozolic prozolic commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Compare the overlapping portions of each chunk from the end using ReadOnlySpan<char>.SequenceEqual instead of walking the chunk arrays one character at a time.
The approach is based on the logic used in StringBuilder.Equals(ReadOnlySpan<char>).

Benchmark

The results of the following benchmarks on local environment, run on the main and pr branches, are summarized below.
In the cases where Size = 1 and Size = 4, some overhead occurs. In many cases, performance improves when the Size is 16 or higher.
Sorry for posting so many benchmarks.

Benchmark result
Method Toolchain Size Mean Error Ratio
Equals_SameInstance \main\corerun.exe 1 1.1135 ns 0.0211 ns 1.00
Equals_SameInstance \pr\corerun.exe 1 0.9777 ns 0.0172 ns 0.88
Equals_SingleChunk \main\corerun.exe 1 3.6104 ns 0.1526 ns 1.00
Equals_SingleChunk \pr\corerun.exe 1 3.2094 ns 0.0819 ns 0.89
Equals_MultipleChunks_SameLayout \main\corerun.exe 1 3.5166 ns 0.0776 ns 1.00
Equals_MultipleChunks_SameLayout \pr\corerun.exe 1 3.3110 ns 0.2230 ns 0.94
Equals_MultipleChunks_ShiftedLayout \main\corerun.exe 1 3.4346 ns 0.1099 ns 1.00
Equals_MultipleChunks_ShiftedLayout \pr\corerun.exe 1 3.2851 ns 0.0615 ns 0.96
Equals_SingleChunkVsMultipleChunks \main\corerun.exe 1 3.6614 ns 0.3163 ns 1.00
Equals_SingleChunkVsMultipleChunks \pr\corerun.exe 1 3.2029 ns 0.0882 ns 0.88
Equals_ManyChunks_SameLayout \main\corerun.exe 1 3.3569 ns 0.1235 ns 1.00
Equals_ManyChunks_SameLayout \pr\corerun.exe 1 3.2865 ns 0.0815 ns 0.98
Equals_ManyChunks_ShiftedLayout \main\corerun.exe 1 3.2851 ns 0.1304 ns 1.00
Equals_ManyChunks_ShiftedLayout \pr\corerun.exe 1 3.3869 ns 0.0520 ns 1.03
NotEquals_SingleChunk_DifferentAtStart \main\corerun.exe 1 1.7665 ns 0.0406 ns 1.00
NotEquals_SingleChunk_DifferentAtStart \pr\corerun.exe 1 2.1042 ns 0.0686 ns 1.19
NotEquals_SingleChunk_DifferentAtMiddle \main\corerun.exe 1 1.7946 ns 0.0237 ns 1.00
NotEquals_SingleChunk_DifferentAtMiddle \pr\corerun.exe 1 2.1242 ns 0.0396 ns 1.18
NotEquals_SingleChunk_DifferentAtEnd \main\corerun.exe 1 2.0014 ns 0.0112 ns 1.00
NotEquals_SingleChunk_DifferentAtEnd \pr\corerun.exe 1 2.0237 ns 0.0937 ns 1.01
NotEquals_MultipleChunks_DifferentAtStart \main\corerun.exe 1 1.9653 ns 0.0620 ns 1.00
NotEquals_MultipleChunks_DifferentAtStart \pr\corerun.exe 1 2.1096 ns 0.0754 ns 1.07
NotEquals_MultipleChunks_DifferentAtMiddle \main\corerun.exe 1 1.9902 ns 0.0587 ns 1.00
NotEquals_MultipleChunks_DifferentAtMiddle \pr\corerun.exe 1 1.8607 ns 0.0627 ns 0.94
NotEquals_MultipleChunks_DifferentAtEnd \main\corerun.exe 1 1.9482 ns 0.0572 ns 1.00
NotEquals_MultipleChunks_DifferentAtEnd \pr\corerun.exe 1 2.0247 ns 0.0649 ns 1.04
NotEquals_ManyChunks_DifferentAtStart \main\corerun.exe 1 1.9696 ns 0.0559 ns 1.00
NotEquals_ManyChunks_DifferentAtStart \pr\corerun.exe 1 2.0436 ns 0.1042 ns 1.04
NotEquals_ManyChunks_DifferentAtMiddle \main\corerun.exe 1 1.9892 ns 0.0473 ns 1.00
NotEquals_ManyChunks_DifferentAtMiddle \pr\corerun.exe 1 2.1088 ns 0.0124 ns 1.06
NotEquals_ManyChunks_DifferentAtEnd \main\corerun.exe 1 1.8266 ns 0.0651 ns 1.00
NotEquals_ManyChunks_DifferentAtEnd \pr\corerun.exe 1 1.9836 ns 0.0606 ns 1.09
Equals_SameInstance \main\corerun.exe 4 1.0170 ns 0.0541 ns 1.00
Equals_SameInstance \pr\corerun.exe 4 1.0052 ns 0.0190 ns 0.99
Equals_SingleChunk \main\corerun.exe 4 6.0236 ns 0.1178 ns 1.00
Equals_SingleChunk \pr\corerun.exe 4 7.9353 ns 0.7966 ns 1.32
Equals_MultipleChunks_SameLayout \main\corerun.exe 4 6.0880 ns 0.1337 ns 1.00
Equals_MultipleChunks_SameLayout \pr\corerun.exe 4 7.1562 ns 1.4019 ns 1.18
Equals_MultipleChunks_ShiftedLayout \main\corerun.exe 4 5.9449 ns 0.1020 ns 1.00
Equals_MultipleChunks_ShiftedLayout \pr\corerun.exe 4 7.5351 ns 0.7333 ns 1.27
Equals_SingleChunkVsMultipleChunks \main\corerun.exe 4 6.1473 ns 0.2202 ns 1.00
Equals_SingleChunkVsMultipleChunks \pr\corerun.exe 4 7.4717 ns 0.6593 ns 1.22
Equals_ManyChunks_SameLayout \main\corerun.exe 4 6.3051 ns 0.0897 ns 1.00
Equals_ManyChunks_SameLayout \pr\corerun.exe 4 7.9799 ns 1.5221 ns 1.27
Equals_ManyChunks_ShiftedLayout \main\corerun.exe 4 5.4386 ns 0.1698 ns 1.00
Equals_ManyChunks_ShiftedLayout \pr\corerun.exe 4 3.9671 ns 0.5840 ns 0.73
NotEquals_SingleChunk_DifferentAtStart \main\corerun.exe 4 4.7061 ns 0.2117 ns 1.00
NotEquals_SingleChunk_DifferentAtStart \pr\corerun.exe 4 3.2681 ns 0.0608 ns 0.70
NotEquals_SingleChunk_DifferentAtMiddle \main\corerun.exe 4 2.9259 ns 0.0313 ns 1.00
NotEquals_SingleChunk_DifferentAtMiddle \pr\corerun.exe 4 3.1548 ns 0.0555 ns 1.08
NotEquals_SingleChunk_DifferentAtEnd \main\corerun.exe 4 2.0035 ns 0.0250 ns 1.00
NotEquals_SingleChunk_DifferentAtEnd \pr\corerun.exe 4 2.0660 ns 0.0799 ns 1.03
NotEquals_MultipleChunks_DifferentAtStart \main\corerun.exe 4 4.6624 ns 0.2060 ns 1.00
NotEquals_MultipleChunks_DifferentAtStart \pr\corerun.exe 4 3.4846 ns 0.0299 ns 0.75
NotEquals_MultipleChunks_DifferentAtMiddle \main\corerun.exe 4 2.9007 ns 0.1027 ns 1.00
NotEquals_MultipleChunks_DifferentAtMiddle \pr\corerun.exe 4 3.2666 ns 0.0936 ns 1.13
NotEquals_MultipleChunks_DifferentAtEnd \main\corerun.exe 4 1.9846 ns 0.0387 ns 1.00
NotEquals_MultipleChunks_DifferentAtEnd \pr\corerun.exe 4 2.1341 ns 0.0223 ns 1.08
NotEquals_ManyChunks_DifferentAtStart \main\corerun.exe 4 4.6724 ns 0.1179 ns 1.00
NotEquals_ManyChunks_DifferentAtStart \pr\corerun.exe 4 3.6270 ns 0.2671 ns 0.78
NotEquals_ManyChunks_DifferentAtMiddle \main\corerun.exe 4 2.8853 ns 0.0613 ns 1.00
NotEquals_ManyChunks_DifferentAtMiddle \pr\corerun.exe 4 3.4713 ns 0.1157 ns 1.20
NotEquals_ManyChunks_DifferentAtEnd \main\corerun.exe 4 1.9963 ns 0.0252 ns 1.00
NotEquals_ManyChunks_DifferentAtEnd \pr\corerun.exe 4 2.0817 ns 0.0409 ns 1.04
Equals_SameInstance \main\corerun.exe 16 0.9988 ns 0.0103 ns 1.00
Equals_SameInstance \pr\corerun.exe 16 0.9862 ns 0.0169 ns 0.99
Equals_SingleChunk \main\corerun.exe 16 16.5071 ns 0.1784 ns 1.00
Equals_SingleChunk \pr\corerun.exe 16 3.5522 ns 0.0652 ns 0.22
Equals_MultipleChunks_SameLayout \main\corerun.exe 16 16.7270 ns 0.3229 ns 1.00
Equals_MultipleChunks_SameLayout \pr\corerun.exe 16 3.3045 ns 0.0766 ns 0.20
Equals_MultipleChunks_ShiftedLayout \main\corerun.exe 16 16.7885 ns 0.4461 ns 1.00
Equals_MultipleChunks_ShiftedLayout \pr\corerun.exe 16 9.1240 ns 0.3578 ns 0.54
Equals_SingleChunkVsMultipleChunks \main\corerun.exe 16 16.9655 ns 0.6782 ns 1.00
Equals_SingleChunkVsMultipleChunks \pr\corerun.exe 16 3.7819 ns 0.2807 ns 0.22
Equals_ManyChunks_SameLayout \main\corerun.exe 16 16.6189 ns 0.4231 ns 1.00
Equals_ManyChunks_SameLayout \pr\corerun.exe 16 3.4566 ns 0.0471 ns 0.21
Equals_ManyChunks_ShiftedLayout \main\corerun.exe 16 16.6556 ns 0.3948 ns 1.00
Equals_ManyChunks_ShiftedLayout \pr\corerun.exe 16 8.8358 ns 0.1589 ns 0.53
NotEquals_SingleChunk_DifferentAtStart \main\corerun.exe 16 15.6765 ns 0.5206 ns 1.00
NotEquals_SingleChunk_DifferentAtStart \pr\corerun.exe 16 3.3680 ns 0.0357 ns 0.22
NotEquals_SingleChunk_DifferentAtMiddle \main\corerun.exe 16 8.5689 ns 0.0694 ns 1.00
NotEquals_SingleChunk_DifferentAtMiddle \pr\corerun.exe 16 3.4508 ns 0.1087 ns 0.40
NotEquals_SingleChunk_DifferentAtEnd \main\corerun.exe 16 1.7736 ns 0.0720 ns 1.00
NotEquals_SingleChunk_DifferentAtEnd \pr\corerun.exe 16 2.0724 ns 0.0388 ns 1.17
NotEquals_MultipleChunks_DifferentAtStart \main\corerun.exe 16 15.2326 ns 0.4526 ns 1.00
NotEquals_MultipleChunks_DifferentAtStart \pr\corerun.exe 16 3.4714 ns 0.1859 ns 0.23
NotEquals_MultipleChunks_DifferentAtMiddle \main\corerun.exe 16 8.2397 ns 0.1418 ns 1.00
NotEquals_MultipleChunks_DifferentAtMiddle \pr\corerun.exe 16 3.5028 ns 0.1434 ns 0.43
NotEquals_MultipleChunks_DifferentAtEnd \main\corerun.exe 16 2.0063 ns 0.0255 ns 1.00
NotEquals_MultipleChunks_DifferentAtEnd \pr\corerun.exe 16 2.0190 ns 0.0380 ns 1.01
NotEquals_ManyChunks_DifferentAtStart \main\corerun.exe 16 15.2601 ns 0.3876 ns 1.00
NotEquals_ManyChunks_DifferentAtStart \pr\corerun.exe 16 3.3538 ns 0.0662 ns 0.22
NotEquals_ManyChunks_DifferentAtMiddle \main\corerun.exe 16 8.1654 ns 0.1536 ns 1.00
NotEquals_ManyChunks_DifferentAtMiddle \pr\corerun.exe 16 3.4753 ns 0.0570 ns 0.43
NotEquals_ManyChunks_DifferentAtEnd \main\corerun.exe 16 2.0509 ns 0.0853 ns 1.00
NotEquals_ManyChunks_DifferentAtEnd \pr\corerun.exe 16 1.8844 ns 0.0208 ns 0.92
Equals_SameInstance \main\corerun.exe 64 0.9799 ns 0.0097 ns 1.00
Equals_SameInstance \pr\corerun.exe 64 0.9776 ns 0.0156 ns 1.00
Equals_SingleChunk \main\corerun.exe 64 60.6137 ns 0.6654 ns 1.00
Equals_SingleChunk \pr\corerun.exe 64 4.7966 ns 0.0621 ns 0.08
Equals_MultipleChunks_SameLayout \main\corerun.exe 64 61.3366 ns 0.6692 ns 1.00
Equals_MultipleChunks_SameLayout \pr\corerun.exe 64 9.0452 ns 0.1307 ns 0.15
Equals_MultipleChunks_ShiftedLayout \main\corerun.exe 64 60.5395 ns 1.7717 ns 1.00
Equals_MultipleChunks_ShiftedLayout \pr\corerun.exe 64 14.1929 ns 0.5287 ns 0.23
Equals_SingleChunkVsMultipleChunks \main\corerun.exe 64 61.6320 ns 1.7760 ns 1.00
Equals_SingleChunkVsMultipleChunks \pr\corerun.exe 64 9.0911 ns 0.1334 ns 0.15
Equals_ManyChunks_SameLayout \main\corerun.exe 64 55.0905 ns 2.2375 ns 1.00
Equals_ManyChunks_SameLayout \pr\corerun.exe 64 11.2673 ns 0.2667 ns 0.20
Equals_ManyChunks_ShiftedLayout \main\corerun.exe 64 55.4080 ns 1.0315 ns 1.00
Equals_ManyChunks_ShiftedLayout \pr\corerun.exe 64 21.1304 ns 0.4139 ns 0.38
NotEquals_SingleChunk_DifferentAtStart \main\corerun.exe 64 58.5009 ns 1.8393 ns 1.00
NotEquals_SingleChunk_DifferentAtStart \pr\corerun.exe 64 3.4960 ns 0.1361 ns 0.06
NotEquals_SingleChunk_DifferentAtMiddle \main\corerun.exe 64 30.7383 ns 0.8658 ns 1.00
NotEquals_SingleChunk_DifferentAtMiddle \pr\corerun.exe 64 4.0171 ns 0.1266 ns 0.13
NotEquals_SingleChunk_DifferentAtEnd \main\corerun.exe 64 2.0281 ns 0.1084 ns 1.00
NotEquals_SingleChunk_DifferentAtEnd \pr\corerun.exe 64 1.9980 ns 0.0605 ns 0.99
NotEquals_MultipleChunks_DifferentAtStart \main\corerun.exe 64 60.1795 ns 1.2281 ns 1.00
NotEquals_MultipleChunks_DifferentAtStart \pr\corerun.exe 64 10.2568 ns 0.2037 ns 0.17
NotEquals_MultipleChunks_DifferentAtMiddle \main\corerun.exe 64 31.0468 ns 0.9409 ns 1.00
NotEquals_MultipleChunks_DifferentAtMiddle \pr\corerun.exe 64 5.4533 ns 0.1886 ns 0.18
NotEquals_MultipleChunks_DifferentAtEnd \main\corerun.exe 64 1.8119 ns 0.0969 ns 1.00
NotEquals_MultipleChunks_DifferentAtEnd \pr\corerun.exe 64 2.0767 ns 0.0574 ns 1.15
NotEquals_ManyChunks_DifferentAtStart \main\corerun.exe 64 58.7732 ns 1.0353 ns 1.00
NotEquals_ManyChunks_DifferentAtStart \pr\corerun.exe 64 10.7658 ns 0.3968 ns 0.18
NotEquals_ManyChunks_DifferentAtMiddle \main\corerun.exe 64 31.6589 ns 0.8916 ns 1.00
NotEquals_ManyChunks_DifferentAtMiddle \pr\corerun.exe 64 5.0362 ns 0.0930 ns 0.16
NotEquals_ManyChunks_DifferentAtEnd \main\corerun.exe 64 1.9717 ns 0.0481 ns 1.00
NotEquals_ManyChunks_DifferentAtEnd \pr\corerun.exe 64 2.0104 ns 0.0431 ns 1.02
Equals_SameInstance \main\corerun.exe 256 0.9878 ns 0.0231 ns 1.00
Equals_SameInstance \pr\corerun.exe 256 0.9874 ns 0.0151 ns 1.00
Equals_SingleChunk \main\corerun.exe 256 200.1105 ns 3.1660 ns 1.00
Equals_SingleChunk \pr\corerun.exe 256 10.5819 ns 0.1813 ns 0.05
Equals_MultipleChunks_SameLayout \main\corerun.exe 256 207.4050 ns 8.7818 ns 1.00
Equals_MultipleChunks_SameLayout \pr\corerun.exe 256 15.9392 ns 0.2053 ns 0.08
Equals_MultipleChunks_ShiftedLayout \main\corerun.exe 256 197.5767 ns 2.6295 ns 1.00
Equals_MultipleChunks_ShiftedLayout \pr\corerun.exe 256 20.1793 ns 0.2216 ns 0.10
Equals_SingleChunkVsMultipleChunks \main\corerun.exe 256 201.0469 ns 3.1192 ns 1.00
Equals_SingleChunkVsMultipleChunks \pr\corerun.exe 256 14.0270 ns 0.3279 ns 0.07
Equals_ManyChunks_SameLayout \main\corerun.exe 256 218.6671 ns 6.0698 ns 1.00
Equals_ManyChunks_SameLayout \pr\corerun.exe 256 20.3297 ns 0.9772 ns 0.09
Equals_ManyChunks_ShiftedLayout \main\corerun.exe 256 216.7353 ns 14.5646 ns 1.00
Equals_ManyChunks_ShiftedLayout \pr\corerun.exe 256 35.6630 ns 1.0189 ns 0.17
NotEquals_SingleChunk_DifferentAtStart \main\corerun.exe 256 200.5018 ns 2.9393 ns 1.00
NotEquals_SingleChunk_DifferentAtStart \pr\corerun.exe 256 3.4008 ns 0.1077 ns 0.02
NotEquals_SingleChunk_DifferentAtMiddle \main\corerun.exe 256 107.6084 ns 2.9091 ns 1.00
NotEquals_SingleChunk_DifferentAtMiddle \pr\corerun.exe 256 6.5363 ns 0.0548 ns 0.06
NotEquals_SingleChunk_DifferentAtEnd \main\corerun.exe 256 1.9669 ns 0.0567 ns 1.00
NotEquals_SingleChunk_DifferentAtEnd \pr\corerun.exe 256 1.9935 ns 0.0783 ns 1.01
NotEquals_MultipleChunks_DifferentAtStart \main\corerun.exe 256 206.9720 ns 7.4576 ns 1.00
NotEquals_MultipleChunks_DifferentAtStart \pr\corerun.exe 256 17.0281 ns 0.4455 ns 0.08
NotEquals_MultipleChunks_DifferentAtMiddle \main\corerun.exe 256 104.7055 ns 1.2640 ns 1.00
NotEquals_MultipleChunks_DifferentAtMiddle \pr\corerun.exe 256 7.1163 ns 0.2177 ns 0.07
NotEquals_MultipleChunks_DifferentAtEnd \main\corerun.exe 256 1.7806 ns 0.0196 ns 1.00
NotEquals_MultipleChunks_DifferentAtEnd \pr\corerun.exe 256 1.8674 ns 0.1097 ns 1.05
NotEquals_ManyChunks_DifferentAtStart \main\corerun.exe 256 229.9022 ns 6.7890 ns 1.00
NotEquals_ManyChunks_DifferentAtStart \pr\corerun.exe 256 21.0382 ns 0.6569 ns 0.09
NotEquals_ManyChunks_DifferentAtMiddle \main\corerun.exe 256 110.2876 ns 2.1452 ns 1.00
NotEquals_ManyChunks_DifferentAtMiddle \pr\corerun.exe 256 4.9631 ns 0.1312 ns 0.05
NotEquals_ManyChunks_DifferentAtEnd \main\corerun.exe 256 1.7619 ns 0.0421 ns 1.00
NotEquals_ManyChunks_DifferentAtEnd \pr\corerun.exe 256 1.8757 ns 0.0539 ns 1.07
Benchmark source
    [BenchmarkCategory(Categories.Libraries, Categories.Runtime)]
    public class Perf_EqualsForStringBuilder
    {
        [Params(1, 4, 16, 64, 256)]
        public int Size;

        private StringBuilder _singleChunk;
        private StringBuilder _singleChunkEqual;
        private StringBuilder _singleChunkDifferentAtStart;
        private StringBuilder _singleChunkDifferentAtMiddle;
        private StringBuilder _singleChunkDifferentAtEnd;

        private StringBuilder _multipleChunks;
        private StringBuilder _multipleChunksSameLayout;
        private StringBuilder _multipleChunksDifferentAtStart;
        private StringBuilder _multipleChunksDifferentAtMiddle;
        private StringBuilder _multipleChunksDifferentAtEnd;
        private StringBuilder _multipleChunksShiftedLayout;

        private StringBuilder _manyChunks;
        private StringBuilder _manyChunksSameLayout;
        private StringBuilder _manyChunksDifferentAtStart;
        private StringBuilder _manyChunksDifferentAtMiddle;
        private StringBuilder _manyChunksDifferentAtEnd;
        private StringBuilder _manyChunksShiftedLayout;

        [GlobalSetup]
        public void Setup()
        {
            char[] chars = new char[Size];
            for (int i = 0; i < Size; i++)
            {
                chars[i] = (char)('a' + (i % 26));
            }
            string text = new string(chars);
            string differentAtStart = Replace(text, 0);
            string differentAtMiddle = Replace(text, Size / 2);
            string differentAtEnd = Replace(text, Size - 1);

            _singleChunk = new StringBuilder(text);
            _singleChunkEqual = new StringBuilder(text);
            _singleChunkDifferentAtStart = new StringBuilder(differentAtStart);
            _singleChunkDifferentAtMiddle = new StringBuilder(differentAtMiddle);
            _singleChunkDifferentAtEnd = new StringBuilder(differentAtEnd);

            _multipleChunks = CreateMultipleChunks(text, initialCapacity: 16);
            _multipleChunksSameLayout = CreateMultipleChunks(text, initialCapacity: 16);
            _multipleChunksDifferentAtStart = CreateMultipleChunks(differentAtStart, initialCapacity: 16);
            _multipleChunksDifferentAtMiddle = CreateMultipleChunks(differentAtMiddle, initialCapacity: 16);
            _multipleChunksDifferentAtEnd = CreateMultipleChunks(differentAtEnd, initialCapacity: 16);
            _multipleChunksShiftedLayout = CreateMultipleChunks(text, initialCapacity: 7);

            _manyChunks = CreateManyChunks(text, initialCapacity: 16);
            _manyChunksSameLayout = CreateManyChunks(text, initialCapacity: 16);
            _manyChunksDifferentAtStart = CreateManyChunks(differentAtStart, initialCapacity: 16);
            _manyChunksDifferentAtMiddle = CreateManyChunks(differentAtMiddle, initialCapacity: 16);
            _manyChunksDifferentAtEnd = CreateManyChunks(differentAtEnd, initialCapacity: 16);
            _manyChunksShiftedLayout = CreateManyChunks(text, initialCapacity: 15);

            static string Replace(string text, int index)
            {
                char[] chars = text.ToCharArray();
                chars[index] = 'Z';
                return new string(chars);
            }

            static StringBuilder CreateMultipleChunks(string text, int initialCapacity)
            {
                StringBuilder builder = new StringBuilder(initialCapacity);
                builder.Append(text);
                return builder;
            }

            static StringBuilder CreateManyChunks(string text, int initialCapacity)
            {
                StringBuilder builder = new StringBuilder(initialCapacity);
                foreach (char c in text)
                {
                    builder.Append(c);
                }
                return builder;
            }
        }

        [Benchmark]
        public bool Equals_SameInstance() => _singleChunk.Equals(_singleChunk);

        [Benchmark]
        public bool Equals_SingleChunk() => _singleChunk.Equals(_singleChunkEqual);

        [Benchmark]
        public bool Equals_MultipleChunks_SameLayout() => _multipleChunks.Equals(_multipleChunksSameLayout);

        [Benchmark]
        public bool Equals_MultipleChunks_ShiftedLayout() => _multipleChunks.Equals(_multipleChunksShiftedLayout);

        [Benchmark]
        public bool Equals_SingleChunkVsMultipleChunks() => _singleChunk.Equals(_multipleChunks);

        [Benchmark]
        public bool Equals_ManyChunks_SameLayout() => _manyChunks.Equals(_manyChunksSameLayout);

        [Benchmark]
        public bool Equals_ManyChunks_ShiftedLayout() => _manyChunks.Equals(_manyChunksShiftedLayout);

        [Benchmark]
        public bool NotEquals_SingleChunk_DifferentAtStart() => _singleChunk.Equals(_singleChunkDifferentAtStart);

        [Benchmark]
        public bool NotEquals_SingleChunk_DifferentAtMiddle() => _singleChunk.Equals(_singleChunkDifferentAtMiddle);

        [Benchmark]
        public bool NotEquals_SingleChunk_DifferentAtEnd() => _singleChunk.Equals(_singleChunkDifferentAtEnd);

        [Benchmark]
        public bool NotEquals_MultipleChunks_DifferentAtStart() => _multipleChunks.Equals(_multipleChunksDifferentAtStart);

        [Benchmark]
        public bool NotEquals_MultipleChunks_DifferentAtMiddle() => _multipleChunks.Equals(_multipleChunksDifferentAtMiddle);

        [Benchmark]
        public bool NotEquals_MultipleChunks_DifferentAtEnd() => _multipleChunks.Equals(_multipleChunksDifferentAtEnd);

        [Benchmark]
        public bool NotEquals_ManyChunks_DifferentAtStart() => _manyChunks.Equals(_manyChunksDifferentAtStart);

        [Benchmark]
        public bool NotEquals_ManyChunks_DifferentAtMiddle() => _manyChunks.Equals(_manyChunksDifferentAtMiddle);

        [Benchmark]
        public bool NotEquals_ManyChunks_DifferentAtEnd() => _manyChunks.Equals(_manyChunksDifferentAtEnd);
    }

Compare the overlapping portions of each chunk from the end using
ReadOnlySpan<char>.SequenceEqual instead of walking the chunk arrays
one character at a time.

The approach is based on the logic used in
StringBuilder.Equals(ReadOnlySpan<char>).
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Sep 3, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

@prozolic
prozolic marked this pull request as ready for review September 3, 2026 13:19
Copilot AI lite review requested due to automatic review settings September 3, 2026 13:19
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new implementation introduces small-input performance regressions that should be mitigated (or at least clearly justified) in an optimization-focused change.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR refactors StringBuilder.Equals(StringBuilder?) to compare chunk contents in larger blocks (using ReadOnlySpan<char>.SequenceEqual) rather than comparing one character at a time, aiming to improve throughput for larger builders and multi-chunk layouts.

Changes:

  • Adds a fast-path for the single-chunk case using SequenceEqual, and an early last-character mismatch check.
  • Moves the multi-chunk comparison logic into a new helper (EqualsCore) that compares overlapping portions of each chunk from the end via SequenceEqual.
File summaries
File Description
src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs Reworks StringBuilder.Equals implementation to compare chunk tails using span-based SequenceEqual and introduces a core helper for multi-chunk comparison.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +2034 to +2037
if (m_ChunkPrevious == null && sb.m_ChunkPrevious == null)
{
return m_ChunkChars.AsSpan(0, thisChunkLength).SequenceEqual(sb.m_ChunkChars.AsSpan(0, sbChunkLength));
}
Comment on lines +2081 to 2084
int length = Math.Min(thisChunkLength, sbChunkLength);
if (!thisChunk.m_ChunkChars.AsSpan(thisChunkLength - length, length)
.SequenceEqual(sbChunk.m_ChunkChars.AsSpan(sbChunkLength - length, length)))
{
@jkotas jkotas added area-System.Runtime tenet-performance Performance related issue and removed area-VM-coreclr labels Sep 3, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Runtime community-contribution Indicates that the PR has been added by a community member tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants