-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Remove extra allocations in PSMemberInfoInternalCollection<T> #7435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove extra allocations in PSMemberInfoInternalCollection<T> #7435
Conversation
PSMemberInfoInternalCollection<T> uses internally OrderedDictionary class. The OrderedDictionary class in .Net Core 2.1 allocates internally ArrayList and Hashtable after any first using any property. Even if we check that OrderedDictionary is empty (Count==0) or use indexer it causes allocations. The commit postpones the internall allocations until this is really necessary. Taking into account that PSMemberInfoInternalCollection<T> is used in each PSObject and is often empty, this change dramatically reduces memory allocations.
|
@SteveL-MSFT If the PR is ok you could consider it for Windows PowerShell 5.1 too. @daxian-dbw Sorry for ping - the PR can be important. @markekraus Could you please check json cmdlets with the change if you have a time. I hope we get a win there too. @vancem Thanks for your great analysis - it helped a lot! |
SteveL-MSFT
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. At this time we're trying not to make changes to 5.1 as we're trying to make 6 a viable replacement for 5.1 and spending time on that instead.
| lock (Members) | ||
| { | ||
| return _members[name] as T; | ||
| return Members[name] as T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: since we already know _member != null when reaching here, maybe we can just continue to use _member for this lock (...) block. It's a little bit more consistent when reading the code.
| if (_members == null) | ||
| { | ||
| _members = new OrderedDictionary(StringComparer.OrdinalIgnoreCase); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There could be a race condition here.
In an exaggerated way, thread A checked _members == null and entered the if block, then A got suspended, and thread B came in and used Members (so _member is initialized). When A got resumed, it would re-create the OrderedDictionary object, and thus any members B added previously will be gone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest the following code:
if (_members == null)
{
Interlocked.CompareExchange(ref _members, new OrderedDictionary(StringComparer.OrdinalIgnoreCase), null);
}
return _members;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch!
Fixed.
| } | ||
|
|
||
| lock (_members) | ||
| lock (Members) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we also check for _members == null for the Remove action as a shortcut path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can add the check in public methods. Moreover, this code can be used in the most unpredictable scenarios
Fixed.
| { | ||
| PSMemberInfoInternalCollection<T> returnValue = new PSMemberInfoInternalCollection<T>(); | ||
| lock (_members) | ||
| lock (Members) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, shall we also check for _members == null for a shortcut path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| lock (Members) | ||
| { | ||
| return _members.Count; | ||
| return Members.Count; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same nitpick here, how about just use _members for the lock(...) block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| lock (Members) | ||
| { | ||
| return _members.Count - _countHidden; | ||
| return Members.Count - _countHidden; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same nitpick here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| lock (Members) | ||
| { | ||
| return _members[index] as T; | ||
| return Members[index] as T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same nitpick here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| public override IEnumerator<T> GetEnumerator() | ||
| { | ||
| lock (_members) | ||
| lock (Members) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we want to go a bit far, maybe also check _members == null here and if so return Array.Empty<T>.GetEnumerator()?
Just an idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea for the public method.
Fixed as return Enumerable.Empty<T>().GetEnumerator();
|
@daxian-dbw I reverted |
|
I measure pwsh.exe startup and update PR description:
|
|
@iSazonov The issue with JSON is one of speed more than memory allocation. I don't have he tooling to test the memory performance. but the code I use to test the JSON performance is this: $json = @'
{
"id": "string",
"data": [
{
"accountId": 0,
"productId": 0,
"resourceLocationId": 0,
"consumedServiceId": 0,
"departmentId": 0,
"accountOwnerEmail": "string",
"accountName": "string",
"serviceAdministratorId": "string",
"subscriptionId": 0,
"subscriptionGuid": "string",
"subscriptionName": "string",
"date": "2017-04-27T23:01:43.799Z",
"product": "string",
"meterId": "string",
"meterCategory": "string",
"meterSubCategory": "string",
"meterRegion": "string",
"meterName": "string",
"consumedQuantity": 0,
"resourceRate": 0,
"Cost": 0,
"resourceLocation": "string",
"consumedService": "string",
"instanceId": "string",
"serviceInfo1": "string",
"serviceInfo2": "string",
"additionalInfo": "string",
"tags": "string",
"storeServiceIdentifier": "string",
"departmentName": "string",
"costCenter": "string",
"unitOfMeasure": "string",
"resourceGroup": "string"
}
],
"nextLink": "string"
}
'@
$json = 1..1000 | % { $json }
$json = '[' + ($json -join ',') + ']'
Measure-Command {$Json | ConvertFrom-Json } | % TotalMilliseconds5.1: 375.3416ms so it is marginally better than 6.1.0-preview.4, better than 6.0.2, but still nowhere near the performance of 5.1. |
|
@markekraus Many thanks for your repo script! I'll try to look it deeper next week. Update: I did fast look - after #7413 and the PR I think we get a win in ConvertFrom-Json too. |
|
@iSazonov You cannot change all |
|
@markekraus By replacing that check, and by keeping a hashset of preValidated member, I can speed it up by a factor of 3 or so. |
| } | ||
|
|
||
| lock (Members) | ||
| lock (_members) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@daxian-dbw Please clarify - is it ok to lock on different objects in different methods? What if different threads call Add() and Remove() in the same time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All locks are essentially on the same object. When Add and Remove are called at the same time, there are two possibilities:
- The check of
_members == nullinRemovehappens before the initialization of_membertriggered byAdd; - The check of
_members == nullinRemovehappens after the initialization of_membertriggered byAdd.
In the first case, Remove will just return, and Add will lock on the newly initialized object.
In the second case, Remove will try to lock on _members (once it's not null, it will never be set back to null), which points to the same object as Member, so Remove and Add will compete locking on the same object.
|
@powercode Using preValidated looks as internal only hack. It will be nice to find more generic solution that we can expose to public. |
|
@iSazonov I agree about It isn't obvious how though. It would require some sort of scope, where I say now I'm gonna add a bunch of similar objects - do validation only on the first one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to get a local reference to the Members instead of calling the property repeatedly?
var members = Members;
lock (members)
{
if (members[member.Name] is T existingMember)
{
Replace(existingMember, member);
}
else
{
members[member.Name] = member;
if (member.IsHidden)
{
_countHidden++;
}
}
}
|
One test failed in AppVeyor, but works fine on my dev box. Will address @powercode's comment and trigger the CIs again. |
|
Thanks all for help and useful comments! |
|
OrderedDictionary was fixed in dotnet/corefx#32001 - some properies now don't allocate if no need. |
PR Summary
Related #7112.
PSMemberInfoInternalCollection uses internally OrderedDictionary class.
The OrderedDictionary class in .Net Core 2.1 allocates internally ArrayList and Hashtable after any first using any property. Even if we check that OrderedDictionary is empty (Count==0) or use indexer it causes allocations.
The commit postpones the internall allocations until this is really necessary.
Taking into account that PSMemberInfoInternalCollection is used in each PSObject and is often empty, this change dramatically reduces memory allocations.
PerfView results
Before fix:
After the fix:
Test script:
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:to the beginning of the title and remove the prefix when the PR is ready.[feature]if the change is significant or affects feature tests