-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Background and Motivation
String.Join currently has a overloads for a string or char as a separator and a string[] or IEnumerable for the values. The implementation for string[] as the values parameter is optimized around calculating the total length of the final string, allocating it, and filling it in. The IEnumerable implementation uses a stringbuilder to aggregate the string and eventually do a memory copy for the final string. By supporting IList as a overload it will have similar runtime characteristics of the string[] implementation.
A microbenchmark of an array vs. Ilist of 5 small strings joined by a character shows the benefits of the string[] approach.
| Method | Job | Runtime | Mean | Error | StdDev | Gen 0 | Allocated |
|---|---|---|---|---|---|---|---|
| Array | .NET 4.7.2 | .NET 4.7.2 | 73.30 ns | 1.495 ns | 2.807 ns | 0.0204 | 88 B |
| Array | .NET Core 3.1 | .NET Core 3.1 | 50.30 ns | 1.026 ns | 1.007 ns | 0.0185 | 80 B |
| Array | .NET Core 5.0 | .NET Core 5.0 | 45.87 ns | 0.941 ns | 1.350 ns | 0.0185 | 80 B |
| IList | .NET 4.7.2 | .NET 4.7.2 | 130.44 ns | 2.634 ns | 5.011 ns | 0.0277 | 120 B |
| IList | .NET Core 3.1 | .NET Core 3.1 | 105.68 ns | 2.142 ns | 3.072 ns | 0.0259 | 112 B |
| IList | .NET Core 5.0 | .NET Core 5.0 | 85.58 ns | 1.706 ns | 2.501 ns | 0.0259 | 112 B |
Proposed API
namespace System
{
public partial class String {
+ public static string Join(char separator, IList<string?> values) { }
+ public static string Join(string? separator, IList<string?> values) { }
}
}Usage Examples
private static string CreateMetricCacheKey(IList<string> metricDimensionNames) {
return string.Join('|', metricDimensionNames);
}Alternative Designs
There aren't any clear alternatives on the runtime implementation side. Individuals can already refactor their codebase to use arrays instead of Ilist and gain the same performance benefits.
Risks
At the bottom of JoinCore for string?[] there is the following snippet. Because an IList would need a different clone there may be code duplication of JoinCore.
// If we copied exactly the right amount, return the new string. Otherwise,
// something changed concurrently to mutate the input array: fall back to
// doing the concatenation again, but this time with a defensive copy. This
// fall back should be extremely rare.
return copiedLength == totalLength ?
result :
JoinCore(separator, separatorLength, (string?[])value.Clone(), startIndex, count);