NSDate, 64-bit, and You

November 22, 2013

We recently discovered a problem while working on a client application: a certain feature, which had always worked before and was still working on the iPhone 5, was suddenly not working on the iPhone 5s when built for the arm64 architecture. There did not seem to be anything in the code that would be affected by 64-bit differences or that could be explained by the differences between the 5 and the 5s. This was a head scratcher.

After digging around, we discovered the issue was connected to [NSDate earlierDate:] and [NSDate laterDate:]. These two methods take an NSDate as an argument, and they return an object — the receiver of the message or the argument — whichever is earlier or later in time respectively. If the two dates are identical, the receiver of the message is returned.

Our NSDate argument was created elsewhere in the code by adding date components to an entirely different NSDate instance. On the occasion that the dates were equal, we saw the expected behavior on all devices except the 5s/arm64. What was going on here?

After even more digging, we discovered that on 64-bit platforms the two NSDate instances were actually the same object (same pointer value). The code was structured like this:

if ([date1 earlierDate:date2] == date2)
    // Do this when date2 is the earlier date but not when they are equal
    // Executed (incorrectly) if date1 and date2 are the same instance

This conditional worked as expected on all other devices because the two dates were different instances: if the dates are equal, the receiver of the message is returned (in this case date1), so naturally the result would not be date2 and the conditional would fail…

… except on a 64-bit platform. We had inadvertently stumbled into a trap that was set with the introduction of Tagged Pointers. (Here is a quick introduction to tagged pointers.)

While encounters with tagged pointers are somewhat common in connection with NSNumber, and only rarely do they cause problems, we had never knowingly encountered a tagged pointer with an NSDate. Since NSDate values can be expressed by a 64-bit double time interval (and fewer bits for an integer representation of an even number of seconds), it makes sense that they would be a candidate for tagged pointers on a 64-bit architecture.

The problem with tagged pointers in this case are the two NSDate methods that return an object, earlierDate and laterDate, where you rely on the two objects to be different instances for a valid result. When two tagged pointers contain the same date value, they have the same pointer value and to these two methods they are the same instance.

The fix was easy. The code structure was changed to this:

if ([date1 earlierDate:date2] != date1)
    // Do this when date2 is the earlier date but not when they are equal
    // Not executed (correctly) if date1 and date2 are the same instance

The moral of the story? Well, I’m not sure there’s a moral, really, but if you always compare your [NSDate earlierDate:] and [NSDate laterDate:] results with the receiver of the message (and not the argument), then the conditional logic will always be predictable.

Tagged with:

Comments are closed.