Using string.Create() and avoiding the throw keyword in our method (yes, you read it right), we can take Marcell's answer one step further. Also, my method handles strings of arbitrary length (e.g. several megabytes of text).
public static string L33t(this string s) { static void ThrowError() => throw new ArgumentException("There is no first letter"); if (string.IsNullOrEmpty(s)) ThrowError(); // No "throw" keyword to avoid costly IL return string.Create(s.Length, s, (chars, state) => { state.AsSpan().CopyTo(chars); // No slicing to save some CPU cycles chars[0] = char.ToUpper(chars[0]); }); }
Performance
Here are the numbers for benchmarks run on .NET Core 3.1.7, x64. I added a longer string to pinpoint the cost of extra copies.
| Method | Data | Mean | Error | StdDev | Median | |-------- |--------------------- |----------:|----------:|----------:|----------:| | L33t | red | 8.545 ns | 0.4612 ns | 1.3308 ns | 8.075 ns | | Marcell | red | 9.153 ns | 0.3377 ns | 0.9471 ns | 8.946 ns | | L33t | red house | 7.715 ns | 0.1741 ns | 0.4618 ns | 7.793 ns | | Marcell | red house | 10.537 ns | 0.5002 ns | 1.4351 ns | 10.377 ns | | L33t | red r(...)house [89] | 11.121 ns | 0.6774 ns | 1.9106 ns | 10.612 ns | | Marcell | red r(...)house [89] | 16.739 ns | 0.4468 ns | 1.3033 ns | 16.853 ns |
Full test code
using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; namespace CorePerformanceTest { class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<StringUpperTest>(); } } public class StringUpperTest { [Params("red", "red house", "red red red red red red red red red red red red red red red red red red red red red house")] public string Data; [Benchmark] public string Marcell() => Data.Marcell(); [Benchmark] public string L33t() => Data.L33t(); } internal static class StringExtensions { public static string Marcell(this string s) { if (string.IsNullOrEmpty(s)) throw new ArgumentException("There is no first letter"); Span<char> a = stackalloc char[s.Length]; s.AsSpan(1).CopyTo(a.Slice(1)); a[0] = char.ToUpper(s[0]); return new string(a); } public static string L33t(this string s) { static void ThrowError() => throw new ArgumentException("There is no first letter"); if (string.IsNullOrEmpty(s)) ThrowError(); // IMPORTANT: Do not "throw" here! return string.Create(s.Length, s, (chars, state) => { state.AsSpan().CopyTo(chars); chars[0] = char.ToUpper(chars[0]); }); } } }
Please let me know if you can make it any faster!