20

Other than giving old school Objective-C programmers heart attacks, are there any other performance implications to doing this:

NSMutableArray *derp = @[].mutableCopy

Versus this:

NSMutableArray *derp = [[NSMutableArray alloc] init];
Luke The Obscure
  • 1,524
  • 2
  • 20
  • 35

4 Answers4

13

I am going to give a different sort of answer than the ones below.

Unless you've measured the performance of your app and found it lacking, and found it lacking right in that part of the code, you're optimizing for the wrong thing. (And if you have done that, you can easily measure which one is faster.)

You should be optimizing for clarity of code, speed with which you can write it, and likelihood that it's correct.

In that respect, I would favor the second code snippet over the first. It says pretty much exactly what you're trying to do. The first snippet is not only harder to read; it also commits the style error of using dot notation to invoke a method, not get the value of a property.

Plus causing heart attacks just isn't nice. :)

dpassage
  • 5,423
  • 3
  • 25
  • 53
9

Class clusters do not have an extra allocation and it has nothing to do with compiler magic. So yes, there is a significant difference between your examples (btw, -1000 internet points for dot-notation abuse.) Your first example has two allocations, the second has only one.

Since we don't have access to the actual source code for NSArray, we can look at GNUStep (an open source implementation) to see how they handle it. In NSArray.m (simplifying and omitting irrelevant stuff):

static GSPlaceholderArray   *defaultPlaceholderArray;
+ (void) initialize
{
    defaultPlaceholderArray = (GSPlaceholderArray*)
        NSAllocateObject(GSPlaceholderArrayClass, 0, NSDefaultMallocZone());
}

+ (id) alloc
{
    return defaultPlaceholderArray;
}

What's going on here is that NSArray defines a singleton, placeholder object that it always returns in alloc. When init is called on this singleton it instantiates the proper private subclass and returns that.

So, how can we tell if Apple's Foundation is doing the same thing? Pretty easy, we just run this test:

NSArray *a1 = [NSArray alloc];
NSArray *a2 = [NSArray alloc];
NSLog(@"%p, %p", a1, a2);

> "0x100102f30, 0x100102f30"

a1 and a2 do have the same memory location meaning Apple is also likely using the singleton approach. If we print out the class name it's __NSPlaceholderArray so that pretty much confirms it.

So yeah, stick with [NSMutableArray new] :)

UPDATE: Greg Parker points out that @[] is also a singleton so @[].mutableCopy results in only one allocation. So performance-wise the two examples are the same.

Ben Dolman
  • 3,165
  • 3
  • 25
  • 25
8

It's hard to tell exactly how many objects are created, especially in the case of the array literal.

The official docs of clang say that @[] expands to arrayWithObjects:count:, which I suspect is implemented as [[[self alloc] initWithObjects:objects count:count] autorelease].

So, there may be a second object allocated at that point, since NSArray is a class cluster, and the implementation of - [NSArray init] may look something like this:

- (id)init
{
    [self release];
    self = [[__NSArrayI alloc] init];
    return self;
}

To confirm my suspicion, I've written a little program that prints the class of various types of NSArray objects at different stages. Note that in order to be really sure, it would be necessary to catch all of the allocation and initialization methods of NSArray. Until we do that, we can only speculate. But anyway, here's the code:

#import <Foundation/Foundation.h>


int main()
{
    NSArray *a = [NSArray alloc];
    NSLog(@"NSArray before init: %@", a.class);

    a = [a init];
    NSLog(@"NSArray after init: %@", a.class);

    NSArray *al = @[];
    NSLog(@"Array literal: %@", al.class);

    NSMutableArray *ma1 = [a mutableCopy];
    NSLog(@"NSMutableArray (copied): %@", ma1.class);

    NSMutableArray *ma2 = [NSMutableArray alloc];
    NSLog(@"NSMutableArray (manufactured) before init: %@", ma2.class);

    ma2 = [ma2 init];
    NSLog(@"NSMutableArray (manufactured) after init: %@", ma2.class);

    return 0;
}

And here's what it prints (NSLog() clutter removed for clarity):

h2co3-macbook:~ h2co3$ ./quirk
NSArray before init: __NSPlaceholderArray
NSArray after init: __NSArrayI
Array literal: __NSArrayI
NSMutableArray (copied): __NSArrayM
NSMutableArray (manufactured) before init: __NSPlaceholderArray
NSMutableArray (manufactured) after init: __NSArrayM

Edit: here's some more code with the hooking involved. The results are very interesting but this prints a lot of text, so you are encouraged to compile and run it. What turns out is, basically, that the initializers do not go through [NSArray init] directly:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>

void hook(Class cls, SEL sel, IMP newimp, IMP *old)
{
    Method m = class_getInstanceMethod(cls, sel);
    *old = method_setImplementation(m, newimp);
}

#define CLS(c) objc_getClass(#c)
#define META(c) objc_getMetaClass(#c)

IMP old_$_NSArray_$_alloc;
IMP old_$_NSMutableArray_$_alloc;
IMP old_$_NSPlaceholderArray_$_alloc;
IMP old_$_NSArrayI_$_alloc;
IMP old_$_NSArrayM_$_alloc;

IMP old_$_NSArray_$_init;
IMP old_$_NSMutableArray_$_init;
IMP old_$_NSPlaceholderArray_$_init;
IMP old_$_NSArrayI_$_init;
IMP old_$_NSArrayM_$_init;

id new_$_NSArray_$_alloc(id self, SEL _cmd)
{
    printf("+ [NSArray<%p> alloc]\n", self);
    return old_$_NSArray_$_alloc(self, _cmd);
}

id new_$_NSMutableArray_$_alloc(id self, SEL _cmd)
{
    printf("+ [NSMutableArray<%p> alloc]\n", self);
    return old_$_NSMutableArray_$_alloc(self, _cmd);
}

id new_$_NSPlaceholderArray_$_alloc(id self, SEL _cmd)
{
    printf("+ [NSPlaceholderArray<%p> alloc]\n", self);
    return old_$_NSPlaceholderArray_$_alloc(self, _cmd);
}

id new_$_NSArrayI_$_alloc(id self, SEL _cmd)
{
    printf("+ [NSArrayI<%p> alloc]\n", self);
    return old_$_NSArrayI_$_alloc(self, _cmd);
}

id new_$_NSArrayM_$_alloc(id self, SEL _cmd)
{
    printf("+ [NSArrayM<%p> alloc]\n", self);
    return old_$_NSArrayM_$_alloc(self, _cmd);
}

id new_$_NSArray_$_init(id self, SEL _cmd)
{
    printf("- [NSArray<%p> init]\n", self);
    return old_$_NSArray_$_init(self, _cmd);
}

id new_$_NSMutableArray_$_init(id self, SEL _cmd)
{
    printf("- [NSMutableArray<%p> init]\n", self);
    return old_$_NSMutableArray_$_init(self, _cmd);
}

id new_$_NSPlaceholderArray_$_init(id self, SEL _cmd)
{
    printf("- [NSPlaceholderArray<%p> init]\n", self);
    return old_$_NSPlaceholderArray_$_init(self, _cmd);
}

id new_$_NSArrayI_$_init(id self, SEL _cmd)
{
    printf("- [NSArrayI<%p> init]\n", self);
    return old_$_NSArrayI_$_init(self, _cmd);
}

id new_$_NSArrayM_$_init(id self, SEL _cmd)
{
    printf("- [NSArrayM<%p> init]\n", self);
    return old_$_NSArrayM_$_init(self, _cmd);
}

int main()
{
    hook(META(NSArray), @selector(alloc), (IMP)new_$_NSArray_$_alloc, &old_$_NSArray_$_alloc);
    hook(META(NSMutableArray), @selector(alloc), (IMP)new_$_NSMutableArray_$_alloc, &old_$_NSMutableArray_$_alloc);
    hook(META(__NSPlaceholderArray), @selector(alloc), (IMP)new_$_NSPlaceholderArray_$_alloc, &old_$_NSPlaceholderArray_$_alloc);
    hook(META(__NSArrayI), @selector(alloc), (IMP)new_$_NSArrayI_$_alloc, &old_$_NSArrayI_$_alloc);
    hook(META(__NSArrayM), @selector(alloc), (IMP)new_$_NSArrayM_$_alloc, &old_$_NSArrayM_$_alloc);

    hook(CLS(NSArray), @selector(init), (IMP)new_$_NSArray_$_init, &old_$_NSArray_$_init);
    hook(CLS(NSMutableArray), @selector(init), (IMP)new_$_NSMutableArray_$_init, &old_$_NSMutableArray_$_init);
    hook(CLS(NSPlaceholderArray), @selector(init), (IMP)new_$_NSPlaceholderArray_$_init, &old_$_NSPlaceholderArray_$_init);
    hook(CLS(NSArrayI), @selector(init), (IMP)new_$_NSArrayI_$_init, &old_$_NSArrayI_$_init);
    hook(CLS(NSArrayM), @selector(init), (IMP)new_$_NSArrayM_$_init, &old_$_NSArrayM_$_init);


    NSArray *a = [NSArray alloc];
    NSLog(@"NSArray before init: %@<%p>", a.class, a);

    a = [a init];
    NSLog(@"NSArray after init: %@<%p>", a.class, a);

    NSArray *al = @[];
    NSLog(@"Array literal: %@<%p>", al.class, al);

    NSMutableArray *ma1 = [a mutableCopy];
    NSLog(@"NSMutableArray (copied): %@<%p>", ma1.class, ma1);

    NSMutableArray *ma2 = [NSMutableArray alloc];
    NSLog(@"NSMutableArray (manufactured) before init: %@<%p>", ma2.class, ma2);

    ma2 = [ma2 init];
    NSLog(@"NSMutableArray (manufactured) after init: %@<%p>", ma2.class, ma2);

    return 0;
}
Community
  • 1
  • 1
4

Yeah, one is allocating 2 objects, the other is not. I do [NSMutableArray new]

UPDATE: Greg Parker is right, both allocate only one object.

atomkirk
  • 3,701
  • 27
  • 30
  • Wouldn't the first array be immediately released? – Luke The Obscure Aug 26 '13 at 21:58
  • 3
    @LukeTheObscure It would, but it would have to be allocated nevertheless. Boom, your tight loop now takes twice the time to run. –  Aug 26 '13 at 21:59
  • 2
    Well, actually... The second version allocates two objects too. `NSMutableArray` is a class cluster. Its `init` method does something like `[self release]; self = [[__NSArrayM alloc] init]; return self;`. –  Aug 26 '13 at 22:01
  • @atomk at least three, but in any case definitely _more_ than calling `alloc] init]` on `NSMutableArray` directly, making your answer substantially correct. – Tommy Aug 26 '13 at 22:32
  • @Tommy No, it is substantially **incorrect.** Stating that "two objects are allocated" when there are at least 3 is just wrong. –  Aug 26 '13 at 22:56
  • Is @[] guaranteed to always return a new, different instance, or might the compiler decide to create a static instance and always use that as an optimization? – Eiko Aug 26 '13 at 23:22
  • @H2CO3 Saying "two objects are allocated" in the context of "what are the performance implications?" to communicate that "more objects are allocated" is substantially correct because it's the _more_ that's important in context, not the specific number, and the use of 'two' successfully implies more. 'Substantially' means "to a great or significant extent" not "completely" and context is key in natural languages. – Tommy Aug 26 '13 at 23:55
  • I highly doubt theres a compiler optimization that would add an exception to our answer that the first way allocates more arrays than the second. – atomkirk Aug 27 '13 at 00:02
  • 1
    @Eiko: I don't believe there is any requirement for it to return different objects, but I also don't believe any such optimization exists in any compiler. I also wouldn't bet on it ever being added — it confers almost no benefits in the real world or in benchmarks. – Chuck Aug 27 '13 at 00:05
  • 4
    In current OS versions, both `[[NSMutableArray alloc] init]` and `@[].mutableCopy` allocate only one object. – Greg Parker Aug 27 '13 at 03:15
  • @GregParker can you post some proof? I'd be very surprised if you're right that the second only allocates one object. – atomkirk Aug 27 '13 at 03:44
  • It's pretty easy to check. Set breakpoints on calloc/malloc and try it. Greg is quite right. – Catfish_Man Aug 27 '13 at 05:52
  • @Chuck: I just did some tests. If you create multiple (immutable) empty arrays, you always get the same instance, no matter if you use `@[]`, `@[].copy` or `[NSArray array]`. – Eiko Aug 27 '13 at 07:55
  • Looks like greg is a runtime engineer at Apple. Is he allowed to explain how this works? :) – atomkirk Aug 27 '13 at 12:09