C / C ++页面上的许多问题和答案,特别是或间接地讨论了微性能问题(例如,间接函数,直接函数与内联函数的开销),或者使用O(N 2)vs O(N log N)算法100个项目列表。
除非或直到我知道我有问题,否则我始终不关心微性能,也不关心宏性能,而是专注于易于维护的可靠代码。
我的问题是,为什么很多程序员这么在乎呢?对于大多数开发人员来说,这真的是一个问题吗?我是否只是幸运地不必为此担心太多,还是我是一个糟糕的程序员?
C / C ++页面上的许多问题和答案,特别是或间接地讨论了微性能问题(例如,间接函数,直接函数与内联函数的开销),或者使用O(N 2)vs O(N log N)算法100个项目列表。
除非或直到我知道我有问题,否则我始终不关心微性能,也不关心宏性能,而是专注于易于维护的可靠代码。
我的问题是,为什么很多程序员这么在乎呢?对于大多数开发人员来说,这真的是一个问题吗?我是否只是幸运地不必为此担心太多,还是我是一个糟糕的程序员?
Answers:
实际上,性能很少是需要在该详细级别上进行管理的问题。如果您知道将要存储和处理大量数据,则应密切注意这种情况,但是否则,您是对的,并且要更好地保持简单。
最容易陷入的陷阱之一-尤其是在具有如此细粒度控制的C和C ++中-太早进行优化并且水平太高了。通常,规则是:A)在发现问题之前不要进行优化,并且B)不要通过使用探查器来优化尚未证明是问题区域的任何内容。
B)的推论是:众所周知,程序员在预测其性能瓶颈在哪里方面很糟糕,即使对于一个人来说,他们也很擅长。使用分析器,优化速度较慢的部分,或者如果多次调用某段代码,则更改算法,从而导致问题。
./configure
,我敢说,多达75%的运行时间可能花费在脚本运行的程序中的“初始化”代码上。25-50%甚至可能花费在动态链接上。
我认为您列表中的所有内容都是微优化,通常不应该关注它,除了
在100个项目列表上使用O(n * n)vs O(NlogN)算法
我认为应该加以考虑。当然,该列表现在有100个项目,并且对于小n来说一切都很快,但是我愿意打赌,相同的代码将重新用于几百万行的列表,并且代码仍将具有合理地工作。
选择正确的算法绝不是微优化。您永远不会知道两个月或两年后将使用相同代码的哪种数据。与在分析器的指导下易于应用的“微优化”不同,算法更改通常需要进行大量重新设计才能有效利用新算法。(例如,某些算法要求输入数据已经被排序,这可能迫使您修改应用程序的重要部分以确保数据保持排序)
很久以前,在我的第一份工作中,我为嵌入式系统编写了代码。这些系统使用8086微处理器,并且内存有限。我们使用了Intel C编译器。我构建的一个系统需要访问3-d结构数组。就像书中告诉我的那样构建它:调用3个维度的malloc,然后为下一个维度分配行,然后为末端节点分配calloc。
这非常复杂(当时对我而言),我必须进行曲线拟合,ANOVA过程控制和卡方分析。没有图书馆为我们做到这一点;我们必须全部编写并将其全部安装到8086上。
该系统像狗一样运行。快速分析之后,我发现最大的问题之一就是分配器。为了解决这个问题,我放弃了对malloc的所有调用,而是对一个大内存块进行了自己的内存管理。
在同一工作的另一种情况下,客户抱怨其统计过程控制系统的响应时间。我之前的团队设计了“软件PLC”系统,操作员可以使用布尔逻辑来组合信号和跳闸开关。他们用一种简化的语言来编写它,今天我们称之为“领域特定语言”。我记得它看起来像((A1 + B1) > 4) AND (C1 > C2)
这样。
原始设计在每次评估时都会解析并解释该字符串。在我们微不足道的处理器上,这消耗了大量时间,这意味着流程控制器无法像流程运行一样快地进行更新。
我重新审视了它,并决定可以在运行时将该逻辑转换为汇编代码。我对它进行了一次解析,然后每次运行时,该应用都会调用到动态生成的函数中。我猜想就像今天的某些病毒一样(但我不是很清楚)。结果是性能提高了100倍,这使客户和我的老板真的很高兴。
由于我已经构建了一个自定义编译器,因此新代码几乎没有可维护性。但是性能优势远胜于维护劣势。
最近,我正在开发一个需要动态解析XML动态的系统。较大的文件将花费更多时间。这对性能非常敏感。解析太慢会导致UI完全无法使用。
这些事情总是出现。
所以....有时您需要可维护的,易于编写的代码。有时,您希望代码能够快速运行。权衡是您需要对每个项目做出的工程决策。
如果要处理大图像并遍历每个像素,则性能调整至关重要。
让我告诉您有关文化背后原因的一些信息。
如果您接近40岁而不是20岁,并且一直在为成年人谋生,那您就已经成年了,那时C ++确实是城里唯一的游戏,台式机应用是常态,而硬件仍然在带宽/性能方面大大落后于软件。
极少数人有今天担心这些事情。
但是,十年前,您仍然需要担心您的软件是通过56kb的调制解调器下载的,并在运行5年的PC上运行的。您还记得1996年的PC多么糟糕吗?考虑一下4GB硬盘,200Mhz处理器和128Mb RAM ...
而10年前的服务器呢?戴尔的“下一代”服务器售价为2000美元,并配有2个(!)1Ghz奔腾处理器,2Gb或Ram和20Gb硬盘。
这只是一场不同的比赛,所有拥有10年经验的“高级”工程师(这些家伙很可能会回答您的问题)在那种环境下cut之以鼻。
这里已经有10个答案,有些真的很好,但是因为这是我的个人宠儿...
过早的优化a)比简单的解决方案要花更多的时间b)引入更多的代码,其中简单的解决方案只有一半的大小和一半的复杂度,并且c)绝对不应该让可读性降低。但是,如果开发人员可以在使用std :: map或std :: vector之间进行选择,并且出于纯粹的无知而选择了错误的集合,则其性能甚至不如过早的优化那样糟糕。如果您今天可以稍稍更改代码,保持可读性,保持相同的复杂性,但使其效率更高,该怎么办呢?还是您称其为“过早优化”?我发现很多人甚至都不会以任何一种方式思考。
我曾经是一个建议“微优化”的人,只需要很少的改动,而我得到的反馈与您刚才说的一样,“您不应该过早地进行优化。让我们开始吧,我们将对其进行更改。如果出现性能问题,请稍后”。我们修复了几个版本。是的,这是一个性能问题。
尽管早期的优化可能不是很好,但是我认为如果人们在编写代码时了解代码将要做什么,而不是简单地忽略任何导致O(x)表示为“优化”的问题,这将非常有益。您现在可以编写很多代码,而对性能的一点思考就可以避免80%的问题。
还请考虑,您的环境中不会立即发生许多性能问题。有时候,您会有一个客户来限制,或者另一个开发人员决定在您的框架之上进行构建,并将对象数量增加10倍。尽管现在有了一些有关性能的信息,您以后可以避免进行非常昂贵的重新设计。而且,如果在软件正式发布后发现问题,那么即使是简单的修复,其使用成本也要贵20倍。
因此,总而言之,随时注意表现有助于养成良好的习惯。与编写简洁,尽可能简单和组织化的代码一样重要。
我怀疑您看到的很多都是简单的采样错误。当人们处理简单的情况时,他们编写代码,这就是结局。他们在处理相对棘手的问题时会提出问题,例如需要优化,尤其是在不一定明显需要优化的情况下。
就是说,无疑也涉及一些过早的优化。正确与否,C和C ++在性能方面享有盛誉,这往往会吸引那些关心性能的人们-包括那些可能因为真正需要而尽可能多地为获得乐趣而进行优化的人。
i++
或花费时间++i
operator ++
没有编译的问题。
其他两个答案都提到了嵌入式系统,我想对此进行扩展。
例如,有许多包含低端处理器的设备:您家中的锅炉控制器,一个简单的袖珍计算器或现代汽车中的数十个芯片。
为了省钱,这些闪存可能具有大量的闪存(用于存储代码)和RAM,对于那些仅为PC或智能手机编写代码的用户而言,它们显得微不足道。为了节省功率,它们可以以相对较低的时钟速率运行。
举个例子,STM32系列微控制器从24 MHz,16 KB闪存和4 KB RAM到120 MHz,1 MB闪存和128 KB RAM。
当为此类芯片编写代码时,如果您希望使代码尽可能高效,则可以节省大量时间。显然,过早的优化仍然不是一个好主意。但是通过实践,您将学习如何快速和/或用最少的资源解决常见问题,并相应地进行编码。
这些本质上是低级语言,当一个人遇到一种病理表现情况时,一个无关紧要的细节在99%的时间内引起了瓶颈,实际上,一个人有机会直接解决该问题(与其他大多数人不同)语言);但是当然,通常,如何立即最有效地执行操作尚不明显。因此,这里有一半的奇怪/有趣的微观优化问题被问到。
另一半来自那些对它们与金属的接近程度感到好奇的人。毕竟,这些本质上是低级语言。
在使用C和C ++时,性能始终是一个热门话题。关于应该走多远,您总是可以疯狂地达到内联ASM的地步,或者使用指针算法来加快迭代速度。但是,有一点指向人们花费大量时间进行优化,以至于开发整个程序的工作就停止了。
处理这些问题时,需要考虑程序员的性能和代码的性能。这些重点放在哪一个总是会提出有趣的问题。最后,最重要的问题是它对用户的吸引力。用户是否将使用创建具有数百或数千个元素的数组的数据?在这种情况下,为使事情快速完成而进行的编码可能会使您的用户抱怨程序的标准操作很慢。
然后就是将要处理少量数据的用户。如果您使用的高级功能使您更容易进行维护,而牺牲了一些性能,那么在这里和那里的一些文件,诸如排序和文件操作之类的操作对于用户而言就不会那么明显。
这只是您将遇到的问题的一个小例子。其他事项包括目标用户的硬件。如果您要处理嵌入式系统,那么您将不得不担心更多的性能,例如,如果您的用户使用的是具有ram内存的双核计算机。
老实说,这取决于您的目标,以及您是专业编程还是业余爱好。
如今,现代计算机确实是功能强大的机器。无论您决定执行什么基本操作,无论您是否尝试进行微优化,它们都可以使他们的工作非常快。但是,当然,如果您要执行其他操作(例如,针对物理或化学等领域的超级计算),则可能需要根据需要进行尽可能多的优化。
早期的MIT程序员并非天生就制作出很棒的东西。他们开始简化和增强现有算法。他们的骄傲是使2 + 2的得分比现有算法少四分之二(这只是一个例子,您知道了)。他们不断尝试在TI-83机器中使用更少的打孔卡来提高性能。
另外,如果您正在为嵌入式系统编程,那么您当然必须关注微性能。您不希望慢速的数字时钟比另一个数字时钟的时间慢5纳秒。
最后,如果您是一个业余程序员,那么即使程序速度很快,优化最小的细节当然也没有害处。不需要它,但是可以肯定的是您可以从事一些工作,并有机会学习更多。如果您是专业从事某款软件的开发人员,那么除非非常需要,否则您将无法获得如此奢华。
在这些讨论中,我一直认为缺少一个答案,这使我有点累加了能源消耗。
当然,如果您使用高级解释语言编写程序,并使其在具有几层间接层的浏览器中运行,或者循环所用的时间是0.01秒而不是0.001秒,那么也许没什么大不了的。没有人会注意到,也就是说,没有个人用户会注意到。
但是,当在某些情况下成千上万甚至数百万个用户使用您的代码时,所有这些额外的低效率加起来了。如果您的工具每天仅阻止CPU进入睡眠状态10秒钟,并且有100万用户使用它,那么效率低下的算法每天仅会消耗140 kWh [1]。
我很少讨论这个问题,我认为这很可悲。我强烈怀疑,对于流行的框架(如Firefox)和精美的交互式Web应用程序,这些数字要差得多,这将是一个有趣的研究。
[1]我只是用一千万秒乘以50瓦来弥补这一点。确切数字取决于很多事情。
有时,您所拥有的算法不能比线性时间更好,而线性时间仍然需要强大的性能。
一个示例是视频处理,在不循环遍历每个像素的情况下,您不能使图像/帧更明亮(作为一个基本示例)(嗯,我想您可以使用某种层次结构来指示子级继承的属性,这些属性最终会下降到图像块中对于叶节点,但是您将需要花费更高的成本循环遍历每个像素到渲染器,并且即使是最优化的图像过滤器,代码也可能难以维护)。
在我的领域中有很多这样的案例。与受益于任何复杂数据结构或算法的线性复杂度循环相比,我倾向于做更多的线性复杂度循环,这些循环必须涉及所有内容或读取所有内容。当必须触摸所有内容时,没有可以跳过的工作。因此,在这一点上,如果您不可避免地要处理线性复杂性,则必须使每次迭代完成的工作越来越便宜。
因此,在我的情况下,最重要和最常见的优化通常是数据表示和内存布局,多线程和SIMD(通常按此顺序排列,其中数据表示最重要,因为这会影响执行后两者的能力)。我没有遇到太多问题,这些问题可以通过树,哈希表,排序算法以及类似的东西来解决。我的日常代码更像是“为每件事做某事”。
当然,讨论何时需要优化(更重要的是不需要优化),微处理或算法是另一种情况。但是在我的特殊情况下,如果关键执行路径需要优化,则通常可以通过微级优化(如多线程,SIMD以及重新布置内存布局和访问模式以提高引用位置)来实现10倍以上的速度提升。我很少会说用Introsort或radix排序替换冒泡排序,或者用BVH替换二次复杂性冲突检测,而发现热点可以从热/冷场拆分中受益。
现在,在我的情况下,我的领域对性能至关重要(光线跟踪,物理引擎等),以至于缓慢但完全正确的光线跟踪器(要花费10个小时来渲染图像)通常被认为是无用的,或者比完全互动但快速的光线跟踪器更为有用。输出最丑陋的图像,由于缺少水密射线/三重交点,射线泄漏到处。速度可以说是此类软件的主要质量指标,可以说甚至比某种程度上的正确性还高(因为“正确性”是光线追踪的模糊概念,因为所有事物都是近似的,只要它不会崩溃或类似的东西)。在这种情况下,如果我不考虑预先的效率,我发现我实际上必须在最昂贵的设计级别上更改代码以处理更高效的设计。所以如果我不
游戏是与我相似的另一个领域。如果您的游戏像幻灯片一样以每秒1帧的速度运行,那么游戏逻辑的正确性或代码库的可维护性和出色的设计都无所谓。在某些领域,缺乏速度实际上会使应用程序对其用户无用。与游戏不同,在光线追踪等领域没有“足够好”的指标。用户始终希望获得更高的速度,而工业竞争主要是在寻求更快的解决方案。除非它是实时的,否则它将永远不够好,届时游戏将使用路径跟踪器。然后对于VFX来说可能还不够好,因为从那时起,美术师可能想加载数十亿个多边形,并以30+ FPS的速度对数十亿个粒子之间的自碰撞进行粒子模拟。
现在,尽管如此,尽管如此,我仍然使用脚本语言(Lua)编写了大约90%的代码,而无需担心性能。但是我有异常大量的代码,实际上确实需要遍历数百万至数十亿的事物,而当您遍历数百万至数十亿的事物时,您的确会注意到天真的单线程代码之间的巨大差异,每次迭代都会调用一个高速缓存未命中,比方说,矢量化代码在并行访问连续块中运行,在连续块中没有无关的数据加载到高速缓存行中。
总的来说,真的不可能回答这个问题。当今正在构建的大多数软件都是内部网站和LOB应用程序,对于这种编程,您的推理是非常正确的。另一方面,如果您正在编写设备驱动程序或游戏引擎之类的东西,则没有优化是“过早的”。您的软件可能会在具有不同硬件限制的完全不同的系统上运行。在这种情况下,您应该针对性能进行设计,并确保不要选择次优算法。
我认为非常关心性能的程序员的问题是,有时在他的一生中,他需要编写微性能代码,也许非常紧急,并且他学会了,学到了很多东西,最后他知道了很多事情和技巧。
现在,很难忘记,并且无需事先测量即可显示出他不需要担心,他使用快速代码站在安全的一面。
展示自己的深厚知识,技能和一些技巧,并重用您学到的东西,总是很高兴的。它使您感到很有价值,花时间在学习上,值得。
有时,在我的生活中,我了解到前缀增量更快...
for (int i = 0; i < MAX; ++i)
...比后缀增加:
for (int i = 0; i < MAX; i++)
现在,如果MAX为低,则没有关系,如果循环中有实际工作,也将无所谓。但是,即使今天的编译器自行优化代码,也没有理由使用postfix版本。
寻求性能的人可能除了编写“工作代码”(例如“工作和可读代码”)外,还需要一个额外的目标,以便在众多选择中获得指导。
我是否有幸不必为此担心太多,还是我是一个糟糕的程序员?
您关心您的要求吗?如果性能不是必需的,那就不用担心。花大量的时间对您的雇主不利。
在一定程度上,性能始终是必需的。如果您可以不考虑它就打它,那么您就可以不考虑它。
就个人而言,当我的测试需要很长时间才能通过时,我通常会受到性能的驱动。我太急躁,无法等待5分钟才能通过一组测试。但这通常可以通过摆弄测试来解决。
我的问题是,为什么很多程序员这么在乎呢?对于大多数开发人员来说,这真的是一个问题吗?
有大量的程序员在乎他们的关心程度是有道理的。有很多人不是。让我们来谈谈那些不是。
在使事物真正起作用之后,程序员在学校学习的第一件事就是大O表示法。他们中的许多人都正确地学习了这一课,因此适当地专注于受n极大影响的事物。其他人则不懂数学,而只是取而代之的是,它必须很快就可以运用。更糟糕的是,这些学生中的一些人除了使代码正常工作并使其快速运行之外,再也没有学到任何关于代码重要的事情。错过的课程:使其可读性强,设计精良,无缘无故地玩耍。
克努斯是对的:过早的优化是万恶之源。但是一旦奏效,下一步是什么?快吧?没有!下一步是可读的。可读是第一步,下一步,中间步骤和最后一步。我发现许多执行不必要的性能优化的人正在总线下提高可读性。
有些人甚至因其代码的可读性而感到不快。他们不得不苦苦寻找难以理解的其他人创建的代码,所以现在轮到他们来回收投资了。
我知道这一点,因为我曾经这样做过。如果结构精简到难以理解的单行布尔表达式,我曾经将一条完全可读的5行重构,并自豪地将其发送给我的教授,期望给我留下深刻的印象,因为我可以创建出如此紧凑而令人生畏的东西。我没有得到我想要的赞美。
如果代码保持可读性,则使以后快速变得容易。这就是为什么Knuth强调“过早”而不是“不需要”的原因。因为可以肯定,越快越好。但是更好只有更好,这取决于您为此付出了什么。因此,请等到知道真正需要的性能后再为此做出牺牲。牺牲可读性很勉强,因为一旦它消失了,就很难找回。
超越可读性的是整个软件设计领域。这个网站是关于什么的。有些人不知道如何设计。因此,由于他们不能给设计留下深刻的印象,所以会造成难以理解的混乱,因此人们无法说出自己毫无头绪。既然没有人修复过他们的代码,那一定是好的代码吧?
对于某些人来说,性能是抓住一切做任何事情的借口。程序员具有很大的权力和自主权。他们之间已经有了信任。不要滥用信任。