Check out our Bootcamp Schedule View Schedule

NS-structs

Mark Dalrymple

I got a question from a friend of mine the other day:

“I changed an instance variable from a float to a NSPoint, and I updated -description, but it’s not working”

Basically, the object looked like this after the change

@interface XYZEvent : NSObject {
    NSInteger _someStuff;   // 0, 1024, 2048 ...
    <strong>NSPoint   _position;    // Relative x axis position in measure</strong>
    id        _someObject;  // Pointer to event object
}

And -description looked like this:

@implementation XYZEvent

- (NSString *) description {
    NSString *desc = [NSString stringWithFormat:
                               @"<%@:%p, stuff: %d, <strong>position: %@</strong>, object: %@>",
                               _someStuff, _someStuff, <strong>_position</strong>, _someObject];
    return desc;
} // description

@end // XYZEvent

See the problem? NSPoint is actually a struct (as is its identical cousin CGPoint). It’s not an object. You shouldn’t pass a struct where Cocoa is expecting an object because weird things can happen (usually crashes, or inexplicable numerical values). The solution is to break the point apart into _position.x and _position.y, or use NSStringFromPoint (on the desktop) or NSStringFromCGPoint (on iOS) to make a string description of the object.

That got me thinking about Cocoa, objects, and structs. It’s not just the points that are structs. NSRect is a struct. NSRange. NSSize.

Why do the Cocoa platforms have all these structs? Wouldn’t it be nicer and more object-oriented if they were full-fledged objects? That’s a pretty common question asked by newcomers to the platform. It would certainly make things a lot more consistent API-wise, instead of remembering that this NS-thing over here is a struct and that NS-thing over there is an object. It’d be cool to be able to add categories onto NSRect to simplify some app-specific coordinate transforms.

I profess no internal knowledge of the decisions involved in the early implementation of Cocoa (in fact, I couldn’t even purchase a NeXT machine at the time), but I can imagine that performance considerations of the original platform had a big impact.

Recall some of the specs of the original NeXT cube:

  • 25 Mhz 68040 (32-bit) processor

  • 16 MB RAM (expandable up to 64 MB)

That’s sixteen megabytes, not gigabytes. I have a sixteen megabyte mp3 of Rhapsody in Blue that would occupy all of the RAM of a stock NeXT machine. That’s 1/32nd of the RAM inside an iPhone4S. So, not a lot of memory in those days.

It’s kind of hard to compare performance of the processors, but by looking at FLOPs (floating point operations per second), the 68040 can do 3.5, and the iPad 2 gets 160. So by crude guesstimation, floating-point power is a factor of 45 times faster today.

The original NeXT cube was, by today’s standards, extremely resource constrained. But it did an incredible amount of work. It ran a full-blown Unix operating system. It had a monstrously huge display for its time. It had Display Postscript for drawing on that screen – essentially the computational guts of a laser printer. It also had the earliest form of the toolkit we now call Cocoa. That’s a lot of work that the machine was doing.

Nowadays, we create and destroy objects with reckless abandon. They appear to us to be as lightweight as structures. I think (mostly) nothing of creating an NSArray to hold a single NSNumber wrapping a float so that I can pass it to a method. Back in the mists of time, though, objects were proportionally much heavier weight.

Every Objective-C object contains a pointer, called isa, that points to its class. There’s four bytes of overhead. A NSPoint holds two floats, for a total of 8 bytes, so you’ve got a 50% overhead just for the isa pointer – you’d need 12 bytes now to store the point.

Objective-C objects are dynamically allocated, ultimately boiling down to a call like malloc or something similar. There is computational overhead with each allocated block, with malloc juggling free lists and data structures, plus it needs some place to store the size of the block. Frequently there’s padding on one or both ends of the allocated memory. That can be overhead consuming the same amount of memory (or more!) than the data being stored. If you browse through Cocoa, you’ll see that most of the objects are pretty heavy-weight, making the overhead of the isa pointer and malloc bookkeeping proportionally much much smaller.

Dynamic memory allocation can also be very slow. You call into a library. There’s locks involved. You might need to dip into the kernel to get a couple more pages of memory. Maybe memory has to be paged back in. You make a similar round-trip when destroying the object. Whereas a simple struct on the stack is “created” by adjusting the value of the stack pointer. That is a very fast operation.

The NS-structs you encounter tend to be ephemeral. You use an NSSize or an NSRect for a couple of calculations, and then you’re done. Same with NSRanges – get the coordinates of a substring match, use it to index something else, and then you’re done. If ranges were objects, this very lightweight operation would suddenly turn very heavy-weight, involving trips through lock-land. On the original NeXT cube, this could have easily dogged performance.

“It couldn’t have been that bad, could it?” I’m afraid so. (old-geezer-story alert) My first job out of college (1990) was at a Unix shop writing GUI software (our product was like the Finder, but ran on every Unix flavor you could think of). As the company n00b, I was tasked with fixing a bug in our product that ran on an obscure windowing system whose relevance thankfully has completely evaporated. While revamping the event handling (mouse/key presses, window movement and updates, timers, etc), I decided to malloc an event, pass it around, then free it. My first run was an embarrassment on my Sun 3/50 (16mhz 68020). Performance dropped through the floor and hit every other cliché on the way down. Being the lo mein on the totem pole, I of course had the slowest machine in the company, but it was similar to machines our paying customers were using. Bad performance on my machine would result in bad performance for my users. I changed the malloc to a structure (actually a kind of complicated union), but that made performance very good again.

Nowadays, machines are so fast (even the portable ones) that a couple dozen extra memory allocations here and here is not a big deal, so having things like points and sizes being first-class objects wouldn’t be a Big Deal. But it’s very difficult to change a legacy code base, especially one as big and as old as Cocoa and its ecosystem, so we’re stuck with the NS-structures. And like a computational Stockholm Syndrome, I kind of like knowing that rects, sizes, and whatnot are stack-based structures and extremely lightweight. I have no qualms about using as many of them as I need, while I still feel a little twinge of guilt when I have to allocate something.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project