Skip to content

Commit 90fd28c

Browse files
committed
Optimize StringBuilder.Equals(ReadOnlySpan<char>) to compare by chunk
Walk the StringBuilder's contiguous chunks and compare each with the vectorized ReadOnlySpan.SequenceEqual, instead of indexing per char (the indexer walks the chunk list on every access). Add multi-chunk and mismatch test coverage.
1 parent a7916e4 commit 90fd28c

13 files changed

Lines changed: 74 additions & 48 deletions

assemblySize.include.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
55
| netstandard2.0 | 8.0KB | 351.5KB | +343.5KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB |
66
| netstandard2.1 | 8.5KB | 306.0KB | +297.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB |
7-
| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB |
7+
| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
88
| net462 | 7.0KB | 354.0KB | +347.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
99
| net47 | 7.0KB | 353.5KB | +346.5KB | +9.0KB | +6.5KB | +9.5KB | +13.5KB |
1010
| net471 | 8.5KB | 353.0KB | +344.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB |
1111
| net472 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
12-
| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
12+
| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
1313
| net481 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
1414
| netcoreapp2.0 | 9.0KB | 327.5KB | +318.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB |
1515
| netcoreapp2.1 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB |
@@ -31,12 +31,12 @@
3131
|----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------|
3232
| netstandard2.0 | 8.0KB | 513.4KB | +505.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB |
3333
| netstandard2.1 | 8.5KB | 441.7KB | +433.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB |
34-
| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB |
34+
| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
3535
| net462 | 7.0KB | 517.0KB | +510.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
3636
| net47 | 7.0KB | 516.2KB | +509.2KB | +16.7KB | +8.2KB | +14.4KB | +18.9KB |
3737
| net471 | 8.5KB | 515.4KB | +506.9KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB |
3838
| net472 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
39-
| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
39+
| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
4040
| net481 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |
4141
| netcoreapp2.0 | 9.0KB | 478.9KB | +469.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB |
4242
| netcoreapp2.1 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB |

src/Polyfill/Polyfill_StringBuilder.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1919
return false;
2020
}
2121

22-
for (var index = 0; index < target.Length; index++)
22+
// Walk the contiguous chunks and compare each with the vectorized SequenceEqual,
23+
// rather than indexing the StringBuilder per char (which walks the chunk list on every access).
24+
foreach (var chunk in target.GetChunks())
2325
{
24-
var ch1 = target[index];
25-
var ch2 = span[index];
26-
if (ch1 != ch2)
26+
var chunkSpan = chunk.Span;
27+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2728
{
2829
return false;
2930
}
31+
32+
span = span.Slice(chunkSpan.Length);
3033
}
3134

3235
return true;

src/Split/net461/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net462/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net47/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net471/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net472/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net48/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/net481/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

src/Split/netcoreapp2.0/Polyfill_StringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan<char> span)
1616
{
1717
return false;
1818
}
19-
for (var index = 0; index < target.Length; index++)
19+
foreach (var chunk in target.GetChunks())
2020
{
21-
var ch1 = target[index];
22-
var ch2 = span[index];
23-
if (ch1 != ch2)
21+
var chunkSpan = chunk.Span;
22+
if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length)))
2423
{
2524
return false;
2625
}
26+
span = span.Slice(chunkSpan.Length);
2727
}
2828
return true;
2929
}

0 commit comments

Comments
 (0)