Does Objective-C use short-circuit evaluation for messages to nil objects?
Asked Answered
I

2

5

Following the usual short-circuit evaluation question, does short-circuit evaluation work for parameters built and sent against nil objects? Example:

NSMutableArray *nil_array = nil;
....
[nil_array addObject:[NSString stringWithFormat:@"Something big %@",
     function_that_takes_a_lot_of_time_to_compute()]];

Is that slow function going to be called or will the whole addObject call be optimized out without processing the parameters?

Imperfection answered 13/2, 2011 at 16:37 Comment(2)
Too afraid of the debugger to find out yourself? :-)Cloth
I was going to answer that myself as I found it interesting trivia, but BoltClock beat me to it, so he gets the points (yet I have to wait 3 minutes to accept his answer, ah, spam filters).Imperfection
S
9

A message is always dispatched to an object pointer, regardless of whether it points to an object or points to nil. Additionally, messages are sent in the runtime and therefore the compiler cannot just assume nil_array really is nil and optimize it away. What if the initialization did something else, and nil_array turns out to be an instance?

That means all the expressions you pass as arguments to your methods will be evaluated in order to be passed, so no short-circuiting of any sort happens. Your slow function will be executed, and if it takes a long time it'll affect the performance of your program.

EDIT: I just whipped up a little test case for the heck of it (empty Objective-C command line program). If you run this and observe the debugger console, you'll notice that output from all three calls to function_that_takes_a_lot_of_time_to_compute() appears (in 5-second intervals), while output from only t1's and t3's test: methods appears — naturally, since these are not nil.

main.m

#import "Test.h"

int function_that_takes_a_lot_of_time_to_compute(void)
{
    static int i = 1;

    sleep(5);
    NSLog(@"%d", i);

    return i++;
}

int main(int argc, const char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    Test *t1 = [[Test alloc] init], *t2 = nil, *t3 = [[Test alloc] init];

    [t1 test:function_that_takes_a_lot_of_time_to_compute()];
    [t2 test:function_that_takes_a_lot_of_time_to_compute()]; // nil
    [t3 test:function_that_takes_a_lot_of_time_to_compute()];

    [t1 release];
    [t3 release];

    [pool drain];
    return 0;
}

Test.h

@interface Test : NSObject {}

- (void)test:(int)arg;

@end

Test.m

@implementation Test

- (void)test:(int)arg
{
    NSLog(@"Testing arg: %d", arg);
}

@end

Output

1
Testing arg: 1
2
3
Testing arg: 3
Sverige answered 13/2, 2011 at 16:43 Comment(0)
C
6

The accepted answer is a good one, but I wanted to add:

function_that_takes_a_lot_of_time_to_compute() or +[NSString stringWithFormat:] can have side effects, so even if we knew with 100% certainty that nil_array was nil (and we can sometimes know this through static analysis), the program would still have to execute function_that_takes_a_lot_of_time_to_compute() and +[NSString stringWithFormat:] to ensure it was behaving as expected.

If a function f() has no side effects, it is considered "pure." This means that it can take input arguments and can return a value, but it never calls any non-pure functions and never modifies any part of the program or global memory (the memory involved in passing arguments and return values doesn't count here.) The following function, for instance, is "pure":

int munge(float foo, char bar) {
    unsigned short quux = bar << 4;
    return foo + quux;
}

Examples of pure functions within the C standard library are memcmp() and strlen().

If and only if a function is known to be pure, the compiler could safely optimize away calls to it, since not calling it would have no effect on the rest of the program. However, GCC is very conservative about doing this, and generally (always?) does it only when a function is marked pure, via the __attribute__((__pure__)) decoration on the function declaration.

If a function is pure, and in addition never dereferences pointers and never accesses any memory outside its stack frame, it can instead be marked __attribute__((__const__)) in GCC, which allows even further static analysis and optimization.

Chorion answered 13/2, 2011 at 21:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.