微优化值得投入时间吗?

44

我是一名PHP开发者,我一直认为微小的优化不值得花时间去做。如果你真的需要额外的性能,你要么编写更高效的软件架构,要么编写一个C++扩展程序来处理耗时任务(或者最好使用HipHop编译代码)。然而,今天我的一位同事告诉我,在字符串连接时使用单引号和双引号有很大的区别。

is_array($array)

$array === (array) $array

我当时觉得“嗯,这真的是一个毫无意义的比较”,但他不同意我的看法..而他是我们公司最优秀的开发人员之一,并负责一个每天执行约5000万个SQL查询的网站。因此,我在想:他可能错了吗?微观优化真的值得花时间去做吗?


30
在公司里仅仅成为最优秀的开发者并不一定有很大意义,至少在许多《The Daily WTF》的故事中,他们是做出最疯狂奇怪事情的那些人;-) - Joey
4
使用存储过程并防止往返查询可以减少查询次数……这就是一种优化。 - gbn
就某个表达式比另一个更可取的断言而言,PHP文档网站上的这条评论提出了同样的观点,但测试似乎有些缺陷(如果我正确理解PHP,它只会由于短路评估而测试$test数组)。我想知道,如果被测试的项目不是数组,是否仍然得出相同的结果?:http://www.php.net/manual/en/function.is-array.php#98156 - Michael Burr
8
清洗化油器肯定会提高汽车性能。但安装燃油喷射系统将更加有效。如果您只是开车三个街区,使用化油器就可以了。如果您想参加NASCAR比赛,请找到一种方法来彻底整修引擎。 - bcosca
2
你的同事告诉你的是一个谬论。 - Artefacto
10个回答

142
对于一个非常小的数组,$array === (array) $arrayis_array($array) 快得多。快了7倍以上。但每次调用只需要大约 1.0 x 10 ^ -6 秒(0.000001秒)。所以,除非你要调用它成千上万次,否则不值得这样做。如果你确实需要调用成千上万次,那么我建议你重新考虑你的代码逻辑...
当处理大型数组时,差异就会显现出来。因为 $array === (array) $array 需要复制一个新变量,并且需要在内部迭代比较数组,所以对于大型数组来说,它会慢得多。例如,在一个包含100个整数元素的数组中,is_array($array) 和小型数组的误差范围相同(< 2%),在10000次迭代中运行时间为 0.0909 秒。但是,$array = (array) $array 的速度非常慢。对于只有100个元素的数组,它已经比 is_array() 慢了两倍以上(运行时间为 0.203 秒)。对于1000个元素,is_array 的运行时间保持不变,但转换比较的时间增加到 2.0699 秒...
它在小数组中更快的原因是is_array()有函数调用的开销,而强制转换操作是一种简单的语言结构...在C代码中迭代小变量通常比函数调用开销更小。但是,对于较大的变量,差异会增加...
这是一个权衡。如果数组足够小,则迭代效率更高。但随着数组大小的增加,它将变得越来越慢(因此函数调用将变得更快)。
另一种看待它的方式是检查每个转换的算法复杂度。
让我们先看一下is_array()。它的source code基本上显示它是一个O(1)操作。这意味着它是一个恒定时间操作。但我们还需要看看函数调用。在PHP中,具有单个数组参数的函数调用可以是O(1)O(n),具体取决于是否需要触发写时复制。如果在$array是变量引用时调用is_array($array),将触发写时复制并执行变量的完整拷贝。
因此,is_array()是最佳情况下的O(1)和最坏情况下的O(n)。但只要不使用引用,它始终是O(1)...
另一方面,类型转换版本执行两个操作。它进行强制类型转换,然后进行相等性检查。因此,让我们分别看一下每个操作。首先,类型转换运算符handler强制执行输入变量的a copy。无论它是否是引用。因此,仅使用(array)强制转换运算符会导致对数组进行O(n)迭代以进行转换(通过copy_ctor调用)。
然后,它将新副本转换为数组。这对于数组和基元来说是O(1)的,但对于对象来说是O(n)的。
然后执行相同的运算符。 handler 只是一个 is_identical_function() 的代理。现在,如果 $array 不是数组,is_identical 将短路。因此,它的最佳情况为 O(1)。但是,如果 $array 是一个数组,它可以再次短路,如果哈希表相同(意味着两个变量都是彼此的写入副本)。因此,该情况也是 O(1)。但请记住,我们在上面强制进行了复制,所以如果它是一个数组,我们不能这样做。因此,由于zend_hash_compare,它是O(n)...

因此,最坏情况下运行时间的结果如下表:

+----------+-------+-----------+-----------+---------------+
|          | array | array+ref | non-array | non-array+ref |
+----------+-------+-----------+-----------+---------------+
| is_array |  O(1) |    O(n)   |    O(1)   |     O(n)      |
+----------+-------+-----------+-----------+---------------+
| (array)  |  O(n) |    O(n)   |    O(n)   |     O(n)      |
+----------+-------+-----------+-----------+---------------+

请注意,它们看起来对于引用变量的比例是相同的。但它们并不相同。它们都以线性方式扩展引用变量。但常数因子会改变。例如,在大小为5的引用数组中,is_array将执行5次内存分配和5次内存复制,然后进行1次类型检查。另一方面,转换版本将执行5次内存分配和5次内存复制,然后进行2次类型检查,接着进行5次类型检查和5次等式检查(memcmp()或类似函数)。所以n=5对于is_array产生11个操作,而对于===(array)产生22个操作...

现在,is_array()确实有一个栈推入的O(1)开销(由于函数调用),但这仅在极小的n值下占主导地位(我们在上面的基准测试中看到,仅10个数组元素就足以完全消除所有差异)。

结论

我建议选择可读性更好的方法。我认为is_array($array)$array === (array) $array更易读。所以你可以兼得两种方法的优点。

我用于基准测试的脚本:

$elements = 1000;
$iterations = 10000;

$array = array();
for ($i = 0; $i < $elements; $i++) $array[] = $i;

$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) is_array($array);
$e = microtime(true);
echo "is_array completed in " . ($e - $s) ." Seconds\n";

$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) $array === (array) $array;
$e = microtime(true);
echo "Cast completed in " . ($e - $s) ." Seconds\n";

编辑:记录一下,这些结果是在Linux上使用5.3.2得出的...

编辑2:修复了数组速度较慢的原因(由于迭代比较而不是内存原因)。请参见compare_function中的迭代代码...


10
+1. 我认为“最佳程序员”应该在你的答案和基准代码片段中得到充分展示。 - Vladislav Rastrusny

92

当你有证据表明你正在优化一个瓶颈时,微优化是值得的。

通常情况下,它并不值得——尽可能编写最易读的代码,并使用真实的基准测试来检查性能。当你发现自己遇到了瓶颈时,只需微调那部分代码(同时进行测量)。有时微小的优化可以带来巨大的改善。

但是不要所有代码进行微优化……这将使维护变得更加困难,并且很可能会发现你忽略了真正的瓶颈,或者你的微小优化反而损害了性能,而不是帮助它们。


3
完全同意。这值得花时间吗?你可以为你的应用程序进行分析,看看优化会带来足够的好处。回答另一个问题:"应该对所有东西进行微观优化吗?"绝对不是。在大多数情况下,清晰可读和易于维护的代码比不敏感性能的快速代码更重要。编程涉及到权衡和平衡。代码复杂度、可维护性、性能、开发人员时间成本、硬件需求成本。在大多数情况下,开发时间和错误比硬件成本更为昂贵。 - rocketmonkeys
6
@Jon - 你能否重新开始写有关C#等内容的书籍,这是我喜爱阅读的,而把低难度的内容留给我们这些凡人吗? - Peter M
23
哦不,他也要去争取PHP徽章! - Max
2
@Jon - 你把一个 Stack Overflow 声望值为 205K 的用户称作“偶尔的”干扰???但我期待着阅读你的下一本书。《C# 深度剖析》让我想起了 Scott Meyers 的 C++ 书籍,我非常喜欢。 - Peter M
@Mike:不,我在书中确实很少提到有关性能分析方面的内容。 - Jon Skeet
显示剩余2条评论

12
微观优化值得花时间吗?
答案是“不值得”,除非知道了代码的某一行占用了大量的时间,那么才值得进行优化。
换句话说,首先进行性能分析,只有在了解了程序的时间消耗后,才会考虑微观优化。无论使用哪种语言或操作系统,这是我依赖的方法
补充:当许多程序员讨论性能时,从专家到初学者,他们往往谈论程序花费时间的“位置”。这个“位置”中存在一个隐蔽的歧义,会使他们偏离可以节省最多时间的事情,即函数调用点。毕竟,在应用程序顶部的“调用Main”几乎从未处于“位置”,但它却占用了100%的时间。现在你肯定无法摆脱“调用Main”,但几乎总会有其他可以消除的调用站点。当程序打开或关闭文件、将某些数据格式化为文本行、等待套接字连接、新分配一块内存或通过大型数据结构传递通知时,它将花费大量时间来调用函数,但那是否就是它花费时间的“位置”呢?无论如何,这些调用可以通过堆栈采样快速找到。

关于“使用堆栈样本快速找到问题”的问题:是的,但即使这么简单,很少有人知道(也许这就是问题所在)。例如,在Visual Studio中通常可以完成此操作,而无需任何其他工具。 - Peter Mortensen
@PeterMortensen:这是一个教育问题。40年前我是一名教授,亲眼目睹了这一切。教授们很少接触到真正的大型代码——他们更关注算法和大O符号,这在某种程度上是可以理解的。当课程要求“讨论性能分析”时,他们会谈论gprof及其现代版本。完成任务后,他们就会继续下一个话题。那么学生们该怎么办呢?在课堂上,他们敞开心扉,希望被智慧充实。然而毕业后,他们却认为角色发生了反转——他们的工作是教育我们这些老古董他们所学到的东西。我相信你也见过这种情况。 :-) - Mike Dunlavey

6
俗话说得好,微观优化通常只在代码中最小的、性能至关重要的热点处值得花时间,而且只有在你已经证明那是瓶颈所在的情况下才值得这样做。然而,我想稍微展开一下,指出一些例外和误解的地方。
1. 这并不意味着完全不考虑性能。我将微观优化定义为基于编译器/解释器、硬件等低级细节的优化。按照定义,微观优化不影响大 O 复杂度。宏观优化应该提前考虑,特别是当它们对高层设计产生重大影响时。例如,可以非常肯定地说,如果你有一个大型、频繁访问的数据结构,则 O(N) 的线性搜索不会奏效。即使是只有常数项但有明显大量开销的事情,也值得提前考虑。两个很好的例子是过度内存分配/数据复制以及计算相同的东西两次,而你本可以计算一次并存储/重用结果。
2. 如果你正在做的事情在稍微不同的上下文中已经做过,可能有一些瓶颈是如此众所周知,以至于提前考虑它们是合理的。例如,我最近在为 D 标准库实现快速傅里叶变换算法 (FFT)。由于许多 FFT 已经在其他语言中编写过了,所以非常明显最大的瓶颈是缓存性能,因此我立即考虑如何优化这一点。

4
通常情况下,你不应该编写任何优化使你的代码更加丑陋或难以理解;在我的书中,这绝对属于这一类别。
改变旧代码比编写新代码要困难得多,因为你必须进行回归测试。因此,通常情况下,已经在生产中的代码不应该因为琐碎的原因而被更改。
PHP是一种非常低效的语言,如果你遇到性能问题,你应该考虑重构热点,使它们执行更少的PHP代码。
所以我会说一般情况下不需要,这种情况也不需要,只有在你绝对需要它并且已经衡量过它可以带来可证明的差异并且是最快的胜利(最容易实现的),才需要使用。
当然,在你现有的、工作的、经过测试的代码中散布这样的微小优化是一件可怕的事情,它肯定会引入回归和几乎肯定不会有任何显著的差异。

3
我假设is_array($array)是首选方法,而$array === (array) $array则是所谓的更快的方法(这也带来了一个问题,为什么不使用该比较实现is_array,但我偏离了主题)。
我几乎不会回到我的代码中插入微小的优化*,但我经常在编写代码时加入它们,前提是:
  • 它不会减慢我的打字速度。
  • 代码的意图仍然清晰。
那种优化在这两个方面都失败了。

* 好吧,实际上我确实会,但这更多是因为我有轻微的强迫症,而不是好的开发实践。


尽管我不是PHP开发者,我知道这有点与实际问题无关,但我很感激有人(不一定是James)评论一下为什么有这样的性能差异(假设它是真的),以及James提出的问题(为什么没有使用快速比较来实现is_array())。 - Michael Burr
@Michael:除了我之外,必须有其他人(我也不是PHP开发者) - James Curran
明白了,我试图让注释表明这一点。此外,我意识到这只是一种闲散的好奇心(尽管微观优化可能是邪恶的,但我仍然经常对各种语言构造背后发生的事情感到好奇)。 - Michael Burr
关于您提出的问题,答案是:前提是错误的。它通常不会更快。 - Artefacto

3
我们的优化在一个地方非常有帮助。以下是一些比较结果:
- `is_array($v)` : 10秒 - `$v === (array)$v` : 3.3秒 - `($v.'') === 'Array'` : 2.6秒
最后一个条件会将$v强制转换为字符串,其中,Array总是被转换为值“Array”的字符串。如果$v是具有“Array”值的字符串(在我们的情况下永远不会发生),则此检查将出错。

这不是一个合成测试,而是应用程序运行的一部分。 - DronNick
1
类型检查(第二个片段,3.3秒)是我认为最好的方法。第一个方法需要函数调用的开销,而第三个方法难以一眼看出并理解。然而,对于大型数组,它可能会有严重的性能问题,因为值被复制了。 - John Weisz

1

微观优化不值得。代码可读性比微观优化更重要。

关于无用的微观优化的文章 by Fabien PotencierSymfony 框架的创造者)很棒:

print 和 echo,哪个更快?

Print 使用了一个额外的操作码因为它实际上返回了一些东西。我们可以得出结论,echo 比 print 更快。但是一个操作码几乎什么都不花费。即使脚本有数百次调用 print。我已经在全新的 WordPress 安装中尝试过了。这个脚本在我的笔记本电脑上在结束之前停止并显示“总线错误”,但操作码的数量已经达到了 230 多万。足以说明问题。


1

嗯,有比速度更重要的事情需要考虑。当你看到“更快”的替代方法时,你是立刻想到“哦,这是在检查变量是否是数组”,还是想着“…什么鬼”?

因为实际上,考虑使用这种方法时,它被调用的频率有多高?确切的速度优势是什么?当数组较大或较小时,是否有效?不能在没有基准测试的情况下进行优化。

此外,如果优化降低了代码的可读性,则不应该进行优化。事实上,减少几十万次查询(这通常比人们想象的要容易得多)或者在适用的情况下对其进行优化,对性能会更加有益,而不是进行微小的优化。

另外,不要被那个经验丰富的人吓倒,正如其他人所说,要自己思考。


1
IMHO,如果您在性能关键领域工作,微观优化实际上比算法优化更加相关。这可能是个大的“如果”,因为即使对于性能关键软件,许多人实际上也不在性能关键领域工作,因为他们可能只是调用第三方库中的高级函数,该库执行实际的性能关键工作。例如,现在许多人尝试编写图像或视频软件时,会编写非性能关键代码来表达他们想要的图像水平,而不必自己手动循环遍历几百万像素并以每秒100多帧的速度运行。由库代替他们完成。
当我说微观优化比算法优化更重要时,我的意思不是说,例如,将最小化缓存未命中应用于冒泡排序的并行SIMD代码将击败introsortradix sort。我的意思是,专业人士不会使用冒泡排序处理大量输入数据。
如果你现在使用任何一个相对高级的编程语言,比如C++,那么你已经可以使用一些相当高效的通用数据结构和算法了。除非你是一个刚开始涉足计算机科学的初学者,并且正在重新发明最原始的轮子,否则没有任何借口应该将二次复杂度排序应用于大规模输入大小或者使用适当的数据结构就可以在常数时间内完成的线性搜索。
所以一旦你超过了这个初级阶段,对性能要求很高的应用程序仍然具有极大的性能差异。为什么呢?为什么一个视频处理软件的帧率可以比另一个快三倍以上,而开发人员并没有做任何极其愚蠢的算法操作?为什么一个执行非常类似的任务的服务器可以使用相同的硬件处理十倍的查询量?为什么这个软件可以在5秒钟内加载一个场景,而另一个软件需要5分钟才能加载相同的数据?为什么这个漂亮的游戏可以拥有丝般顺滑和连贯的帧率,而另一个游戏在图形和照明方面更加原始和丑陋,并且偶尔会出现卡顿,同时还需要两倍的内存?
这归结为微观优化,而不是算法上的差异。此外,我们今天的内存层次结构性能相差巨大,如果算法的空间局部性不好,那么几十年前被认为很好的算法现在可能已经不再适用了。

所以,如果你想编写具有竞争力的软件,今天很大程度上会涉及多线程、SIMD、GPU、GPGPU、通过改进内存访问模式(循环瓦片、SoA、热/冷字段拆分等)提高参考局部性,甚至在极端情况下优化分支预测等方面,而不是算法突破,除非你正在攻克极少有程序员探索过的领域。

有时仍会出现潜在改变游戏规则的算法突破,例如最近的体素锥追踪。但这些是例外,那些想出这些方法的人通常会投入他们的生命到研究和开发中(他们通常不是编写和维护大型代码库的人),并且它仍然归结为微优化是否可以将体素锥追踪应用于实时环境,如游戏等。如果您不擅长微优化,则即使使用这些算法突破,也无法获得足够的帧速率。


抱歉,我在这篇文章中有点发牢骚。我对细微差别和上下文非常着迷,有时会对一般的经验法则感到不耐烦。即使是 Knuth 的原始论文,引用了流行的“过早优化是万恶之源”的名言,也是一篇提出微小优化以优化带有 goto 指令的循环的论文,但要特别注意节约使用此类优化,只在需要和计数时使用,并配备分析器(甚至进入一个整个部分,阐述 Knuth 认为所有编译器都应该配备内置分析器)。 - user4842163
我认为需要一生甚至更长时间来掌握的主要技能是有效地设置优先级(我们都可能需要在这方面努力)。分析器有助于抵制那种丑陋的程序员冲动,即优化一切,而实际上往往什么都没有优化,反而使一切变得复杂(通常是悲观的优化而不是乐观的优化)。如果我们的优先事项正确,绝大部分代码库应该被设计成尽可能易于理解和维护。但是,在优化的背景下,“微小”的概念在循环情况下可能会产生比微小更多的影响。 - user4842163

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