using System.Diagnostics;
private static readonly string[] TestCases = new[]
"This is a test string with MIXED case and multiple occurrences of Test and TEST and test",
"Short string test TEST Test",
"testTestTESTtestTestTEST",
"t" + new string('x', 1000) + "test" + new string('x', 1000) + "TEST"
public static void Main()
const int iterations = 10000;
const int warmupIterations = 1000;
foreach (var testCase in TestCases)
for (int i = 0; i < warmupIterations; i++)
ReplaceIgnoreCase(testCase, "test", "REPLACED");
ReplaceIgnoreCaseSpan(testCase, "test", "REPLACED");
ReplaceIgnoreCaseChunked(testCase, "test", "REPLACED");
var results1 = BenchmarkMethod("Original", iterations, () =>
TestCases.Sum(tc => ReplaceIgnoreCase(tc, "test", "REPLACED").Length));
var results2 = BenchmarkMethod("Span", iterations, () =>
TestCases.Sum(tc => ReplaceIgnoreCaseSpan(tc, "test", "REPLACED").Length));
var results3 = BenchmarkMethod("Chunked", iterations, () =>
TestCases.Sum(tc => ReplaceIgnoreCaseChunked(tc, "test", "REPLACED").Length));
var fastest = allResults.OrderBy(x => x.Item2.Average).First();
Console.WriteLine("\nBenchmark Results:");
Console.WriteLine("==================");
foreach (var (name, result) in allResults)
Console.WriteLine($"{name,-10} - Avg: {result.Average:F2}ms, Min: {result.Min:F2}ms, Max: {result.Max:F2}ms");
Console.WriteLine($"\nFastest Implementation: {fastest.Item1}");
Console.WriteLine("\nMemory Test (single iteration with longest test case):");
Console.WriteLine("================================================");
var longString = TestCases[4];
var beforeMem1 = GC.GetTotalMemory(true);
ReplaceIgnoreCase(longString, "test", "REPLACED");
var afterMem1 = GC.GetTotalMemory(true);
var beforeMem2 = GC.GetTotalMemory(true);
ReplaceIgnoreCaseSpan(longString, "test", "REPLACED");
var afterMem2 = GC.GetTotalMemory(true);
var beforeMem3 = GC.GetTotalMemory(true);
ReplaceIgnoreCaseChunked(longString, "test", "REPLACED");
var afterMem3 = GC.GetTotalMemory(true);
Console.WriteLine($"Original: {afterMem1 - beforeMem1} bytes");
Console.WriteLine($"Span : {afterMem2 - beforeMem2} bytes");
Console.WriteLine($"Chunked : {afterMem3 - beforeMem3} bytes");
private static (double Average, double Min, double Max) BenchmarkMethod(
var times = new double[5];
for (int trial = 0; trial < 5; trial++)
var sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
times[trial] = sw.ElapsedMilliseconds;
Console.WriteLine($"{name} Trial {trial + 1}: {times[trial]}ms");
Average: times.Average(),
public static string ReplaceIgnoreCase(string str, string oldValue, string newValue)
if (string.IsNullOrEmpty(str))
else if (oldValue == null)
throw new ArgumentNullException(nameof(oldValue));
else if (oldValue.Length == 0)
throw new ArgumentException("String " + nameof(oldValue) + " can not be of zero length.");
int startSearchFromIndex = 0;
int foundAt = str.IndexOf(oldValue, startSearchFromIndex, StringComparison.OrdinalIgnoreCase);
StringBuilder resultStringBuilder = new(str.Length);
bool isReplacementNullOrEmpty = string.IsNullOrEmpty(newValue);
int charsUntilReplacement = foundAt - startSearchFromIndex;
if (charsUntilReplacement != 0)
resultStringBuilder.Append(str, startSearchFromIndex, charsUntilReplacement);
if (!isReplacementNullOrEmpty)
resultStringBuilder.Append(newValue);
startSearchFromIndex = foundAt + oldValue.Length;
if (startSearchFromIndex == str.Length)
return resultStringBuilder.ToString();
foundAt = str.IndexOf(oldValue, startSearchFromIndex, StringComparison.OrdinalIgnoreCase);
resultStringBuilder.Append(str, startSearchFromIndex, str.Length - startSearchFromIndex);
return resultStringBuilder.ToString();
public static string ReplaceIgnoreCaseSpan(string str, string oldValue, string newValue)
if (string.IsNullOrEmpty(str)) return string.Empty;
if (oldValue == null) throw new ArgumentNullException(nameof(oldValue));
if (oldValue.Length == 0) throw new ArgumentException("String cannot be empty", nameof(oldValue));
ReadOnlySpan<char> span = str.AsSpan();
ReadOnlySpan<char> oldSpan = oldValue.AsSpan();
var idx = span.ToString().IndexOf(oldValue, StringComparison.OrdinalIgnoreCase);
if (idx == -1) return str;
var estimatedLength = str.Length + (newValue.Length - oldValue.Length) * 2;
var sb = new StringBuilder(estimatedLength);
idx = span[currentIndex..].ToString().IndexOf(oldValue, StringComparison.OrdinalIgnoreCase);
sb.Append(span[currentIndex..]);
sb.Append(span.Slice(currentIndex, idx));
currentIndex += idx + oldValue.Length;
public static string ReplaceIgnoreCaseChunked(string str, string oldValue, string newValue)
if (string.IsNullOrEmpty(str)) return string.Empty;
if (oldValue == null) throw new ArgumentNullException(nameof(oldValue));
if (oldValue.Length == 0) throw new ArgumentException("String cannot be empty", nameof(oldValue));
var firstIndex = str.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase);
if (firstIndex == -1) return str;
const int chunkSize = 4096;
var sb = new StringBuilder(str.Length);
while (currentIndex < str.Length)
int remainingLength = Math.Min(chunkSize, str.Length - currentIndex);
int chunkEnd = currentIndex + remainingLength;
int foundAt = str.IndexOf(oldValue, currentIndex, StringComparison.OrdinalIgnoreCase);
if (foundAt == -1 || foundAt >= chunkEnd)
sb.Append(str, currentIndex, remainingLength);
while (foundAt != -1 && foundAt < chunkEnd)
sb.Append(str, currentIndex, foundAt - currentIndex);
currentIndex = foundAt + oldValue.Length;
if (currentIndex >= chunkEnd) break;
foundAt = str.IndexOf(oldValue, currentIndex, StringComparison.OrdinalIgnoreCase);