PDA

View Full Version : NSNumber reference counting question




mduser63
Jun 1, 2006, 12:12 AM
I'm working my way through Stephen Kochan's Programming in Objective-C right now, and I've just finished Chapter 17 on memory management. The first exercise is to write a program to determine the effect on reference counts of adding and removing objects in a dictionary object. Anyway, I've got the following code:

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSNumber *num1, *num2, *num3;
NSString *string1, *string2, *string3;
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity: 3];

num1 = [NSNumber numberWithInt: 1];
num2 = [NSNumber numberWithInt: 2];
num3 = [NSNumber numberWithInt: 3];

string1 = [NSString stringWithString: @"first string"];
string2 = [NSString stringWithString: @"second string"];
string3 = [NSString stringWithString: @"third string"];

printf("\n num1 retain count before dictionary: %x\n", [num1 retainCount]);
printf("string1 retain count before dictionary: %x\n", [string1 retainCount]);
printf(" num2 retain count before dictionary: %x\n", [num2 retainCount]);
printf("string2 retain count before dictionary: %x\n", [string2 retainCount]);
printf(" num3 retain count before dictionary: %x\n", [num3 retainCount]);
printf("string3 retain count before dictionary: %x\n", [string3 retainCount]);

[pool release];
return 0;
}


When I compile and run it, I get the following:

num1 retain count before dictionary: 2
string1 retain count before dictionary: 1
num2 retain count before dictionary: 2
string2 retain count before dictionary: 1
num3 retain count before dictionary: 2
string3 retain count before dictionary: 1

Can anyone tell me why the NSNumber objects have a reference count of 2 after creation, while the strings only have a count of 1? I expected both types of objects to have a reference count of 1 after initialization.



HexMonkey
Jun 1, 2006, 12:54 AM
I think it's just some internal optimization that NSNumber does. Since NSNumbers are not mutable, it just returns the same object whenever initialised with certain common values.

This might be better illustrated with an example:

NSNumber *num1, *num2;
num1 = [NSNumber numberWithInt:1];
NSLog(@"num1 retain count after setting to 1: %d", [num1 retainCount]);
num2 = [NSNumber numberWithInt:1];
NSLog(@"num1 retain count after setting num2 to 1: %d", [num1 retainCount]);
NSLog(@"Address of num1: %p", num1);
NSLog(@"Address of num2: %p", num2);

num1 = [NSNumber numberWithInt:1000];
NSLog(@"num1 retain count after setting to 1000: %d", [num1 retainCount]);
num2 = [NSNumber numberWithInt:1000];
NSLog(@"num1 retain count after setting num2 to 1000: %d", [num1 retainCount]);
NSLog(@"Address of num1: %p", num1);
NSLog(@"Address of num2: %p", num2);
This code produces the following output:
num1 retain count after setting to 1: 2
num1 retain count after setting num2 to 1: 3
Address of num1: 0x3068c0
Address of num2: 0x3068c0

num1 retain count after setting to 1000: 1
num1 retain count after setting num2 to 1000: 1
Address of num1: 0x3073d0
Address of num2: 0x3073e0
In the first section num1's retain count is changed when num2 is created with the same value (1). In fact, when their addresses are printed they are the same, so NSNumber is just returning the same object.

In the second section, num1's retain count is not changed, as NSNumber returns different objects for num1 and num2. This shows that the optimizations are only done for more common numbers.

mduser63
Jun 1, 2006, 01:01 AM
Thanks for the reply HexMonkey. That makes sense. I'm quite new at this, but I assume that since they're not mutable, it doesn't matter of num1 and num2 are really the same object because the reference counting mechanism ensures that even if num1 is no longer needed and is released the underlying object sticks around until num2 is also no longer needed. Is that correct?

Also, I'm thinking that the reason an NSNumber object with an int value of 1 starts out with a retain count of 2 is because code that's part of the system (nothing I wrote) is already using an NSNumber object with a value of 1. Does that sound right?

HexMonkey
Jun 1, 2006, 01:12 AM
Yeah, you've got the idea. I suspect what's happening internally is that NSNumber is creating all the common number objects the first time it's called, so that it can easily just return pointers to these objects on demand. This would explain why the numbers start off with retain counts of 2.

HiRez
Jun 1, 2006, 01:13 AM
Something is still fishy here. Try this: change num1 = [NSNumber numberWithInt:1]; to num1 = [NSNumber numberWithFloat:1.0]; and the retain count becomes 1 instead of 2. And when I tried num1 = [NSNumber numberWithBool:YES];, the retain count was 2147483647 (which is the maximum value of a 32-bit signed integer). WTF? Quite confusing.

HexMonkey
Jun 1, 2006, 01:27 AM
Something is still fishy here. Try this: change num1 = [NSNumber numberWithInt:1]; to num1 = [NSNumber numberWithFloat:1.0]; and the retain count becomes 1 instead of 2.

That makes sense, floats aren't used nearly as much as ints so it probably doesn't cache them. And of course, a float with value 1 and an int with value 1 are quite different internally.

And when I tried num1 = [NSNumber numberWithBool:YES];, the retain count was 2147483647 (which is the maximum value of a 32-bit signed integer). WTF? Quite confusing.

That's strange, it looks like it's doing something non-standard. It's retain count doesn't even change if you send it retain or release messages. Perhaps it's a safeguard to make sure it's never unallocated.

HiRez
Jun 1, 2006, 01:33 AM
OK, so after some more testing, yes it does appear the system is retaining a single immutable object for numbers re-used.NSNumber *num1 = [NSNumber numberWithInt:1];
NSLog(@"num1 retain count = %d", [num1 retainCount]);
NSNumber *num2 = [NSNumber numberWithInt:1];
NSLog(@"num2 retain count = %d", [num2 retainCount]);
NSNumber *num3 = [NSNumber numberWithInt:1];
NSLog(@"num3 retain count = %d", [num3 retainCount]);yields:

2006-05-31 23:29:35.018 RetainCountTest[1309] num1 retain count = 2
2006-05-31 23:29:35.019 RetainCountTest[1309] num2 retain count = 3
2006-05-31 23:29:35.019 RetainCountTest[1309] num3 retain count = 4

But I still can't explain the Boolean test. It seems like maybe it's not even initialized, maybe when you call for an NSNumber representing a Boolean, it intercepts that and knows it doesn't need to store a value.

HiRez
Jun 1, 2006, 01:44 AM
It's retain count doesn't even change if you send it retain or release messages. Perhaps it's a safeguard to make sure it's never unallocated.Ah yes, maybe...although if calls to release: don't do anything anyway I'm not sure why it's needed. Maybe the system has privileges to send release messages where normal user apps don't. Anyway, I'm glad you asked the question, mduser, because I'm sure someday I would have hit upon this issue and it would have confused me.

caveman_uk
Jun 1, 2006, 03:12 AM
What's more odd is that these objects have never actually been retained. They're not created using copy or alloc so I would have thought the retain count would be 0.

Wierd.

HexMonkey
Jun 1, 2006, 03:34 AM
What's more odd is that these objects have never actually been retained. They're not created using copy or alloc so I would have thought the retain count would be 0.

I think it's correct, NSNumber would retain them, autorelease them and then return them. They would then be released at a later time when the autorelease pool is emptied.

Remember that numberWithInt etc are just convenience methods that call their respective init methods, eg:

+(NSNumber*)numberWithInt:(int)value
{
return [[[self class] initWithInt:value] autorelease];
}

JonDann
Nov 27, 2007, 04:58 PM
Thanks guys, I came across this today when looking to see what the retain count of an NSNumber boolean was! I spent all night trying to work it out, writing examples to try and see what was going on.

You guys rock!

Krevnik
Nov 27, 2007, 06:50 PM
What's more odd is that these objects have never actually been retained. They're not created using copy or alloc so I would have thought the retain count would be 0.

Wierd.

Autoreleased objects will keep a retain count of 1 until the pool is released. That is because interally, all objects MUST be created via alloc or copy, and the pool is used to delay the release message. It doesn't actually change the retain count until the delayed release occurs.

Gelfin
Nov 27, 2007, 07:42 PM
There is some cleverness going on inside the Foundation here. NSNumber is the interface you see, but there are a handful of private classes doing things behind the scenes. From the little bit of examination I've been able to do, it looks as if you will get "shared" versions of integer NSNumbers for values in the range [0-12]. Anything larger than 12 gets you a unique instance even if the values are equal. Why twelve? No clue. I don't even know if that's a hard number or circumstantial.

When you call numberWithInt what you get is actually an NSCFNumber, which I gather would be the implementation for the "toll-free bridging" between NSNumber and CFNumber you'll read about in the documentation.

Calling numberWithBool returns you a special case, an NSCFBoolean. It appears the Foundation keeps a static YES and NO object in its own memory space, and returns one or the other of those as appropriate, so that there is only ever one instance of YES and NO as NSNumbers in use. The weird results you get when trying to peer into a boolean NSNumber would be a result of this special case.

I am curious now whether setting the retain count of any object to INT_MAX makes it immortal (which would be a bad design idea, so I expect not), whether there is an "immortal" flag on NSNumber that causes retainCount to always return INT_MAX, or if NSCFBoolean just overrides retainCount. Unfortunately I don't have time to find out right now. :-/

Catfish_Man
Nov 27, 2007, 11:08 PM
There is some cleverness going on inside the Foundation here. NSNumber is the interface you see, but there are a handful of private classes doing things behind the scenes. From the little bit of examination I've been able to do, it looks as if you will get "shared" versions of integer NSNumbers for values in the range [0-12]. Anything larger than 12 gets you a unique instance even if the values are equal. Why twelve? No clue. I don't even know if that's a hard number or circumstantial.

When you call numberWithInt what you get is actually an NSCFNumber, which I gather would be the implementation for the "toll-free bridging" between NSNumber and CFNumber you'll read about in the documentation.

Calling numberWithBool returns you a special case, an NSCFBoolean. It appears the Foundation keeps a static YES and NO object in its own memory space, and returns one or the other of those as appropriate, so that there is only ever one instance of YES and NO as NSNumbers in use. The weird results you get when trying to peer into a boolean NSNumber would be a result of this special case.

I am curious now whether setting the retain count of any object to INT_MAX makes it immortal (which would be a bad design idea, so I expect not), whether there is an "immortal" flag on NSNumber that causes retainCount to always return INT_MAX, or if NSCFBoolean just overrides retainCount. Unfortunately I don't have time to find out right now. :-/

The implementation of retain and release is Really OddŽ and only odder in Leopard (sadly the CF source for leopard hasn't been posted yet, so I'm going on hearsay and limited testing). Fun example: write a benchmark that measures how long -retain takes in a loop. I suspect you'll find that it's only linear up to a certain retain count :)

HiRez
Nov 28, 2007, 11:10 AM
Haha, funny to see this old thread pop up again. Anyway, if they're doing some internal optimization cleverness, doesn't it make sense for them to also change the external representation of the retain counts to match the expected values? It's not too developer-friendly when you're trying to track down a bug and get all this weird, undocumented behavior.

Nutter
Nov 28, 2007, 11:13 AM
A retain count of UINT_MAX denotes an object that cannot be released. Evidently, a boolean YES NSNumber is a sort of singleton (http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_10.html).

Catfish_Man
Nov 28, 2007, 11:40 PM
fwiw I hacked up a quick retain benchmark, but the data was way too noisy to see what I was expecting. I'm extremely unclear on why, although I suspect my timings weren't accurate enough (I was using NSDate rather than mach_absolute_time).

<edit>
The thing I was thinking of only applies to CFRetain anyway, and behaves slightly differently than I had thought. Please disregard my cryptic and ultimately incorrect meanderings :)
</edit>