Optimize sorting by leveraging ArrayPool for reduced allocations in ParameterCollection.#4331
Optimize sorting by leveraging ArrayPool for reduced allocations in ParameterCollection.#4331danielmarbach wants to merge 1 commit intoaws:developmentfrom
Conversation
| } | ||
| finally | ||
| { | ||
| ArrayPool<string>.Shared.Return(stringArray); |
There was a problem hiding this comment.
Safer to clear?
|
Using the pool here seems safe since all generated enumerator paths need to dispose of the enumerator, which triggers the finally. If someone were to acquire the enumerator manually and not dispose of it, then the finally would not be called. Given this is a wrong usage of the enumerator API and the method is internal, I would not be concerned about this. Additionally, there is also this hint
Which indicates it is not the end of the world. But again, if someone manually acquires the enumerator and doesn't follow the disposal guidelines, I guess that abuse of the internal method deserves to be punished with a small perf hit ;) |
|
@peterrsongg This is ready for review |
Just doing our due-diligence on this. Someone else on the team is actively looking into this and has this assigned to them as I got pulled into something else. But I will be the second reviewer |
| sortedStringListParameterValue.Sort(StringComparer.Ordinal); | ||
| foreach (var listValue in sortedStringListParameterValue) | ||
| yield return new KeyValuePair<string, string>(name, listValue); | ||
| { |
There was a problem hiding this comment.
I checked the values of the parameters dictionary for the reproduction code provided in #1922 and none of them are StringListParameterValue or DoubleListParameterValue
All of them are StringParameterValue, which means this change wont affect the issue and we wont make any improvements for the memory allocation.
This change looks useful to me and it would improve the allocations for cases where there are ListParameterValues, but I wouldn't recommend merging it until we figure out the root cause of the issue as we might need to rework how GetParametersEnumerable work.
Maybe revert to using GetSortedParametersList for now? What do you think @danielmarbach ?
There was a problem hiding this comment.
Overall when I did the small improvements here in the past I deliberately left out any redesigns. The entire parameter valua collection being mutable would be one thing to look at besides others.
Judging by the benchmark I ran this seems still giving good allocation gains that benefit's all SDK clients that run through this path I'd say it is worth and safe to keep.
|
Hi @danielmarbach, |
|
No worries. There is no rush |
There was a problem hiding this comment.
Pull request overview
This PR fixes a mutation bug in ParameterCollection.GetParametersEnumerable() where calling the method would sort the original StringListParameterValue and DoubleListParameterValue lists in place, modifying the underlying data on every call. The fix uses ArrayPool<T>.Shared.Rent() to obtain temporary arrays for sorting, avoiding both the mutation bug and heap allocations.
Changes:
- Replaced in-place sorting of original lists with
ArrayPool-backed temporary arrays for bothStringListParameterValueandDoubleListParameterValuecases inGetParametersEnumerable() - Added
using System.Buffersimport to supportArrayPool<T>usage
You can also share your feedback on Copilot code review. Take the survey.
| * permissions and limitations under the License. | ||
| */ | ||
| using System; | ||
| using System.Buffers; |
There was a problem hiding this comment.
This PR modifies Core SDK code but does not include a DevConfig file, which is required per CONTRIBUTING.md for all code changes. Since this is a bug fix (preventing in-place mutation of the original lists), a DevConfig file with a "core" section and "type": "patch" should be added under generator/.DevConfigs/.
| { | ||
| var stringList = stringListParameterValue.Value; | ||
| var stringArray = ArrayPool<string>.Shared.Rent(stringList.Count); | ||
| try | ||
| { | ||
| stringList.CopyTo(stringArray); | ||
| Array.Sort(stringArray, 0, stringList.Count, StringComparer.Ordinal); | ||
| for (int i = 0; i < stringList.Count; i++) | ||
| yield return new KeyValuePair<string, string>(name, stringArray[i]); | ||
| } | ||
| finally | ||
| { | ||
| ArrayPool<string>.Shared.Return(stringArray); | ||
| } |
There was a problem hiding this comment.
The key motivation for this change is to fix a mutation bug where calling GetParametersEnumerable() (or GetSortedParametersList()) would sort the original lists in place. However, there are no tests verifying this non-mutation behavior. Consider adding a test that calls GetSortedParametersList() (or GetParametersAsString) on a ParameterCollection containing unsorted string/double lists, and then asserts that the original lists remain in their original unsorted order. The existing test in AWSSDKUtilsTests.cs already tests GetParametersAsString with sorted data, so a complementary test with unsorted data would also verify correct sorted output while confirming non-mutation.
f30fa50 to
fbec458
Compare
…arameterCollection.
|
The commit will no longer be co-authored? |
fbec458 to
af0e476
Compare
|
@danielmarbach I rewrote your commit using the same commands that I usually use for rewriting my commits which added me as the author, then it took me a while to figure out how to add you back as the author, didn't mean to take the credit for your change 😅 I'm currently running a dry run for this PR in our internal build system, will approve it when it passes. |
|
@muhammad-othman no worries. I'm not easily offended 😜 I just figured I'll ask. Happy coding! |

Alternative to #4330
Commit
37a9afeoptimizedGetParametersAsStringto useGetParametersEnumerable()directly instead ofGetSortedParametersList().ToList(). While this avoided the intermediateList<KeyValuePair<string,string>>allocation, it exposed the pre-existing mutation bug where the original lists were being sorted in-place on every call.Use
ArrayPool<T>.Shared.Rent()to obtain temporary arrays for sorting, then return them to the pool. This approach:Benchmark Results
BenchmarkDotNet v0.15.0 shows the
ArrayPool.Rentapproach is competitive with alternatives:ArrayPool.Renthas identical performance tonew List<T>(copy)but with ~40% less memory allocationArrayPool.Rentis ~25% faster than LINQ OrderBy with ~3x less memory allocationAlthough the span approach doesn't work due to the iterator. It is just here for comparison
Benchmark
Details
```csharp using System; using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices;using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
namespace MutationBenchmark
{
[MemoryDiagnoser]
[SimpleJob]
[Config(typeof(Config))]
public class ParameterSortingBenchmarks
{
private List _stringList;
private List _doubleList;
}