Skip to content

NameValueCollectionExtensions Performance Optimisation #4951

Closed
@stevejgordon

Description

@stevejgordon

Would you be open to taking PR(s) for performance optimisations, specifically for cases where the NEST library is consumed within NetStandard 2.1 and NetCoreApp 2.1 apps?

I was exploring NameValueCollectionExtensions and one simple example is the ToQueryString method. By applying Span<T> optimisation, it's possible to reduce the allocations fairly significantly. Given this is called every time a RequestData instance is constructed it may have worthwhile affect for consumers.

I've created a benchmark and an initial optimised implementation with the following results:

|   Method |     Mean |     Error |    StdDev | Ratio | RatioSD |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------- |---------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:|
| Original | 4.593 us | 0.1413 us | 0.4099 us |  1.00 |    0.00 | 0.2060 |     - |     - |     896 B |
|      New | 2.685 us | 0.1360 us | 0.3859 us |  0.93 |    0.12 | 0.1221 |     - |     - |     544 B |

This reduced allocated bytes by 39.3% and operates 42% quicker with my test data of 5 random keys and values.

A note regarding the execution time. My prototype takes a shortcut and uses a rented char[] from the ArrayPool with a minimum length of 512 chars. If there is a realistic maximum length expected for the qs, then hard-coding it rather than calculating the length each time is potentially worthwhile. With the length calculation the execution time is 4.227 us, so only slightly faster than current performance.

With my test data (where one key has a char that requires escaping), by applying custom Uri escaping, it may be possible to reduce a further 48 bytes. If it is common for keys/values to require escaping then the allocation reduction could be more significant.

If you feel such allocation reductions would have value to consumers and you'd be open to such a PR, I'd happily submit this?

Example of prototype code...

internal static string ToQueryStringNew(this NameValueCollection nv)
{
    if (nv == null || nv.AllKeys.Length == 0) return string.Empty;

    var length = 1 + nv.AllKeys.Length - 1; // acct. for '?', and '&' chars
    foreach (var key in nv.AllKeys)
    {
        length += (key.Length * 3) + (nv[key].Length * 3) + 1; // worst case all escaped chars + '=' 
    }

    // possible to reduce execution time ~50% by hard-coding to 512 chars
    var buffer = ArrayPool<char>.Shared.Rent(length); 
    var bufferSpan = buffer.AsSpan();

    try
    {
        var position = 0;
        bufferSpan[position++] = '?';

        foreach (var key in nv.AllKeys)
        {
            if (position != 1)
                bufferSpan[position++] = '&';

            var escapedKey = Uri.EscapeDataString(key);
            escapedKey.AsSpan().CopyTo(bufferSpan.Slice(position));
            position += escapedKey.Length;

            var value = nv[key];
            if (!value.IsNullOrEmpty())
            {
                bufferSpan[position++] = '=';
                var escaped = Uri.EscapeDataString(value);                
                escaped.AsSpan().CopyTo(bufferSpan.Slice(position));
                position += escaped.Length;
            }
        }

        return new string(bufferSpan.Slice(0, position));
    }
    finally
    {
        ArrayPool<char>.Shared.Return(buffer);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions