Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C cpu cache behavior

Apple provides some documentation about synchronizing variables and even order of execution. What I don't see is any documentation on CPU cache behavior. What guarantees and control does the Objective-C developer have to ensure cache coherence between threads?

Consider the following where a variable is set on a background thread but read on the main thread:

self.count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
  self.count = 5;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%i", self.count);
  });
}

Should count be volatile in this case?

Update 1

The documentation in Inter-thread Communication guarantees that shared variables can be used for inter-thread communication.

Another simple way to communicate information between two threads is to use a global variable, shared object, or shared block of memory.

Does this imply volatile is not required in this case? This is conflicting with the documentation in Memory Barriers and Volatile Variables:

If the variable is visible from another thread however, such an optimization might prevent the other thread from noticing any changes to it. Applying the volatile keyword to a variable forces the compiler to load that variable from memory each time it is used.

So I still don't know whether volatile is required because the compiler could use register caching optimizations or if it's not required because the compiler somehow knows it's a "shared" something.

The documentation is not very clear about what a shared variable is or how the compiler knows about it. In the above example, is count a shared object? Let's say count is an int, then it's not an object. Is it a shared block of memory or does that only apply to __block declared variables? Maybe volatile is required for non-block, non-object, non-global, shared variables.

Update 2

To everyone thinking this is a question about synchronization, it's not. This is about CPU cache behavior on the iOS platform.

like image 762
John K Avatar asked Feb 20 '17 01:02

John K


1 Answers

I know you are probably asking about the general case of using variables across threads (in which case the rules about using volatile and locks are the same for ObjC as it is for normal C). However, for the example code you posted the rules are a little different. (I'll be skipping over and simplifying things and using Xcode to mean both Xcode and the compiler)

self.count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
  self.count = 5;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%i", self.count);
  });
}

I'm going to assume self is an NSObject subclass something like this:

@interface MyClass : NSObject {
    NSInteger something;
}
@property (nonatomic, assign) NSInteger count;
@end

Objective C is a superset of C, and if you've ever done any reverse engineering of ObjC you'll know that ObjC code (sort of, not quite) gets converted into C code before it's compiled. All [self method:object] calls get converted to objc_msgSend(self, "method:", object) calls and self is a C struct with ivars and other runtime info in it.

This means this code doesn't do quite what you might expect.

-(void)doThing{
   NSInteger results = something + self.count;
}

Just accessing something isn't just accessing the variable but is instead doing self->something (which is why you need to get a weak reference to self when accessing an ivar in an Objective C block to avoid a retain cycle).

The second point is Objective C properties don't really exist. self.count gets turned into [self count] and self.count = 5 gets turned into [self setCount:5]. Objective C properties are just syntax sugar; convenience save you some typing and make things look a bit nicer.

If you've been using Objective C for more than a few years ago you'll remember when you had to add @synthesize propertyName = _ivarName to the @implementation for ObjC properties you declared in the header. (now Xcode does it automatically for you)

@synthesize was a trigger for Xcode to generate the setter and getter methods for you. (if you hadn't written @synthesize Xcode expected you to write the setter and getter yourself)

// Auto generated code you never see unless you reverse engineer the compiled binary
-(void)setCount:(NSInteger)count{
    _count = count;
}
-(NSInteger)count{
    return _count;
}

If you are worried about threading issues with self.count you are worried about 2 threads calling these methods at once (not directly accessing the same variable at once, as self.count is actually a method call not a variable).

The property definition in the header changes what code is generated (unless you implement the setter yourself).

@property (nonatomic, retain)
[_count release];
[count retain];
_count = count;

@property (nonatomic, copy)
[_count release];
_count = [count copy];

@property (nonatomic, assign)
_count = count;

TLDR

If you care about threading and want to make sure you don't read the value half way through a write happening on another thread then change nonatomic to atomic (or get rid of nonatomic as atomic is the default). Which will result in code generated something like this.

@property (atomic, assign) NSInteger count;

// setter
@synchronized(self) {
    _count = count;
}

This won't guarantee your code is thread safe, but (as long as you only access the property view it's setter and getter) should mean you avoid the possibility of reading the value during a write on another thread. More info about atomic and nonatmoic in the answers to this question.

like image 165
Kyle Howells Avatar answered Oct 02 '22 16:10

Kyle Howells