如何最小化分配和初始化NSDateFormatter的成本?

10
我注意到使用NSDateFormatter可能会很昂贵。我发现分配和初始化对象已经消耗了很多时间。
此外,似乎在多个线程中使用NSDateFormatter会增加成本。是否存在一个阻塞,使得线程必须等待彼此?
我创建了一个小的测试应用程序来说明这个问题。请查看它。 这种成本的原因是什么,如何改进使用方法?
12月17日 - 更新我的观察:我不明白为什么并行处理时线程运行时间更长,而串行处理时则不然。只有在使用NSDateFormatter时才会出现时间差异。

如果可以的话,我会给你这个基准测试应用程序三个分数,好吧,2个分数,因为所有的iVars都以“m_”为前缀,但是...还是一个很好的起点,可以深入研究Instruments、采样、线程等等... - bbum
6个回答

17

注意:你的示例程序非常注重微基准测试,非常有效地放大日期格式化器的成本。你正在比较什么都不做做一些事情。因此,无论那个某些事情是什么,它看起来都会比什么都不做若干倍

这样的测试非常有价值,但也极易误导。微基准测试通常只有在您遇到真正的“缓慢”情况时才有用。如果您将此基准测试加速10倍(实际上,通过下面我提出的建议,您可能可以做到这一点),但真实世界的情况仅占应用程序总CPU时间的1%,则最终结果不会是显著的加速-它几乎不会被注意到。

为什么会产生这样的成本?

NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyyMMdd HH:mm:ss.SSS"];
很可能,成本与必须解析/验证日期格式字符串以及执行任何NSDateFormatter特定于区域设置的操作有关。Cocoa对本地化提供了非常全面的支持,但这种支持的代价是复杂性。
考虑到您编写了一个相当棒的示例程序,您可以在Instruments中启动应用程序并尝试各种CPU采样工具,以了解消耗CPU周期的内容以及Instruments的工作方式(如果发现任何有趣的内容,请更新您的问题!)。
能否阻止线程互相等待?
我很惊讶当你在多个线程上使用单个格式化程序时它竟然没有崩溃。 NSDateFormatter 没有明确说明它是线程安全的。因此,您必须假设它不是线程安全的。
如何改进使用方法?
不要创建太多日期格式化程序!
可以为一批操作保留一个格式化程序,然后将其丢弃;或者,如果您经常使用它们,则可以在应用程序运行开始时创建一个,并保留直到格式更改。
对于线程,每个线程保留一个,如果您真的需要(我敢打赌这是过度的 - 您的应用程序的体系结构使得为每个操作批次创建一个更明智)。

谢谢您的回复。我试图保持测试应用程序简单,这就是为什么它“什么都不做”。当然,有一个真实的世界背景。它是一个文件解析器,检索时间戳和更多信息。尽管如此,对NSDateFormatter进行简单的分配会使例程非常缓慢。-我知道将格式化程序处理为成员变量以在一个线程中重用它可以加快速度。但是,当多个线程使用它时,我仍然无法解释为什么格式化程序的成本如此之高。当我不使用成员变量时,时间差异更加明显。 - JJD
在多线程情况下,NSDateFormatter的行为是未定义的,因为它没有明确声明线程安全性。因此,在多线程情况下它运行缓慢并且可能无法正常工作。您是否尝试使用Instruments中的CPU采样器来对运行基准进行采样?这将告诉您循环花费的时间。 - bbum
我不确定我是否理解了。如果CPU采样器可以显示哪个CPU处理了哪个线程,那么我在Instruments中找不到它。 - JJD
有几种不同的配置是有用的;时间分析器提供所有(或一个,如果我没记错的话)进程的基于时间的采样,CPU采样器是略微不同的基于时间的采样器,最后,多核可以提供基于线程状态的CPU采样。 - bbum
嗯,也许我应该借鉴你的例子,启动Instruments,并撰写一篇博客文章,展示从一个非常简单的基准测试中可以获取到的所有不同信息。 - bbum

5
我喜欢使用GCD顺序队列来确保线程安全,它方便、有效且高效。类似这样:

dispatch_queue_t queue = dispatch_queue_create("com.example.myqueue", DISPATCH_QUEUE_SERIAL);

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;
}

3
使用GDC dispatch_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;
}

1
这个很棒。只创建一次NSDateFormatter就解决了我的问题。 - Jassi

3

使用-initWithDateFormat:allowNaturalLanguage:代替-init再跟上-setDateFormat:应该会更快(大约是原来的两倍)。

总的来说,像bbum所说的那样:为热门代码缓存您的日期格式化程序。

(编辑:在iOS 6 / OSX 10.8中不再正确,它们现在应该都一样快)


谢谢。函数调用 setDateFormat 不产生任何费用。实际上,您可以在测试应用程序中将其省略。我仅添加它以防编译器尝试进行优化并删除 dateFormatter,因为它根本没有被使用。 - JJD
确实更快。你怎么知道的?不过,我不确定使用这个初始化程序是否聪明,因为它不清楚它是否已经被弃用了。请参见此处:https://dev59.com/XHA75IYBdhLWcg3wqKx0 - JJD
我启动了Shark(或Instruments,我现在记不清了),并查看了成本所在的位置。-init和-set调用都最终调用ICU格式设置代码,这是昂贵的部分。(编辑)哦,嗯。我看到问题了。这会创建一个10.0样式的格式化程序。这非常奇怪...使用格式化程序的预期方式不应该重复工作。我会进行调查。 - Catfish_Man
调查后发现,似乎没有好的方法来实现这个。多么不幸 :( 然而缓存仍然可以使其无关紧要。 - Catfish_Man

2
NSDateFormatter的创建/初始化以及格式和区域设置的更改都需要很高的成本。我创建了一个“工厂”类来处理重用我的NSDateFormatters。我有一个NSCache实例,其中存储了多达15个NSDateFormatter实例,基于格式和语言环境信息,在我创建它们的那一刻。因此,当我稍后再次需要它们时,我通过一些使用语言环境为“pt-BR”的日期格式“dd/MM/yyyy”的NSDateFormatter向我的类询问,并且我的类会提供对应的已加载好的NSDateFormatter实例。
您应该同意,在大多数标准应用程序中,每个运行时有超过15种日期格式的情况是罕见的,因此我认为这是缓存它们的极限。如果您仅使用1或2种不同的日期格式,则只会加载这些数量的NSDateFormatter实例。对于我的需求听起来很不错。
如果您想尝试一下,我在GitHub上公开了它。

你在实现中为什么要扩展 NSObject?难道你不能写一个 NSDateFormatter 的扩展吗? - JJD
我需要初始化NSCache实例,我不知道是否可以使用类别来完成,但我认为可以从NSDateFormatter进行扩展。我错过了它。 - Douglas Fischer
1
应该是可以的。在Objective-C中搜索“关联引用”。 - JJD
1
@DouglasFischer,如果您想重新设置日期格式化程序缓存,请参考这篇文章,它描述了使用单例作为NSDateFormatter缓存的绝佳方法-https://krakendev.io/blog/antipatterns-singletons。 - Natalia

0

我认为最好的实现方式如下:

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;
         }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接