Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize String.Replace(string, string) #44088

Merged
merged 1 commit into from
Oct 31, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1124,48 +1124,71 @@ public string Replace(char oldChar, char newChar)

public string Replace(string oldValue, string? newValue)
{
if (oldValue == null)
if (oldValue is null)
{
throw new ArgumentNullException(nameof(oldValue));
}
if (oldValue.Length == 0)
{
throw new ArgumentException(SR.Argument_StringZeroLength, nameof(oldValue));
}

// Api behavior: if newValue is null, instances of oldValue are to be removed.
newValue ??= string.Empty;
// If newValue is null, treat it as an empty string. Callers use this to remove the oldValue.
newValue ??= Empty;

// Track the locations of oldValue to be replaced.
var replacementIndices = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]);
EgorBo marked this conversation as resolved.
Show resolved Hide resolved

unsafe
if (oldValue.Length == 1)
{
fixed (char* pThis = &_firstChar)
// Special-case oldValues that are a single character. Even though there's an overload that takes
// a single character, its newValue is also a single character, so this overload ends up being used
// often to remove characters by having an empty newValue.

if (newValue.Length == 1)
{
int matchIdx = 0;
int lastPossibleMatchIdx = this.Length - oldValue.Length;
while (matchIdx <= lastPossibleMatchIdx)
{
char* pMatch = pThis + matchIdx;
for (int probeIdx = 0; probeIdx < oldValue.Length; probeIdx++)
{
if (pMatch[probeIdx] != oldValue[probeIdx])
{
goto Next;
}
}
// Found a match for the string. Record the location of the match and skip over the "oldValue."
replacementIndices.Append(matchIdx);
matchIdx += oldValue.Length;
continue;
// With both the oldValue and newValue being a single character, it's cheaper to just use the other overload.
return Replace(oldValue[0], newValue[0]);
}

Next:
matchIdx++;
// Find all occurrences of the oldValue character.
char c = oldValue[0];
int i = 0;
while (true)
{
int pos = SpanHelpers.IndexOf(ref Unsafe.Add(ref _firstChar, i), c, Length - i);
if (pos == -1)
{
break;
}
replacementIndices.Append(i + pos);
i += pos + 1;
}
}
else
{
// Find all occurrences of the oldValue string.
int i = 0;
while (true)
{
int pos = SpanHelpers.IndexOf(ref Unsafe.Add(ref _firstChar, i), Length - i, ref oldValue._firstChar, oldValue.Length);
if (pos == -1)
{
break;
}
replacementIndices.Append(i + pos);
i += pos + oldValue.Length;
}
}

// If the oldValue wasn't found, just return the original string.
if (replacementIndices.Length == 0)
{
return this;
}

// String allocation and copying is in separate method to make this method faster for the case where
// nothing needs replacing.
// Perform the replacement. String allocation and copying is in separate method to make this method faster
// for the case where nothing needs replacing.
string dst = ReplaceHelper(oldValue.Length, newValue, replacementIndices.AsSpan());

replacementIndices.Dispose();
Expand Down