diff --git a/assemblySize.include.md b/assemblySize.include.md index 4c85bdc0..a9b57af0 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -4,12 +4,12 @@ |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 351.5KB | +343.5KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | | netstandard2.1 | 8.5KB | 306.0KB | +297.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | net462 | 7.0KB | 354.0KB | +347.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | net47 | 7.0KB | 353.5KB | +346.5KB | +9.0KB | +6.5KB | +9.5KB | +13.5KB | | net471 | 8.5KB | 353.0KB | +344.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | | net472 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | net481 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | netcoreapp2.0 | 9.0KB | 327.5KB | +318.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | | netcoreapp2.1 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | @@ -31,12 +31,12 @@ |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 513.4KB | +505.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | | netstandard2.1 | 8.5KB | 441.7KB | +433.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net462 | 7.0KB | 517.0KB | +510.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net47 | 7.0KB | 516.2KB | +509.2KB | +16.7KB | +8.2KB | +14.4KB | +18.9KB | | net471 | 8.5KB | 515.4KB | +506.9KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | | net472 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | net481 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | netcoreapp2.0 | 9.0KB | 478.9KB | +469.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | netcoreapp2.1 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | diff --git a/src/Polyfill/Polyfill_StringBuilder.cs b/src/Polyfill/Polyfill_StringBuilder.cs index b03f75cf..55eb59b8 100644 --- a/src/Polyfill/Polyfill_StringBuilder.cs +++ b/src/Polyfill/Polyfill_StringBuilder.cs @@ -19,14 +19,17 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) return false; } - for (var index = 0; index < target.Length; index++) + // Walk the contiguous chunks and compare each with the vectorized SequenceEqual, + // rather than indexing the StringBuilder per char (which walks the chunk list on every access). + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + + span = span.Slice(chunkSpan.Length); } return true; diff --git a/src/Split/net461/Polyfill_StringBuilder.cs b/src/Split/net461/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net461/Polyfill_StringBuilder.cs +++ b/src/Split/net461/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net462/Polyfill_StringBuilder.cs b/src/Split/net462/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net462/Polyfill_StringBuilder.cs +++ b/src/Split/net462/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net47/Polyfill_StringBuilder.cs b/src/Split/net47/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net47/Polyfill_StringBuilder.cs +++ b/src/Split/net47/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net471/Polyfill_StringBuilder.cs b/src/Split/net471/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net471/Polyfill_StringBuilder.cs +++ b/src/Split/net471/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net472/Polyfill_StringBuilder.cs b/src/Split/net472/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net472/Polyfill_StringBuilder.cs +++ b/src/Split/net472/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net48/Polyfill_StringBuilder.cs b/src/Split/net48/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net48/Polyfill_StringBuilder.cs +++ b/src/Split/net48/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/net481/Polyfill_StringBuilder.cs b/src/Split/net481/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/net481/Polyfill_StringBuilder.cs +++ b/src/Split/net481/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/netcoreapp2.0/Polyfill_StringBuilder.cs b/src/Split/netcoreapp2.0/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/netcoreapp2.0/Polyfill_StringBuilder.cs +++ b/src/Split/netcoreapp2.0/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/netstandard2.0/Polyfill_StringBuilder.cs b/src/Split/netstandard2.0/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/netstandard2.0/Polyfill_StringBuilder.cs +++ b/src/Split/netstandard2.0/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Split/uap10.0/Polyfill_StringBuilder.cs b/src/Split/uap10.0/Polyfill_StringBuilder.cs index 11599333..318263ed 100644 --- a/src/Split/uap10.0/Polyfill_StringBuilder.cs +++ b/src/Split/uap10.0/Polyfill_StringBuilder.cs @@ -16,14 +16,14 @@ public static bool Equals(this StringBuilder target, ReadOnlySpan span) { return false; } - for (var index = 0; index < target.Length; index++) + foreach (var chunk in target.GetChunks()) { - var ch1 = target[index]; - var ch2 = span[index]; - if (ch1 != ch2) + var chunkSpan = chunk.Span; + if (!chunkSpan.SequenceEqual(span.Slice(0, chunkSpan.Length))) { return false; } + span = span.Slice(chunkSpan.Length); } return true; } diff --git a/src/Tests/PolyfillTests_Memory.cs b/src/Tests/PolyfillTests_Memory.cs index 92a60d9f..90cb0748 100644 --- a/src/Tests/PolyfillTests_Memory.cs +++ b/src/Tests/PolyfillTests_Memory.cs @@ -527,5 +527,28 @@ public async Task StringEqualsSpan() { var builder = new StringBuilder("value"); await Assert.That(builder.Equals("value".AsSpan())).IsTrue(); + await Assert.That(builder.Equals("other".AsSpan())).IsFalse(); + await Assert.That(builder.Equals("val".AsSpan())).IsFalse(); + await Assert.That(builder.Equals("values".AsSpan())).IsFalse(); + await Assert.That(builder.Equals("".AsSpan())).IsFalse(); + await Assert.That(new StringBuilder().Equals("".AsSpan())).IsTrue(); + } + + [Test] + public async Task StringEqualsSpanMultipleChunks() + { + // A small initial capacity followed by appends past it forces the + // StringBuilder to span multiple chunks, exercising the chunk-walking path. + var builder = new StringBuilder("a", 1); + builder.Append("bb"); + builder.Append("ccc"); + + await Assert.That(builder.Equals("abbccc".AsSpan())).IsTrue(); + // mismatch in the first chunk + await Assert.That(builder.Equals("xbbccc".AsSpan())).IsFalse(); + // mismatch in a later chunk, after the span has advanced across a chunk boundary + await Assert.That(builder.Equals("abbcxc".AsSpan())).IsFalse(); + // matching prefix but wrong length + await Assert.That(builder.Equals("abbcc".AsSpan())).IsFalse(); } }