Setup
I have the following project: IvanStoychev.Useful.String.Extensions
It's just a bunch of string helper methods.
Such as:
public static bool ContainsAny(this string str, IEnumerable keywords, StringComparison comparison = StringComparison.Ordinal)
{
Validate.OriginalInstanceNotNull(str);
Validate.NotNull(keywords);
Validate.IEnumNotEmpty(keywords);
Validate.EnumContainsValue(comparison);
foreach (var word in keywords)
{
Validate.NotNullMember(word, nameof(keywords));
if (str.Contains(word, comparison))
return true;
}
return false;
}
The "Validate" class just performs checks and throws an exception if they don't hold up. Apart from that it's just a foreach using the original "Contains" method.
Aim
I wanted to benchmark my library in Debug vs Release configuration and in .Net 8 vs .Net 9, expecting the results to be better in Release and in .Net 9.
Benchmarking
I add net8-DBG to the name/version of my library and I compile it by using the command dotnet pack -c Release -o out, replacing "Release" with "Debug" and copying the output package file from the "out" directory to another local directory and I point my Benchmarker project to that directory as its only nuget source and install/update the package version in the Benchmarker project. I do this once for .Net 8 and then I change the version in the library project csproj file to .Net 9 and repeat (changing "net8" to "net9" in the name/version).
I have not specified the property in my csproj so I would expect the default behaviour of the "Release" configuration being more optimized than the "Debug" one. Yet the results are not such.
I'm using the following Benchmarker code:
class Program
{
static void Main(string[] args)
{
//var summary = BenchmarkRunner.Run(ManualConfig.Create(DefaultConfig.Instance).WithOptions(ConfigOptions.DisableOptimizationsValidator)); --- I use this one for Debug versions
var summary = BenchmarkRunner.Run();
}
}
[MemoryDiagnoser]
public class Benchmarker
{
const string constantStr = "<2394 char string>";
[Benchmark]
public void Remove()
{
constantStr.Remove("a");
}
[Benchmark]
public void Replace()
{
constantStr.Replace("a", "e");
}
[Benchmark]
public void Substring()
{
constantStr.Substring("user", "Library");
}
[Benchmark]
public void ContainsAny()
{
constantStr.ContainsAny(["zwarghsgorp"]);
}
[Benchmark]
public void Trim()
{
constantStr.Trim("DescriptionI");
}
}
Results
8-DBG
| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|
| Remove | 6,494.7 ns | 44.50 ns | 39.45 ns | 6,496.8 ns | 0.5417 | - | 4536 B |
| Replace | 206.5 ns | 4.14 ns | 8.65 ns | 201.6 ns | 0.5691 | - | 4760 B |
| Substring | 1,252.9 ns | 25.11 ns | 58.69 ns | 1,231.3 ns | 0.7019 | - | 5888 B |
| ContainsAny | 176.3 ns | 0.76 ns | 0.63 ns | 176.1 ns | 0.0095 | - | 80 B |
| Trim | 377.8 ns | 7.18 ns | 7.05 ns | 378.3 ns | 0.5746 | 0.0100 | 4808 B |
8-RLS
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|
| Remove | 6,344.7 ns | 20.10 ns | 16.79 ns | 0.5417 | - | 4536 B |
| Replace | 214.4 ns | 4.21 ns | 4.14 ns | 0.5691 | - | 4760 B |
| Substring | 1,168.3 ns | 17.14 ns | 16.03 ns | 0.7019 | - | 5888 B |
| ContainsAny | 206.9 ns | 1.72 ns | 1.53 ns | 0.0095 | - | 80 B |
| Trim | 315.2 ns | 5.82 ns | 5.16 ns | 0.5746 | 0.0100 | 4808 B |
9-DBG
| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|
| Remove | 6,507.8 ns | 59.81 ns | 55.95 ns | 6,507.8 ns | 0.5417 | - | 4536 B |
| Replace | 208.2 ns | 4.21 ns | 9.32 ns | 202.1 ns | 0.5691 | - | 4760 B |
| Substring | 1,229.1 ns | 5.49 ns | 4.59 ns | 1,228.1 ns | 0.7019 | - | 5888 B |
| ContainsAny | 172.6 ns | 3.26 ns | 3.04 ns | 171.4 ns | 0.0095 | - | 80 B |
| Trim | 379.9 ns | 7.29 ns | 7.48 ns | 380.7 ns | 0.5746 | 0.0100 | 4808 B |
9-RLS
| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|
| Remove | 6,655.3 ns | 132.70 ns | 136.27 ns | 6,656.5 ns | 0.5417 | - | 4536 B |
| Replace | 211.2 ns | 4.25 ns | 9.15 ns | 211.1 ns | 0.5691 | - | 4760 B |
| Substring | 1,155.1 ns | 21.20 ns | 41.35 ns | 1,133.6 ns | 0.7019 | - | 5888 B |
| ContainsAny | 208.3 ns | 0.72 ns | 0.60 ns | 208.4 ns | 0.0095 | - | 80 B |
| Trim | 312.8 ns | 6.11 ns | 6.00 ns | 313.4 ns | 0.5746 | 0.0100 | 4808 B |
As you can see while some methods are faster other are slower under "Release". Furthermore seeing all the improvements that went in .Net 9 I expected it to be MUCH faster than 8 but, again, the results are very similar.
Question
Why is there no significant performance benefit under "Release" and .Net 9?
I thought perhaps it might be because I am not doing much else other than foreaching over built-in string extension methods, which might be as optimized as can be but am not sure.
And even if that is the case the "Substring" method ought to have space for improvement under "Release" and .Net 9.