10

I noticed that using an NSDateFormatter can be quite costly. I figured out that allocating and initializing the object already consumes a lot of time.
Further, it seems that using an NSDateFormatter in multiple threads increases the costs. Can there be a blocking where the threads have to wait for each other?

I created a small test application to illustrate the problem. Please check it out.

What is the reason for such costs and how can I improve the usage?


17.12. - To update my observation: I do not understand why the threads run longer when processed parallel compared to when the run in serial order. The time difference only occurs when NSDateFormatter is used.

JJD
  • 50,076
  • 60
  • 203
  • 339
  • I'd give you three points for that benchmark app, if I could. Well, 2 because the iVars are all prefixed with `m_`, but... still... it is a great starting point to dive deep w/Instruments, sampling, threading, etc... – bbum Dec 14 '10 at 18:13

6 Answers6

17

Note: Your example program is very much a micro-benchmark and very effectively maximally amplifies that cost of a date formatter. You are comparing doing absolutely nothing with doing something. Thus, whatever that something is, it will appear to be something times slower than nothing.

Such tests are extremely valuable and extremely misleading. Micro-benchmarks are generally only useful when you have a real world case of Teh Slow. If you were to make this benchmark 10x faster (which, in fact, you probably could with what I suggest below) but the real world case is only 1% of overall CPU time used in your app, the end result is not going to be a dramatic speed improvement -- it will be barely noticeable.

What is the reason for such costs?

NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyyMMdd HH:mm:ss.SSS"];

Most likely, the cost is associated with both having to parse/validate the date format string and having to do any kind of locale specific goop that NSDateFormatter does. Cocoa has extremely thorough support for localization, but that support comes at a cost of complexity.

Seeing as how you wrote a rather awesome example program, you could fire up your app in Instruments and try the various CPU sampling instruments to both understand what is consuming CPU cycles and how Instruments works (if you find anything interesting, please update your question!).

Can there be a blocking where the threads have to wait for each other?

I'm surprised it doesn't simply crash when you use a single formatter from multiple threads. NSDateFormatter doesn't specifically mention that it is thread safe. Thus, you must assume that it is not thread safe.

How can I improve the usage?

Don't create so many date formatters!

Either keep one around for a batch of operations and then get rid of it or, if you use 'em all the time, create one at the beginning of your app's run and keep around until the format changes.

For threading, keep one per thread around, if you really really have to (I'd bet that is excessive -- that the architecture of your app is such that creating one per batch of operations will be more sensible).

bbum
  • 162,346
  • 23
  • 271
  • 359
  • Thank you for your reply. I tried to keep the test app simple. That is why it is "doing absolutely nothing". Of course there is a real world context. It is a file parser retrieving the timestamp and more. Nevertheless the simple allocation of an NSDateFormatter makes the routine very slow. - I am aware that dealing with the formatter as a member variable to reuse it within one thread speeds up things. But still I cannot explain why the formatter is that costy when multiple threads use it. The time differences are more obvious when I do not use the member. – JJD Dec 15 '10 at 11:01
  • The behavior in the threaded case is undefined in that `NSDateFormatter` does not explicitly claim thread safety. Thus, no surprise that it is slow in the threaded case (big surprise that it works at all). Did you try using the CPU sampler in Instruments to sample your benchmark as it runs? That'll tell you were the cycles go. – bbum Dec 15 '10 at 16:29
  • I am not sure if I understand. If CPU sampler can show which CPU processed which thread I cannot find it in Instruments. – JJD Dec 17 '10 at 11:34
  • There are a couple of different configurations that are useful; Time Profiler provides time-based sampling of all (or one, IIRC) processes, CPU Sampler is a slight different time-based sampler, and -- finally -- Multicore can provide thread state based CPU sampling. – bbum Dec 17 '10 at 18:11
  • Hrm -- maybe I should take your example, fire up Instruments, and write a weblog post showing all the different bits of information that can be gleaned from an otherwise very simple benchmark. – bbum Dec 17 '10 at 18:12
5

I like to use a GCD sequential queue for ensuring thread safety, it's convenient, effective, and efficient. Something like:

dispatch_queue_t formatterQueue = dispatch_queue_create("formatter queue", NULL);
NSDateFormatter *dateFormatter;
// ...
- (NSDate *)dateFromString:(NSString *)string
{
    __block NSDate *date = nil;
    dispatch_sync(formatterQueue, ^{
        date = [dateFormatter dateFromString:string];
    });
    return date;
}
shawkinaw
  • 3,190
  • 2
  • 27
  • 30
3

Use GDC dispath_once and you're good. This will ensure syncing between multiple threads and ensure that the date formatter is only created once.

+ (NSDateFormatter *)ISO8601DateFormatter {
    static NSDateFormatter *formatter;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        formatter = [[NSDateFormatter alloc] init];
        formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";
    });
    return formatter;
}
leviathan
  • 11,080
  • 5
  • 42
  • 40
3

Using -initWithDateFormat:allowNaturalLanguage: instead of -init followed by -setDateFormat: should be much faster (probably ~2x).

In general though, what bbum said: cache your date formatters for hot code.

(Edit: this is no longer true in iOS 6/OSX 10.8, they should all be equally fast now)

Catfish_Man
  • 41,261
  • 11
  • 67
  • 84
  • Thank you. The function call `setDateFormat` does not produce the costs. Actually, you can leave it out from the test app. I put it in just in case the compile tries to optimize and removes the dateFormatter cause it is not used at all. – JJD Dec 15 '10 at 11:06
  • It is faster, indeed. How did you know? Nevertheless, I am not sure whether it is clever to use this initializer since it is unclear if it is deprecated or not. See here: http://stackoverflow.com/questions/3182314/nsdateformatters-init-method-is-deprecated/ – JJD Dec 17 '10 at 11:32
  • I fired up Shark (or Instruments, I don't remember now) and looked at where the costs were. Both the -init and -set calls end up calling into the ICU formatter setup code, which is the expensive bit. (edited) Oh, hm. I see the issue. That creates a 10.0-style formatter. This is very odd... the expected way to use a formatter shouldn't do duplicate work. I'll investigate. – Catfish_Man Dec 19 '10 at 19:46
  • Investigated. Apparently there's no good way to do this. How unfortunate :( Caching should still make it irrelevant though. – Catfish_Man Dec 28 '10 at 22:03
2

Since the creation/init of NSDateFormatter AND the format and locale changes costs a lot. I've created a "factory" class to handle the reuse of my NSDateFormatters.

I have a NSCache instance where I store up to 15 NSDateFormatter instances, based on format and locale info, in the moment when I created then. So, sometime later when I need them again, I ask my class by some NSDateFormatter of format "dd/MM/yyyy" using locale "pt-BR" and my class give the correspondent already loaded NSDateFormatter instance.

You should agree that it's an edge case to have more than 15 date formats per runtime in most standard applications, so I assume this is a great limit to cache them. If you use only 1 or 2 different date formats, you'll have only this number of loaded NSDateFormatter instances. Sounds good for my needs.

If you would like to try it, I made it public on GitHub.

JJD
  • 50,076
  • 60
  • 203
  • 339
Douglas Fischer
  • 535
  • 4
  • 7
  • Is there a special reason you extend `NSObject` in your [implementation](https://github.com/DougFischer/DFD – JJD Apr 27 '13 at 18:01
  • I need to init de NSCache instance, I don't know if is it possible to do using categories, but I think, it would be possible to extend from NSDateFormatter. I missed it. – Douglas Fischer Apr 27 '13 at 18:54
  • 1
    It should be possible. Search for "associative references" in Objective-C. – JJD Apr 28 '13 at 21:02
  • 1
    @DouglasFischer, if you ever want to re-do you Date Formatter cache setup, this article describes a great use of a singleton as a NSDateFormatter cache - https://krakendev.io/blog/antipatterns-singletons. – Natalia May 15 '18 at 20:29
0

I think the best implementation is like below:

NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSDateFormatter *dateFormatter = threadDictionary[@”mydateformatter”];
if(!dateFormatter){
    @synchronized(self){
        if(!dateFormatter){
            dateFormatter = [[NSDateFormatter alloc] init];
           [dateFormatter setDateFormat:@”yyyy-MM-dd HH:mm:ss”];
           [dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@”Asia/Shanghai”]];
          threadDictionary[@”mydateformatter”] = dateFormatter;
         }
    }
}