为什么这么多开发人员认为性能,可读性和可维护性不能共存?


34

在回答这个问题时,我开始怀疑为什么这么多的开发人员认为好的设计不应该考虑性能,因为这样做会影响可读性和/或可维护性。

我相信,一个好的设计在编写时也要考虑性能,并且一个好的设计的优秀开发人员可以编写一个高效的程序,而不会对可读性或可维护性产生不利影响。

尽管我承认存在极端情况,但为什么许多开发人员坚持认为高效的程序/设计将导致较差的可读性和/或较差的可维护性,因此,性能不应作为设计考虑因素?


9
大规模地对此进行推理几乎是不可能的,但是对于一小段代码而言,这是显而易见的。只需比较一下Quicksort的可读版本和有效版本。
SK-logic

7
亩。您应该从支持许多开发人员坚持认为效率导致不可维护性的声明开始。
彼得·泰勒

2
SK-logic:在我看来,这是所有堆栈交换站点中最好的部分之一,因为人们开始质疑显而易见的事物,因为它不时会变得健康。对您而言可能显而易见的东西对其他人而言可能并不明显,反之亦然。:)分享是关怀。
Andreas Johansson

2
@贾斯汀,不。在我看来,该线程假定了一种情况,即必须在有效代码或可维护代码之间进行选择。发问者没有说他在这种情况下发现自己的频率,并且回答者似乎并没有声称自己经常处于这种情况下。
彼得·泰勒

2
-1为问题。当我读到它时,我以为这是一个唯一的真实答案:“因为他们不使用python”。
Ingo

Answers:


38

我认为这样的观点通常是对过早(微)优化尝试的反应,这种尝试仍然很普遍,而且弊大于利。当一个人试图反驳这种观点时,很容易陷入另一种极端,或者至少看起来像是另一种极端。

但是,随着近几十年来硬件资源的巨大发展,对于当今编写的大多数程序而言,性能不再是主要的限制因素,这是事实。当然,在设计阶段应该考虑预期和可实现的性能,以便确定性能可能成为(主要)问题的情况。然后从一开始就设计性能确实很重要。但是,整体的简单性,可读性和可维护性仍然更为重要。正如其他人指出的那样,与最简单的解决方案相比,性能优化的代码更复杂,更难以阅读和维护,并且更容易出现错误。因此,必须证明花在优化上的任何努力-不仅仅是相信-带来真正的好处,同时尽可能降低程序的长期可维护性。因此,良好的设计可以将复杂的,性能密集的部分与其余的代码隔离开,这些部分应保持尽可能简单和整洁。


8
“当一个人试图反驳这种观点时,很容易陷入-或至少看起来像是另一个极端。”我一直在遇到问题,人们以为我只是在平衡职业者的时候就持有相反的观点。缺点。不只是编程,还有一切。
2011年

1
我很讨厌每个人都在讨论这个,我生气,并采取极端..
托马斯博尼尼

已经有好几个反应,但我认为您已尽最大努力详细说明了这种心态的起源。感谢所有参与的人!
贾斯汀

我的回答是...大多数开发人员的工作都很糟糕
TheCatWhisperer

38

来自从事高性能代码开发工作的开发人员方面的问题,设计中需要考虑几件事。

  • 不要过早地悲观。在复杂度相同的两种设计之间进行选择时,请选择性能最佳的设计。著名的C ++示例之一是循环中计数器(或迭代器)后递增的盛行。这是完全不必要的过早悲观,它可能不会花费您任何钱,但可能会,所以不要这样做。
  • 在许多情况下,您几乎没有业务可以进行微优化。算法优化是一个低挂的成果,并且几乎总是比真正的低级优化更容易理解。
  • 如果并且仅在性能绝对至关重要的情况下,您会陷入困境。实际上,您首先要尽可能地隔离代码,然后陷入困境。它在那里真的很脏,它具有缓存方案,惰性评估,用于缓存的内存布局优化,内联内在函数或汇编块,模板的逐层模板等等。您在这里疯狂地测试和记录文档,您知道它正在发展如果必须对此代码进行任何维护,可能会造成伤害,但是您必须这样做,因为性能绝对至关重要。编辑:顺便说一句,我并不是说这段代码不能漂亮,应该尽可能地使它漂亮,但是与未优化的代码相比,它仍然会非常复杂,而且常常令人费解。

正确处理,美观处理,快速获取。以该顺序。


我喜欢经验法则:“让它变得美丽,让它快速。以该顺序'。我将开始使用它。
马丁·约克

究竟。并在第三点尽可能地隔离代码。因为当您使用不同的硬件时,即使是像具有不同缓存大小的处理器一样小的硬件,这些情况也可能会发生变化。
KeithB 2011年

@KeithB-您的观点很好,我将其添加到我的答案中。
Joris Timmermans

+1:“正确处理,美观处理,快速处理。按此顺序。” 非常好的总结,我同意90%。有时候,一旦我变得漂亮(并且更容易理解),我就只能修复某些错误(正确)。
乔治

为“请勿过早地悲观” +1。避免过早优化的建议是不允许随意使用笨拙的算法。如果您正在编写Java,并且有一个集合,那么您会经常调用contains它,请使用HashSet而不是ArrayList。性能可能无关紧要,但是没有理由不这样做。利用好的设计和性能之间的一致性-如果处理某些集合,则尝试一次完成所有操作,这可能会更易读,而且速度更快(可能)。
汤姆·安德森

16

如果我可以假定“借用” @greengit的漂亮图表,并做一些小的补充:

|
P
E
R
F
O  *               X <- a program as first written
R   * 
M    *
A      *
N        *
C          *  *   *  *  *
E
|
O -- R E A D A B I L I T Y --

我们都被“教导”了权衡曲线。此外,我们都认为我们是这样的最佳的程序员,任何给定的程序,我们写是那么紧它是曲线上。如果计划在进行中,那么一个方面的任何改进必然会在另一个方面产生成本。

以我的经验,程序只能通过调整,调整,锤打,打蜡并变成“代码高尔夫球”来接近任何曲线。大多数程序在各个方面都有很大的改进空间。 这就是我的意思。


我个人认为曲线的另一端在右侧再次上升(只要您向右移动足够远(这可能意味着重新考虑算法))。
马丁·约克

2
+1表示“大多数程序在各个方面都有很大的改进空间。”
史蒂文

5

正是因为高性能软件组件通常比其他软件组件复杂几个数量级(所有其他条件都相同)。

即使这样也不是很明确,如果性能指标是至关重要的要求,则必须使设计具有适应这些要求的复杂性。危险在于,开发人员浪费时间在相对简单的功能上,试图从其组件中挤出几毫秒的时间。

无论如何,设计的复杂性与开发人员快速学习并熟悉这种设计的能力直接相关,并且对复杂组件中功能的进一步修改可能会导致错误可能无法被单元测试捕获。复杂的设计具有更多的方面和可能的测试用例,可以考虑使100%单元测试覆盖率的目标成为梦a以求的目标。

话虽这么说,但应该注意的是,一个性能不佳的软件组件可能会由于原始编写者的愚昧而愚蠢地编写并且不必要地变得复杂而导致性能不佳((如果只需要一个,那么就进行8个数据库调用来构建单个实体) ,完全不必要的代码,无论如何都将导致单个代码路径等。)这些情况更多的是提高代码质量,并且由于重构而导致性能提高,而不是预期的结果。

但是,假设组件设计良好,它将始终比针对性能进行了优化(其他所有条件都相同)的类似设计良好的组件复杂。


3

这些事情并不能并存。问题在于,每个人的代码在第一次迭代时都很慢,难以阅读且无法维护。剩下的时间都花在了改进最重要的事情上。如果那是性能,那就去争取。不要编写恶意的代码,但是如果只需要X快速,那么就使其快速X。我相信性能和清洁度基本上是不相关的。性能代码不会导致难看的代码。但是,如果您花时间调整所有代码以使其更快,您猜您没有花时间做什么?使您的代码干净且可维护。


2
    |
    P
    Ë
    [R
    F
    *
    R * 
    M *
    一种 *
    N *
    C * * * * *
    Ë
    |
    O-可读性-

如你看到的...

  • 牺牲可读性可以提高性能-但只有这么多。在特定点之后,您必须诉诸“真实”的手段,例如更好的算法和硬件。
  • 而且,以可读性为代价而导致的性能损失只能在某种程度上发生。之后,您可以使程序具有尽可能高的可读性,而不会影响性能。例如,添加更多有用的评论不会影响收费。

因此,性能和可读性只是适度相关的-在大多数情况下,并没有真正的大的动机来鼓励前者胜过后者。我在这里谈论高级语言。


1

在我看来,性能是实际问题(或要求)时,应该考虑的因素。不这样做往往会导致微优化,这可能会导致代码更加混乱,只是在这里和那里节省了几微秒的时间,从而导致代码的可维护性和可读性降低。相反,如果需要的话,应该专注于系统的真正瓶颈,并着重那里的性能。


1

重点不是可读性应始终胜过效率。如果从一开始就知道算法需要高效,那么这将是您开发算法的因素之一。

问题是大多数情况下不需要盲目的快速代码。在许多情况下,IO或用户交互导致的延迟要比算法执行引起的延迟多得多。关键是,如果您不知道这是瓶颈,就不要全力以赴,使某些事情变得更有效率。

优化代码的性能通常会使其变得更加复杂,因为它通常涉及以一种巧妙的方式来做事,而不是最直观的方式。更复杂的代码更难维护,其他开发人员也难以采用(这都是必须考虑的成本)。同时,编译器非常擅长优化常见情况。您尝试改善一种常见情况可能意味着编译器不再识别该模式,因此无法帮助您快速编写代码。应该注意的是,这并不意味着编写任何内容而无需担心性能。您不应该做任何明显无效的事情。

重点是不要担心可能会使事情变得更好的小事情。使用事件探查器,可以看到1)您现在拥有的是一个问题,以及2)您将其更改为什么是一项改进。


1

我认为大多数程序员会感到直觉,这是因为在大多数情况下,性能代码是基于比应用程序中任何其他代码更多信息(关于上下文,硬件知识,全局体系结构)的代码。大多数代码只会针对以模块化方式封装在某些抽象中的特定问题(如函数)表达一些解决方案,这意味着将上下文的知识仅限于输入该封装的内容(如函数参数)。

当您为提高性能而写作时,修复所有算法优化后,您会进入需要更多有关上下文知识的细节。这自然会使不那么专注于该任务的程序员不知所措。


1

因为全球变暖的成本(来自由数亿台PC加上大量数据中心设施扩展的额外CPU周期)和(在用户的移动设备上)运行不佳的优化代码所需的平均电池寿命(在大多数情况下很少出现)程序员的表现或同行评价。

这是经济上的负面外部效应,类似于一种被忽略的污染形式。因此,从根本上偏离思考性能的成本/收益比。

硬件设计人员一直在努力为最新的CPU添加省电和时钟缩放功能。程序员应通过不浪费每个可用的CPU时钟周期,让硬件更频繁地利用这些功能。

添加:在远古时代,一台计算机的成本为数百万美元,因此优化CPU时间非常重要。然后,开发和维护代码的成本变得大于计算机的成本,因此与程序员的工作效率相比,优化已不受欢迎。但是,现在,另一成本正变得比计算机成本高,给所有这些数据中心供电和冷却的成本现在正变得高于内部所有处理器的成本。


除了PC是否导致全球变暖的问题外,即使这是真实的:这是一个谬论,提高能效会导致更少的能源需求。从PC上市的第一天就可以看出几乎完全相反。在此之前,成百上千个大型机(每个虚拟机实际上都配备了自己的发电厂)消耗的能源要比今天少得多,如今,一分钟的CPU运算量要比当时少得多,而成本和能源需求却是零头。但是,计算的总能源需求比以前更高。
Ingo

1

我认为很难实现这三个目标。我认为两个可行。例如,我认为在某些情况下可以实现效率和可读性,但是对于微调的代码而言,可维护性可能很难。地球上最高效的代码通常将缺乏可维护性和可读性,对大多数人来说可能是显而易见的,除非您是那种能够理解英特尔使用内联汇编编写的手工SoA矢量化,多线程SIMD代码的人,行业中使用的最先进的算法,仅在2个月前发表了40页的数学论文,以及12种有价值的代码库,用于一种极其复杂的数据结构。

微观效率

我建议可能与流行观点相反的一件事是,最智能的算法代码通常比最微调的直接算法更难维护。我要挑战的想法是,可伸缩性改进带来了微调代码(例如:缓存友好的访问模式,多线程,SIMD等)带来的巨大收益,至少在一个极其复杂的行业工作过数据结构和算法(可视化FX行业),尤其是在网格处理等领域,因为爆炸的影响可能很大,但引入新的算法和数据结构带来的损失却是巨大的,因为它们是品牌之前从未听说过新。而且,我

因此,我认为算法优化始终胜过与内存访问模式相关的优化这一想法一直是我不太同意的事情。当然,如果您使用的是气泡排序,那么任何微优化都无法为您提供帮助...但是在一定程度上,我认为这并非总是那么明确。而且可以说算法优化比微优化更难维护。我会发现,与Dreamwork的OpenVDB代码(用于算法加速流体模拟的最先进方法)相比,英特尔的Embree采用经典而直接的BVH算法并对其进行微调,维护起来要容易得多。因此,至少在我的行业中,我希望看到更多熟悉计算机体系结构的人更多地进行微优化,就像英特尔进军现场那样,而不是提出成千上万的新算法和数据结构。通过有效的微优化,人们可能会发现越来越少的发明新算法的理由。

我在旧版代码库中工作,在此之前,几乎每个单个用户操作背后都有自己独特的数据结构和算法(总共有数百个奇异的数据结构)。而且它们大多数具有非常偏斜的性能特征,适用范围非常狭窄。如果该系统能够围绕数十个更广泛应用的数据结构旋转,那将容易得多,而且我认为,如果对它们进行更好的微优化,情况可能会如此。我之所以提到这种情况,是因为微优化可以在这种情况下极大地改善可维护性,如果这意味着数百种微悲观的数据结构之间的差异,这些数据结构甚至不能安全地用于严格的只读目的,这涉及到遗漏的缓存和对与

功能语言

同时,我遇到过的一些最可维护的代码相当有效,但由于用功能语言编写,因此极难阅读。在我看来,一般而言,可读性和超级可维护性是相互矛盾的想法。

很难一次性使代码具有可读性,可维护性和高效性。通常,您必须在这三个(如果不是两个)之一中进行一些折衷,例如,为了提高可维护性而降低可读性,或者为了提高效率而降低可维护性。当您寻求其他两个方面时,通常会损害可维护性。

可读性与可维护性

现在,正如我所说,我认为可读性和可维护性不是和谐的概念。毕竟,对于我们大多数人来说,最易读的代码非常直观地映射到人类的思维模式,而人类的思维模式天生就容易出错:“ 如果发生,请执行。如果发生,请执行。否则,请这样做。 ,我忘记了一些事情!如果这些系统相互交互,则应该发生这种情况,以便该系统可以执行此操作...等等,触发该事件时该系统如何处理?“我忘了确切的报价,但有人曾经说过,如果罗马像软件那样建造,那么只需将鸟降落在墙上才能将其推倒。大多数软件就是这种情况。它比我们经常关心的要脆弱得多。想想看,这里和那里的几行看似无害的代码可能会使它停顿下来,以至于让我们重新考虑整个设计,而旨在尽可能可读的高级语言也不是这种人为设计错误的例外。 。

纯粹的功能语言几乎可以做到这一点(几乎无法克服,但是比大多数语言更接近)。这部分是因为它们没有直观地映射到人类思想。它们不可读。它们迫使我们产生思维模式,这使我们不得不使用尽可能少的知识并在不引起任何副作用的情况下,以尽可能少的特殊情况解决问题。它们具有极强的正交性,它们使代码可以经常更改和更改而不会出现突如其来的史诗般的变化,以至于我们不得不在绘图板上重新考虑设计,甚至到改变我们对整体设计的看法,而不必重写所有内容。似乎没有比这更容易维护了……但是代码仍然很难阅读,


1
“微效率”有点像在说“没有O(1)内存访问之类的东西”
Caleth

0

一个问题是,有限的开发人员时间意味着您寻求优化的一切都会浪费时间在其他问题上。

在Meyer的《代码完成》中对此进行了相当不错的实验。要求不同的开发人员小组针对速度,内存使用,可读性,健壮性等进行优化。发现他们的项目在要求进行优化的方面得分很高,但在所有其他质量方面得分都较低。


很明显,你可以投入更多的时间,但最终你开始质疑为什么开发商会抽出时间对Emacs编程,以表达对孩子的爱,在这一点上你基本上谢尔顿从大爆炸理论
deworde

0

因为经验丰富的程序员已经知道这是真的。

我们使用的是精简且没有性能问题的代码。

我们已经处理了很多代码,以解决性能问题非常复杂。

我想到的一个直接示例是,我的上一个项目包括8,192个手动分片的SQL表。由于性能问题而需要这样做。从1个表中进行选择的设置比从中选择和维护8,192个分片要简单得多。


0

还有一些著名的高度优化的代码片段,将使大多数人不知所措,这支持了高度优化的代码难以阅读和理解的情况。

这是我认为最著名的。取自Quake III Arena,并归因于John Carmak,尽管我认为此功能已经进行了多次迭代,但它并不是他最初创建的(Wikipedia很棒吗?)。

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.