-
Notifications
You must be signed in to change notification settings - Fork 806
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
Improve perf in KVO and NSString #2323
Conversation
Perf numbers, with some numbers from before #2240 for comparison Improvements across the board, but we're still slower than the old NSOperationQueue on adding operations (though not executing them). (Sorry for misleading you on this @rajsesh-msft, I forgot I switched hardware recently and would get lower numbers). The cost of addOperation: is currently about 2/3 KVO addObserver: and 1/3 libdispatch. The biggest cost in KVO as it currently stands is construction of objc objects - NSKVOObservationInfo et al seem like they could easily be lighter-weight structs, but this would be a fairly heavy change that I didn't want to get into while @DHowett-MSFT was out. If we want to further pursue KVO perf, I can discuss the issue with him once he returns. |
// Private helper that backs the default implementation of keyPathsForValuesAffectingValueForKey: | ||
// Returns nil instead of constructing an empty set if no keyPaths are found | ||
// Also used internally as a minor optimization, to avoid constructing an empty set when it is not needed | ||
static NSSet* _keyPathsForValuesAffectingValueForKey(Class self, NSString* key) { |
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.
nit: rename self to something that's not a keyword.
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 have other instances of 'self' being what it would normally be, were this an objc function. Wontfixing this.
CFIndex leftLength = CFStringGetLength(left); | ||
return leftLength == CFStringGetLength(right) && | ||
CFStringFindWithOptions(left, right, { 0, leftLength }, kCFCompareCaseInsensitive, nullptr); | ||
return CFStringCompare(left, right, kCFCompareCaseInsensitive) == kCFCompareEqualTo; |
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 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.
Sorry! Dx
} | ||
} | ||
return [NSSet set]; | ||
NSSet* ret = _keyPathsForValuesAffectingValueForKey(self, key); |
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.
_keyPathsForValuesAffectingValueForKey takes a Class but this is an NSObject?
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.
This is a + function, so self is a Class.
@@ -221,7 +221,9 @@ | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ClangCompile Include="$(StarboardBasePath)\tests\Benchmark\BenchmarkSampleTests.mm" /> | |||
<ClangCompile Include="$(StarboardBasePath)\tests\Benchmark\KVOBenchmarkTests.mm" /> |
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 is also a tests\testapps\kvoperf, which is probably obsolete now if we move all those tests 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.
I forgot about that - I don't think I'm comfortable removing it while @DHowett-MSFT is out, but I'll take a look and see if anything can be copied from there to 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.
That app is not used at all (it is a manual tests). When it was written, we didn't have the benchmark tests. It should be safe to remove once all the pieces of it are salvaged.
In reply to: 108012483 [](ancestors = 108012483)
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.
It's more or less the same scenario as the ObserveChange benchmark, but also measures the amount of time spent in the actual notification part, which the benchmark framework doesn't really have a mechanism for.
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 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.
The multiple observer scenario is important. We do see some improvement there so thats good to see.
In reply to: 108014282 [](ancestors = 108014282)
@@ -43,9 +43,7 @@ | |||
* Helper function that efficiently compares two CFStringRef and returns true if they are equal (case insensitive) |
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.
efficiently [](start = 24, length = 11)
oh the irony!
|
||
inline void Run() { | ||
for (NSObject* obj in _testObservers.get()) { | ||
[_testKVOObject addObserver:obj forKeyPath:@"intProperty" options:0 context:nullptr]; |
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.
NSArray now has the helper function to add observers for every object in the arraay, so you can cut this loop.
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.
Though that has the added cost of making an NSIndexSet* and is only a convenience wrapper
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 it would end up being harder to read because of the need to construct an NSIndexSet. Leaving as-is.
if (quickSet<type>(self, setter, value, valueType)) { \ | ||
return true; \ | ||
} \ | ||
case encodingChar: \ |
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.
No changes other than formatting in this file?
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.
Will back this out.
- Fixed a bug in CFStringGetCStringPtr where it called _fastCStringContents with an NSString encoding, but _fastCStringContents expected a CFString encoding. As a result, CFStringGetCStringPtr now actually returns something for most strings. - Similarly, [NSString UTF8String] on a literal now returns a fast pointer rather than an autorelease buffer. - Changed [NSString isEqualToString:], [NSConstantString hash], and [NSConstantString isEqualToString:] to rely on faster functions - Made a similar change to speed up a local helper (sorry @aballway i misled you) - Changed [Class keyPathsForValuesAffectingKey:] to use C-string parsing - added a private helper for keyPathsForValuesAffectingKey: to not construct the empty set, for internal usage - Changed NSKVOKeypathObserver to store an observer as assign instead of as weak. This makes us theoretically less safe if an observer deallocs without removing itself as an observer. However, the observed object already threw once it reached its dealloc and still had observers, and the above case is considered incorrect anyway, so it only allowed us to fail quietly in the case where the observed object has a much longer lifetime, which is of questionable value. Opting in favor of assign here, as it provided a substantial (-~40%) decrease in time it takes to addObserver: - Changed _dispatchWillChange to just take an NSMutableDictionary instead of doing an unnecessary mutableCopy - Minor change to setObservationInfo: to be atomic on the associated object table rather than the object itself - Added more benchmark tests for KVO, String scenarios Fixes microsoft#2040
Failure did not repro locally - i'll assume it's spurious and queue another CI build. |
@@ -740,13 +743,11 @@ - (NSString*)description { | |||
@Status Interoperable | |||
*/ | |||
- (BOOL)isEqualToString:(NSString*)compStr { | |||
RETURN_FALSE_IF(!compStr); | |||
|
|||
if ([compStr isKindOfClass:[NSString class]]) { |
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.
isKindOfClass [](start = 17, length = 13)
We can't use the short path for custom classes (such as NSStringOnHStting). Why is this removed?
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.
am confused - NSHSTRINGString isKindOfClass:[NSString class] would have returned YES 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.
Yes it would. Looking at CFStringCompareWithOptions, it can handle both non CF NSStrings as well as CFStrings, so this should be okay. Still confused about the original intent here :)
In reply to: 108258325 [](ancestors = 108258325)
// fast path using c string manipulation, will cover most cases, as most keyPaths are short | ||
char selectorName[sc_bufferSize] = "keyPathsForValuesAffecting"; // 26 chars | ||
selectorName[sc_prefixLength] = toupper(rawKey[0]); | ||
strcat_s(&selectorName[sc_prefixLength + 1], sc_bufferSize - sc_prefixLength - 1, &rawKey[1]); |
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.
strcat_s [](start = 12, length = 8)
strcpy instead since you have already computed the offset.
@@ -424,7 +450,7 @@ - (void*)observationInfo { | |||
@Status Interoperable | |||
*/ | |||
- (void)setObservationInfo:(void*)observationInfo { | |||
objc_setAssociatedObject(self, &s_kvoObservationInfoAssociationKey, (__bridge id)observationInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |||
objc_setAssociatedObject(self, &s_kvoObservationInfoAssociationKey, (__bridge id)observationInfo, OBJC_ASSOCIATION_RETAIN); |
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.
OBJC_ASSOCIATION_RETAIN [](start = 102, length = 23)
why this subtle change 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.
Changed to this from an @ synchronized elsewhere. More of a consistency change (this had maybe ~3% perf impact) - there was another call to this function that didn't do the synchronized for whatever reason, and the synchronization should probably occur within the body of this function rather than around it. If you want, I can change this to @ synchronized instead of using the associated objects table's locking, but I don't think it makes much of a difference.
@@ -238,7 +264,7 @@ static void _addNestedObserversAndOptionallyDependents(_NSKVOKeyObserver* keyObs | |||
|
|||
// Aggregate all keys whose values will affect us. | |||
if (dependents) { | |||
NSSet* valueInfluencingKeys = [[object class] keyPathsForValuesAffectingValueForKey:key]; | |||
NSSet* valueInfluencingKeys = _keyPathsForValuesAffectingValueForKey([object class], key); |
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.
valueInfluencingKeys [](start = 15, length = 20)
could be nil. messages to nil object should be okay but i would double check this on ARM (we have had funky issues there but not with integral type rets).
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.
seems to be fine - my only test failures were the CGGradient tests, which are a known issue. i doublechecked the nil message behavior in an isolated test as well.
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.
Just a few minor comments.
@@ -573,7 +591,7 @@ - (void)willChangeValueForKey:(NSString*)key { | |||
if (![self observationInfo]) { | |||
return; | |||
} | |||
_dispatchWillChange(self, key, @{ NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting) }); | |||
_dispatchWillChange(self, key, [NSMutableDictionary dictionaryWithObject:@(NSKeyValueChangeSetting) forKey:NSKeyValueChangeKindKey]); |
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.
Oh no! @aballway this is what actually introduced the "information leak" I was talking about.
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 told ya it was already there
but _fastCStringContents expected a CFString encoding. As a result, CFStringGetCStringPtr now actually returns something for most strings.
This makes us theoretically less safe if an observer deallocs without removing itself as an observer.
However, the observed object already threw once it reached its dealloc and still had observers,
and the above case is considered incorrect anyway,
so it only allowed us to fail quietly in the case where the observed object has a much longer lifetime, which is of questionable value.
Opting in favor of assign here, as it provided a substantial (-~40%) decrease in time it takes to addObserver:
Fixes #2040