Skip to content

Instantly share code, notes, and snippets.

@habbes
Created December 1, 2023 13:55
Show Gist options
  • Save habbes/54f9fc9c28683e13d87db479fd829281 to your computer and use it in GitHub Desktop.
Save habbes/54f9fc9c28683e13d87db479fd829281 to your computer and use it in GitHub Desktop.
Testing different implementations of generating a path string from a collection of segments
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SegmentsPathString
{
internal static class PathHelpers
{
public static string GetPathStringWithStringBuilder(this SamplePath path)
{
StringBuilder builder = new StringBuilder();
int index = 0;
while (index < path.Segments.Count)
{
if (index != 0)
{
builder.Append('/');
}
builder.Append(path.Segments[index].Name);
index++;
}
return builder.ToString();
}
public static string GetPathStringWithJoin(this SamplePath path)
{
string[] segments = new string[path.Segments.Count];
for (int i = 0; i < path.Segments.Count; i++)
{
segments[i] = path.Segments[i].Name;
}
return string.Join("/", segments);
}
public static string GetPathStringWithJoinAndArrayPool(this SamplePath path)
{
string[] segments = ArrayPool<string>.Shared.Rent(path.Segments.Count);
for (int i = 0; i < path.Segments.Count; i++)
{
segments[i] = path.Segments[i].Name;
}
string result = string.Join("/", segments, 0, path.Segments.Count);
ArrayPool<string>.Shared.Return(segments);
return result;
}
public static string GetPathStringWithCharArray(this SamplePath path)
{
int length = 0;
for (int i = 0; i < path.Segments.Count; i++)
{
if (i != 0)
{
length++; // for the separator
}
length += path.Segments[i].Name.Length;
}
char[] pathArray = new char[length];
int pathIndex = 0;
for (int i = 0; i < path.Segments.Count ; i++)
{
if (i != 0)
{
pathArray[pathIndex++] = '/';
}
string segment = path.Segments[i].Name;
segment.CopyTo(0, pathArray, pathIndex, segment.Length);
pathIndex += segment.Length;
}
return new string(pathArray);
}
public static string GetPathStringWithCharArrayAndPooling(this SamplePath path)
{
int length = 0;
for (int i = 0; i < path.Segments.Count; i++)
{
if (i != 0)
{
length++; // for the separator
}
length += path.Segments[i].Name.Length;
}
char[] pathArray = null;
Span<char> buffer = length < 256 ?
stackalloc char[length] : pathArray = ArrayPool<char>.Shared.Rent(length);
int pathIndex = 0;
for (int i = 0; i < path.Segments.Count; i++)
{
if (i != 0)
{
buffer[pathIndex++] = '/';
}
string segment = path.Segments[i].Name;
segment.CopyTo(buffer.Slice(pathIndex, segment.Length));
pathIndex += segment.Length;
}
string result = new string(buffer.Slice(0, length));
if (pathArray != null)
{
ArrayPool<char>.Shared.Return(pathArray);
}
return result;
}
}
}
using BenchmarkDotNet.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SegmentsPathString
{
[MemoryDiagnoser]
[ShortRunJob]
public class PathStringBenchmarks
{
private static Dictionary<string, SamplePath> Paths = new Dictionary<string, SamplePath>
{
{
"shortPath",
new SamplePath(new[]
{
new PathSegment("one"),
new PathSegment("two"),
new PathSegment("three")
})
},
{
"longPath",
new SamplePath(new[]
{
new PathSegment("oneoneone"),
new PathSegment("twotwotwo"),
new PathSegment("threethreethree"),
new PathSegment("oneoneoneone"),
new PathSegment("twotwotwotwotwo"),
new PathSegment("threethreethreethree"),
new PathSegment("oneoneoneoneoneone"),
new PathSegment("twotwotwotwotwotwotwotwotwo"),
new PathSegment("threethreethreethreethreethreethree"),
})
},
{
"verLongPath",
new SamplePath(new[]
{
new PathSegment("oneoneone"),
new PathSegment("twotwotwo"),
new PathSegment("threethreethree"),
new PathSegment("oneoneoneone"),
new PathSegment("twotwotwotwotwo"),
new PathSegment("threethreethreethree"),
new PathSegment("oneoneoneoneoneone"),
new PathSegment("twotwotwotwotwotwotwotwotwo"),
new PathSegment("threethreethreethreethreethreethree"),
new PathSegment("oneoneone"),
new PathSegment("twotwotwo"),
new PathSegment("threethreethree"),
new PathSegment("oneoneoneone"),
new PathSegment("twotwotwotwotwo"),
new PathSegment("threethreethreethree"),
new PathSegment("oneoneoneoneoneone"),
new PathSegment("twotwotwotwotwotwotwotwotwo"),
new PathSegment("threethreethreethreethreethreethree"),
new PathSegment("oneoneone"),
new PathSegment("twotwotwo"),
new PathSegment("threethreethree"),
new PathSegment("oneoneoneone"),
new PathSegment("twotwotwotwotwo"),
new PathSegment("threethreethreethree"),
new PathSegment("oneoneoneoneoneone"),
new PathSegment("twotwotwotwotwotwotwotwotwo"),
new PathSegment("threethreethreethreethreethreethree"),
})
}
};
[ParamsSource(nameof(GetPathNames))]
public string pathName;
public IEnumerable<string> GetPathNames() => Paths.Keys;
private SamplePath path;
[GlobalSetup]
public void Setup()
{
path = Paths[pathName];
}
[Benchmark]
public string WithStringBuilder() => path.GetPathStringWithStringBuilder();
[Benchmark]
public string WithJoin() => path.GetPathStringWithJoin();
[Benchmark]
public string WithJoinAndArrayPool() => path.GetPathStringWithJoinAndArrayPool();
[Benchmark]
public string WithCharArray() => path.GetPathStringWithCharArray();
[Benchmark]
public string WithNoTempAllocCharArray() => path.GetPathStringWithCharArrayAndPooling();
}
}
// See https://aka.ms/new-console-template for more information
using BenchmarkDotNet.Running;
using SegmentsPathString;
BenchmarkRunner.Run(typeof(PathStringBenchmarks));
//RunTests();
void RunTests()
{
try
{
Test(
("StringBuilder", PathHelpers.GetPathStringWithStringBuilder),
("Join", PathHelpers.GetPathStringWithJoin),
("JoinWithArrayPool", PathHelpers.GetPathStringWithJoinAndArrayPool),
("CharArray", PathHelpers.GetPathStringWithCharArray),
("CharArrayAndPooling", PathHelpers.GetPathStringWithCharArrayAndPooling)
);
}
catch
{
throw;
}
}
void Test(params (string name, Func<SamplePath, string> method)[] funcs)
{
SamplePath path = new SamplePath(new[]
{
new PathSegment("oneoneone"),
new PathSegment("twotwotwo"),
new PathSegment("threethreethree")
});
string expected = "oneoneone/twotwotwo/threethreethree";
foreach (var (name, method) in funcs)
{
string actual = method(path);
if (actual != expected)
{
throw new Exception($"Test failed for method {name}. Expected '{expected}' but got '{actual}'");
}
}
}
Method pathName Mean Error StdDev Gen0 Allocated
WithStringBuilder longPath 274.86 ns 126.345 ns 6.925 ns 0.2856 1232 B
WithJoin longPath 138.62 ns 29.393 ns 1.611 ns 0.1056 456 B
WithJoinAndArrayPool longPath 178.99 ns 18.947 ns 1.039 ns 0.0834 360 B
WithCharArray longPath 198.61 ns 32.978 ns 1.808 ns 0.1669 720 B
WithNoTempAllocCharArray longPath 169.25 ns 25.699 ns 1.409 ns 0.0834 360 B
WithStringBuilder shortPath 58.52 ns 146.921 ns 8.053 ns 0.0352 152 B
WithJoin shortPath 55.31 ns 34.871 ns 1.911 ns 0.0222 96 B
WithJoinAndArrayPool shortPath 80.61 ns 28.241 ns 1.548 ns 0.0111 48 B
WithCharArray shortPath 69.61 ns 12.681 ns 0.695 ns 0.0241 104 B
WithNoTempAllocCharArray shortPath 59.23 ns 3.982 ns 0.218 ns 0.0111 48 B
WithStringBuilder verLongPath 565.84 ns 20.285 ns 1.112 ns 0.5779 2496 B
WithJoin verLongPath 412.97 ns 214.625 ns 11.764 ns 0.2966 1280 B
WithJoinAndArrayPool verLongPath 447.89 ns 49.392 ns 2.707 ns 0.2408 1040 B
WithCharArray verLongPath 586.33 ns 327.525 ns 17.953 ns 0.4816 2080 B
WithNoTempAllocCharArray verLongPath 489.28 ns 108.696 ns 5.958 ns 0.2403 1040 B
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SegmentsPathString;
internal class SamplePath
{
public SamplePath(IEnumerable<PathSegment> segments)
{
Segments = segments.ToList();
}
public IReadOnlyList<PathSegment> Segments { get; private set; }
}
class PathSegment
{
public PathSegment(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment