好的,您正在将问题定义为似乎没有太多改进空间的地方。根据我的经验,那是相当罕见的。我试图在1993年11月的Dobbs博士的一篇文章中对此进行解释,从一个设计合理,无明显浪费的平凡程序开始,并进行了一系列优化,直到其挂钟时间从48秒减少到到1.1秒,源代码的大小减少了4倍。我的诊断工具是this。更改的顺序是这样的:
发现的第一个问题是使用列表集群(现在称为“迭代器”和“容器类”)占了一半以上的时间。那些被替换为相当简单的代码,使时间减少到20秒。
现在最大的时间接受者是建立更多的清单。以百分比计,它以前没那么大,但是现在是因为消除了更大的问题。我找到了一种加快速度的方法,时间减少到17秒。
现在很难找到明显的罪魁祸首,但是我可以做一些较小的罪魁祸首,而时间缩短到13秒。
现在我似乎撞墙了。这些样本准确地告诉了我它在做什么,但是我似乎找不到任何可以改进的地方。然后,我对程序的基本设计,事务驱动的结构进行了反思,并询问该程序正在执行的所有列表搜索是否实际上都是由问题的要求所强制执行的。
然后我进行了重新设计,在该程序中,实际上是从较小的一组源中(通过预处理器宏)生成了程序代码,并且在该程序中,程序并没有不断地弄清程序员知道是可以预知的。换句话说,不要“解释”要做的事情的顺序,而要“编译”它。
- 重新设计完成,将源代码缩小4倍,时间减少到10秒。
现在,由于它变得如此之快,因此很难进行采样,因此我给它做的工作量是其的10倍,但接下来的时间是基于原始的工作量。
更多的诊断表明,它花时间在队列管理上。内联这些将时间减少到7秒。
现在,我一直在做的诊断打印非常重要。冲洗-4秒钟。
现在,花费最大的时间是调用malloc和free。回收对象-2.6秒。
继续进行示例,我仍然发现并非绝对必要的操作-1.1秒。
总加速因子:43.6
现在没有两个程序是相同的,但是在非玩具软件中,我总是看到这样的进展。首先,您会获得一些简单的东西,然后获得更多的困难,直到收益递减。然后,您获得的见解很可能会导致重新设计,开始新一轮的提速,直到您再次获得递减的收益。现在,这是它可能是有意义的怀疑是否点++i
或i++
或for(;;)
或while(1)
更快:题型我看到经常堆栈溢出。
PS也许想知道为什么我不使用探查器。答案是这些“问题”中的几乎每个都是一个函数调用站点,这些站点会精确地采样。直到今天,事件探查器仍几乎不了解这样的想法,即语句和调用指令比整个功能更重要的是定位和更容易修复。
我实际上构建了一个探查器来执行此操作,但是对于与代码正在做的事情真正亲密无间的亲密关系,没有什么可替代的。样本数量很少不是问题,因为发现的所有问题都没有很小到可以轻易遗漏的程度。
添加:jerryjvl请求了一些示例。这是第一个问题。它由少量单独的代码行组成,耗时超过一半:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
这些正在使用列表集群ILST(类似于列表类)。它们以通常的方式实现,带有“信息隐藏”,这意味着该类的用户不必关心如何实现它们。编写这些行时(大约800行代码中),并未想到它们可能是“瓶颈”(我讨厌那个词)。它们只是推荐的操作方式。在事后看来,应该避免这些事情很容易,但是以我的经验来看,所有性能问题都是这样。通常,最好避免产生性能问题。最好找到并修复所创建的对象,即使它们“应该避免”(事后看来)。
这是第二个问题,分为两行:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
这些是通过在项目末尾附加项目而建立的列表。(解决方法是将项目收集到数组中,并一次构建所有列表。)有趣的是,这些语句仅花费(即在调用堆栈上)原始时间的3/48,因此它们不在刚开始时是一个大问题。但是,在消除第一个问题之后,它们花费了3/20的时间,因此成为了“大鱼”。总的来说,就是这样。
我可能会补充说,这个项目是从我帮助过的真实项目中提炼出来的。在该项目中,性能问题更为严重(与提速一样),例如在内部循环中调用数据库访问例程以查看任务是否完成。
参考:原始和重新设计的源代码可以在1993年的www.ddj.com中找到,位于文件9311.zip,文件slug.asc和slug.zip中。
编辑2011/11/26:现在有一个SourceForge项目,其中包含Visual C ++中的源代码以及对其进行调整的详细说明。它仅经历上述场景的前半部分,并且不遵循完全相同的顺序,但是仍然会加快2-3个数量级。