Search code examples
iosobjective-cnsdate

Why NSDate's dateWithTimeIntervalSince1970 modifies its input?


I'm running the following code:

for (int i = 0; i < 100; ++i) {
  NSDate *date = [NSDate date];
  NSTimeInterval interval = date.timeIntervalSince1970;
  NSDate *newDate = [NSDate dateWithTimeIntervalSince1970:interval];

  if (![date isEqualToDate:newDate]) {
    NSLog(@"Not equal!");
  }
}

Surprisingly, in many iterations the dates are not equal to each other. How can that be?


Solution

  • Disassembling dateWithTimeIntervalSince1970: shows that it calls -initWithIntervalSinceReferenceDate::

    CoreFoundation`+[NSDate dateWithTimeIntervalSince1970:]:
        0x10fd39430 <+0>:  pushq  %rbp
        0x10fd39431 <+1>:  movq   %rsp, %rbp
        0x10fd39434 <+4>:  pushq  %rbx
        0x10fd39435 <+5>:  pushq  %rax
        0x10fd39436 <+6>:  movsd  %xmm0, -0x10(%rbp)
        0x10fd3943b <+11>: movq   0x2d8146(%rip), %rsi      ; "alloc"
        0x10fd39442 <+18>: movq   0x29edc7(%rip), %rbx      ; (void *)0x000000010f35e940: objc_msgSend
        0x10fd39449 <+25>: callq  *%rbx
        0x10fd3944b <+27>: movsd  -0x10(%rbp), %xmm0        ; xmm0 = mem[0],zero 
        0x10fd39450 <+32>: addsd  0x1f9d58(%rip), %xmm0     ; _CFLog_os_trace_type_map + 16
        0x10fd39458 <+40>: movq   0x2d9041(%rip), %rsi      ; "initWithTimeIntervalSinceReferenceDate:"
        0x10fd3945f <+47>: movq   %rax, %rdi
        0x10fd39462 <+50>: callq  *%rbx
        0x10fd39464 <+52>: movq   0x2d81b5(%rip), %rsi      ; "autorelease"
        0x10fd3946b <+59>: movq   %rax, %rdi
        0x10fd3946e <+62>: movq   %rbx, %rax
        0x10fd39471 <+65>: addq   $0x8, %rsp
        0x10fd39475 <+69>: popq   %rbx
        0x10fd39476 <+70>: popq   %rbp
        0x10fd39477 <+71>: jmpq   *%rax
        0x10fd39479 <+73>: nopl   (%rax)
    

    Additionally, the input to the initializer is secs - 978307200 (or, secs - NSTimeIntervalSince1970), which is the time difference between 1970 and the reference date. This computation, while subtracting an integer (double) from a double can change the fraction of the input value due to rounding errors. For example, here's a date that failed the test:

    date: Sun Mar 25 17:54:39 2018 (543682479.8504179716),
    newDate: Sun Mar 25 17:54:39 2018 (543682479.8504180908)
    

    Since log2(543682479.8504179716 + NSTimeIntervalSince1970) ~ 30.5 and log2(543682479.8504179716) ~ 29.01, the exponent of the double value needs to be adjusted and the mantissa needs to be normalized, which may affect the fractional value.

    The solution is to use the +dateWithTimeIntervalSinceReferenceDate: factory method instead, which directly initializes an NSDate without an additional computation.