Skip to content

Commit fcde467

Browse files
authored
Optimize StringBuilder.Equals(ReadOnlySpan<char>) to compare by chunk (#565)
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 fcde467

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)