Skip to content

Add optimized String.Join for IList<string> #44027

@johnthcall

Description

@johnthcall

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);

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions