Repository navigation
Create optimized ODataPath cloning constructor - #2489
Conversation
60dd270 to
17fbb7e
Compare
|
This PR has Quantification details
Why proper sizing of changes matters
Optimal pull request sizes drive a better predictable PR flow as they strike a
What can I do to optimize my changes
How to interpret the change counts in git diff output
Was this comment helpful? 👍 :ok_hand: :thumbsdown: (Email) |
|
Not sure I understand what you mean by this statement:
Are you saying that if you make this constructor public that will result into existing user code automatically invoking it instead of the constructor that is being invoked presently? |
|
@habbes The following statement was confusing to me at first:
"same capacity" is what threw me off (almost). I think you should say "Create a new List with the capacity equal to the length of the existing list" |
|
@habbes Under scenario 4, what does int capacity = odataPath.Segments.Length < odataPath.segments.Count ? odataPath.segments.Capacity : odataPathSegments.Capacity + 1;Did you intend: int capacity = odataPath.segments.Count < odataPath.segments.Capacity ? odataPath.segments.Capacity : odataPathSegments.Capacity + 1;You also say that you based this PR on scenario 4 but the code in this PR is different |
|
@habbes Curious whether there are places in the existing code that could make use of this new constructor. |
|
@habbes I'm not sure how much we do equality checks for |
Yes, that is precisely what I mean. Currently we have a constructor with the existing signature: public ODataPath(IEnumerable<ODataPathSegment>);
So we have code internally that create a copy of the ODataPath oldPath;
var newPath = new ODataPath(oldPath);Look at the This PR adds a new a constructor with the signature: internal ODataPath(ODataPath);Now the Note that this change won't affect calls that pass a list of segments directly, or any other enumerable for the matter. It only "hijacks" interal calls that pass an Let me know if that clears things up. |
Yes, you're right, that is what I meant: int capacity = odataPath.segments.Count < odataPath.segments.Capacity ? odataPath.segments.Capacity : odataPathSegments.Capacity + 1;Also you're right that this slightly differs from the actual code in the PR. I must have changed after writing the description. The initial thought was to set the same capacity as the input list if was already greater than the length. But I didn't notice any improvements in the benchmarks from doing that. The thought was that doing that may reduce resizing. But that benefit didn't materialize because only one segment is going to be added to this newly created list. The next segment after that is going to be added to a whole new |
Yes, I have mentioned an example in a comment above: #2489 (comment) |
Yeah, we could make that improvement. But I have seen a couple of other places that could benefit from using plain loops to iterate over the ODataPath. I think I could bundle them up in a separate PR, what do you think? |
Issues
*This pull request fixes #2488
Description
Creates a new constructor that takes
ODataPathas input and usesodataPath.segmentsas input to copy the list of segments to the newODataPath. Since we're passing aListto aList, it uses a faster copy usingArray.Copyinstead of iterating through theIEnumerableto add items to the list one at a time, which is less memory and CPU efficient. See List constructor and InsertRange implementations.I made the new constructor internal to be on the safe side. To me it appears safe enough to be public. But, If I make it public, a different method overload will get called without the user having to opt-in to it or without the user having to change arguments. I'm not sure whether that's desirable or not.
On the following benchmarks, it sheds off about 2MB of allocations. About 0.8% reduction. Not that significant here. But I expect it will reduce around 0.3-0.4% in AGS based on how much these allocations already contribute in AGS (>.6%). I think that's not bad for such a small change.
Before
After
This approach eliminates array resizes in the
ODataPathconstructor. Most of the time the path when we're about to add a new segment. To avoid resizing the list when the new segment is added, I also create the new list with enough room for the new segment. I tried a number of alternatives to evaluate the trade-offs between avoiding resizing and pre-allocation/copying arrays that are too big. I evaluated the different options against the following benchmarks and picked the one that seemed to have the best balanceHere are the results:
1. Baseline: Existing implementation
2. Create a new List with capacity equal to the length of the existing list
The new list only has enough capacity for the current segments. This avoids memory waste if we don't add new segments. But it always leads to a resize when the next segment is added.
3. Set the capacity of the new List to the capacity of the existing List
This is a slight optimization from the previous case. It avoids resizes by relying on the assumption that in most cases the capacity will be greater than the number of segments in the list. However, if the input is already at capacity, the next segment will force a resize.
4. Set the capacity to one more than the size of the list: THIS PR
This creates a new List with enough room to store one more element. This ensures the next segment after the clone will not resize the array. Of course we allocate more memory than needed if we don't resize. But all but the 2nd alternative have that issue.
5. Set the capacity to 4 more than the size of the list
This is a slight variation from the previous one suggested by @gathogojr. We wanted to see whether creating more room will reduce the number of resizes if we have many consecutive new segments. However, it performs worse than the previous one on the benchmarks:
6. Double the capacity before of the input list
This is also a variation of the previous 2, aimed at checking whether pre-allocating a larger capacity will reduce resizes.
The conclusion I made is that while the last 2 may reduce resizes, each new segment added has to clone the path first. That means that an array of a similar size or greater has to be allocated each time a new item is added. For that reason, it's not beneficial to ensure capacity for more than the new segment being added.
This chosen approach is still fairly inefficient as it requires copying the entire array when adding a new segment. There are more efficient implementations we could consider, for example different clones could share the same list of segments or a linked-list of segments like persistent. But the purpose of this PR was a quick, low-hanging optimization with minimal changes while we continue to conduct more investigations in this area.
Checklist (Uncheck if it is not completed)
Additional work necessary
If documentation update is needed, please add "Docs Needed" label to the issue and provide details about the required document change in the issue.