RequestData.CreatePathWithQueryStrings Performance Optimisation
See original GitHub issueFurther to my PR #4952, I have been reviewing the calling code to look at potential further performance optimisations.
CreatePathWithQueryStrings
is called within the RequestData
ctor to populate the PathAndQuery
property for every instance. The current implementation builds a string after populating NameValueCollection
instances from both the global connection config and the request parameters. Eventually, it calls down to the optimised NameValueCollection
extension method.
I’ve created a prototype of an optimised version of this method, which rents an oversized Span<char>
from the ArrayPool
and builds the path and query within that buffer. One key change this includes, is to avoid creating a copy of the existing IConnectionConfigurationValues.QueryStringParameters
collection and converting the IRequestParameters.QueryString
dictionary to a NameValueCollection
. Instead, it performs the conversion of object
s from the dictionary directly into the buffer from the pool.
The logic I’ve added so far is simplified for prototyping. It would require some tests to properly validate that it matches (or can match) the existing behaviour. Having benchmarked the prototype for one example request, the potential optimisation is fairly significant.
| Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------------- |-----------:|----------:|------------:|-----------:|-------:|------:|------:|----------:|
| CreatePathWithQueryStrings | 7,879.6 ns | 390.67 ns | 1,139.61 ns | 7,195.2 ns | 0.3357 | - | - | 1464 B |
| CreatePathWithQueryStringsV2 | 648.0 ns | 12.86 ns | 12.03 ns | 648.9 ns | 0.0305 | - | - | 128 B |
This equates to a 91.2% reduction in execution time and 91.3% fewer bytes allocated. The final bytes here is pretty much just the creation of the final string, with little or no additional overhead. Given that this occurs per request instance, it seems like a reasonable improvement to go after.
For reference: In the case of the above benchmarks, I used connection settings as follows:
_connectionSettings.GlobalQueryStringParameters(new NameValueCollection
{
{ "allow_no_indices", "false" },
{ "global_key", "thing" } // made up for testing
});
and a request:
new SearchRequestParameters { AllowNoIndices = true, DocValueFields = new[] { "item1" } };
Before investigating this further, I wanted to check if you agree that the risk vs. reward would be considered acceptable in this case. Given it’s a pretty important method for building valid requests, so it’s sensitive should any regressions occur. That said, with sufficient tests, the existing logic is not too complex to thoroughly test before making the change.
There is also a comment from @Mpdreamz in the code…
// TODO This feels like its in the wrong place
I’d consider adding a type (helper) capable of encapsulating the path and query building functionality. If that’s something you’d consider at this point, given that it’s a public API, would there be a preferred strategy for supporting a change. It’s a very functional method, unlikely to be replaced during testing or with alternative runtime implementations.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:5 (5 by maintainers)
Top GitHub Comments
Thanks for your effort and focus here, @stevejgordon!
Thanks @Mpdreamz! I’m away on holiday for the remainder of this week, but I’ll continue to take a look at this upon my return. Once I have the core cases working reliably, I’ll get a PR in to review from there.