Skip to content

Optimize string.Join() (internally JoinCore) for IEnumerable<string> that is Collection<string> #116260

Closed
@Domik234

Description

@Domik234

Description

When calling string.Join() that calls JoinCore<T> in String.Manipulation.cs, if the IEnumerable<string> parameter is a Collection<string>, it is ~2x slower than when it is a List<string>.

I guess that this is because line 925 only checks for List<string>, and does not check for IList<string>. As a result, collections that implement IList<string> (such as Collection<string>, ReadOnlyCollection<string>) do not benefit from the optimized path.

The Collection class uses IList as internal items storage (Source) so couldn't be used it's internal IList<T> for this purpose?

Context

Simple benchmark

Code
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class TestCase01
{
    Collection<string> collection;
    List<string> list;
    public TestCase01()
    {
        collection = [];
        for (var i = 0; i < 25; i++)
        {
            collection.Add(Random.Shared.Next(10000, 1000000).ToString());
        }
        list = [.. collection];

    }

    [Benchmark]
    public string CollectionToRangeString_NoStringJoin()
    {
        var count = collection.Count;
        StringBuilder sb = new();
        sb.Append('{');
        for (int i = 0; i < count; i++)
        {
            sb.Append(collection.ElementAt(i));

            if (i < count - 1)
                sb.Append(", ");
        }
        sb.Append('}');

        return sb.ToString();
    }

    [Benchmark]
    public string CollectionToRangeString2_StringJoinChar()
    {
        StringBuilder sb = new();
        sb.Append('{');
        sb.Append(string.Join(',', collection));
        sb.Append('}');

        return sb.ToString();
    }

    [Benchmark]
    public string CollectionToRangeString3_StringJoinString()
    {
        StringBuilder sb = new();
        sb.Append('{');
        sb.Append(string.Join(", ", collection));
        sb.Append('}');

        return sb.ToString();
    }

    [Benchmark]
    public string ListToRangeString_NoStringJoin()
    {
        var count = list.Count;
        StringBuilder sb = new();
        sb.Append('{');
        for (int i = 0; i < count; i++)
        {
            sb.Append(list.ElementAt(i));

            if (i < count - 1)
                sb.Append(", ");
        }
        sb.Append('}');

        return sb.ToString();
    }

    [Benchmark]
    public string ListToRangeString2_StringJoinChar()
    {
        StringBuilder sb = new();
        sb.Append('{');
        sb.Append(string.Join(',', list));
        sb.Append('}');

        return sb.ToString();
    }

    [Benchmark]
    public string ListToRangeString3_StringJoinString()
    {
        StringBuilder sb = new();
        sb.Append('{');
        sb.Append(string.Join(", ", list));
        sb.Append('}');

        return sb.ToString();
    }
}

Results:

| Method                                    |  Job/Rt  | Mean     | Error   | StdDev  | Gen0   | Gen1   | Allocated |
|------------------------------------------ |--------- |---------:|--------:|--------:|-------:|-------:|----------:|
| CollectionToRangeString_NoStringJoin      | .NET 8.0 | 284.4 ns | 5.22 ns | 6.41 ns | 0.1025 |      - |   1.26 KB |
| CollectionToRangeString2_StringJoinChar   | .NET 8.0 | 304.7 ns | 3.67 ns | 3.43 ns | 0.1335 |      - |   1.64 KB |
| CollectionToRangeString3_StringJoinString | .NET 8.0 | 265.5 ns | 2.50 ns | 2.09 ns | 0.1483 | 0.0005 |   1.82 KB |
| ListToRangeString_NoStringJoin            | .NET 8.0 | 283.3 ns | 3.33 ns | 2.95 ns | 0.1030 |      - |   1.27 KB |
| ListToRangeString2_StringJoinChar         | .NET 8.0 | 153.7 ns | 2.38 ns | 2.23 ns | 0.1326 |      - |   1.63 KB |
| ListToRangeString3_StringJoinString       | .NET 8.0 | 210.6 ns | 2.49 ns | 2.08 ns | 0.1478 | 0.0005 |   1.81 KB |
|--.NET 9.0-------------------------------- |--------- |---------:|--------:|--------:|-------:|-------:|----------:|
| CollectionToRangeString_NoStringJoin      | .NET 9.0 | 222.7 ns | 3.00 ns | 2.66 ns | 0.1032 | 0.0002 |   1.27 KB |
| CollectionToRangeString2_StringJoinChar   | .NET 9.0 | 305.6 ns | 3.96 ns | 3.51 ns | 0.1354 |      - |   1.66 KB |
| CollectionToRangeString3_StringJoinString | .NET 9.0 | 244.3 ns | 4.82 ns | 4.74 ns | 0.1516 |      - |   1.86 KB |
| ListToRangeString_NoStringJoin            | .NET 9.0 | 214.9 ns | 4.04 ns | 3.78 ns | 0.1032 | 0.0002 |   1.27 KB |
| ListToRangeString2_StringJoinChar         | .NET 9.0 | 143.7 ns | 1.73 ns | 1.45 ns | 0.1326 |      - |   1.63 KB |
| ListToRangeString3_StringJoinString       | .NET 9.0 | 199.4 ns | 2.90 ns | 2.57 ns | 0.1459 | 0.0002 |   1.79 KB |

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions