From dd12a19ea075f7ef9c6c0cd39466d68b2b32d745 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 18 May 2026 19:40:04 +1000 Subject: [PATCH] reduce ToCamelCase allocations by using string.Create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces ToCharArray() + new string(chars) with string.Create, which writes directly into the new string's buffer. Halves allocations (1.37 KB → 688 B) and runs ~37% faster on a 15-name workload. --- .../NamingStrategy/CamelCaseNamingStrategy.cs | 52 +++++++++---------- .../Benchmarks/CamelCaseBenchmarks.cs | 37 +++++++++++++ src/Benchmark.Tests/Program.cs | 2 +- 3 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 src/ArgonTests/Benchmarks/CamelCaseBenchmarks.cs diff --git a/src/Argon/NamingStrategy/CamelCaseNamingStrategy.cs b/src/Argon/NamingStrategy/CamelCaseNamingStrategy.cs index c50385c41..686bb9e49 100644 --- a/src/Argon/NamingStrategy/CamelCaseNamingStrategy.cs +++ b/src/Argon/NamingStrategy/CamelCaseNamingStrategy.cs @@ -46,38 +46,38 @@ internal static string ToCamelCase(string s) return s; } - var chars = s.ToCharArray(); - - for (var i = 0; i < chars.Length; i++) + return string.Create(s.Length, s, static (span, source) => { - var ch = chars[i]; - if (i == 1 && !char.IsUpper(ch)) + source.AsSpan().CopyTo(span); + for (var i = 0; i < span.Length; i++) { - break; - } - - var hasNext = i + 1 < chars.Length; - if (i > 0 && hasNext && !char.IsUpper(chars[i + 1])) - { - // if the next character is a space, which is not considered uppercase - // (otherwise we wouldn't be here...) - // we want to ensure that the following: - // 'FOO bar' is rewritten as 'foo bar', and not as 'foO bar' - // The code was written in such a way that the first word in uppercase - // ends when if finds an uppercase letter followed by a lowercase letter. - // now a ' ' (space, (char)32) is considered not upper - // but in that case we still want our current character to become lowercase - if (char.IsSeparator(chars[i + 1])) + var ch = span[i]; + if (i == 1 && !char.IsUpper(ch)) { - chars[i] = char.ToLower(ch, InvariantCulture); + break; } - break; - } + var hasNext = i + 1 < span.Length; + if (i > 0 && hasNext && !char.IsUpper(span[i + 1])) + { + // if the next character is a space, which is not considered uppercase + // (otherwise we wouldn't be here...) + // we want to ensure that the following: + // 'FOO bar' is rewritten as 'foo bar', and not as 'foO bar' + // The code was written in such a way that the first word in uppercase + // ends when if finds an uppercase letter followed by a lowercase letter. + // now a ' ' (space, (char)32) is considered not upper + // but in that case we still want our current character to become lowercase + if (char.IsSeparator(span[i + 1])) + { + span[i] = char.ToLower(ch, InvariantCulture); + } - chars[i] = char.ToLower(ch, InvariantCulture); - } + break; + } - return new(chars); + span[i] = char.ToLower(ch, InvariantCulture); + } + }); } } \ No newline at end of file diff --git a/src/ArgonTests/Benchmarks/CamelCaseBenchmarks.cs b/src/ArgonTests/Benchmarks/CamelCaseBenchmarks.cs new file mode 100644 index 000000000..0db915195 --- /dev/null +++ b/src/ArgonTests/Benchmarks/CamelCaseBenchmarks.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2007 James Newton-King. All rights reserved. +// Use of this source code is governed by The MIT License, +// as found in the license.md file. + +using BenchmarkDotNet.Attributes; + +[MemoryDiagnoser] +public class CamelCaseBenchmarks +{ + static readonly string[] names = + [ + "Id", + "Name", + "FirstName", + "LastName", + "EmailAddress", + "PhoneNumber", + "DateOfBirth", + "IsActive", + "CreatedAt", + "UpdatedAt", + "URL", + "HTTPStatusCode", + "XMLDocument", + "SomeVeryLongPropertyNameThatExercisesTheLoop", + "ABC" + ]; + + [Benchmark] + public void Run() + { + for (var i = 0; i < names.Length; i++) + { + CamelCaseNamingStrategy.ToCamelCase(names[i]); + } + } +} diff --git a/src/Benchmark.Tests/Program.cs b/src/Benchmark.Tests/Program.cs index 8a395cd5e..5d3e9f3e3 100644 --- a/src/Benchmark.Tests/Program.cs +++ b/src/Benchmark.Tests/Program.cs @@ -11,7 +11,7 @@ public static void Main(string[] args) var attribute = (AssemblyFileVersionAttribute)typeof(JsonConvert).Assembly.GetCustomAttribute(typeof(AssemblyFileVersionAttribute))!; Console.WriteLine($"Json.NET Version: {attribute.Version}"); - var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList)]); + var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList), typeof(CamelCaseBenchmarks)]); if (args.Length == 0) { switcher.Run(["*"]);