本文概述
性能优化是对代码的最大威胁之一。
你可能在想, 而不是其中的另一个人。我明白。从词源上看, 任何形式的优化显然都是一件好事, 因此, 自然而然地想做到这一点。
不仅仅是让自己成为更好的开发人员而与众不同。不仅是为了避免成为Daily WTF的”丹”, 还因为你认为代码优化是正确的做法。你以工作为荣。
计算机硬件的速度越来越快, 软件的制造变得更容易, 但是, 无论你只是想做些简单的事情, 该死总是比上一个花费更长的时间。你对这种现象(偶然称为Wirth定律)摇了摇头, 并决心逆转这种趋势。
贵族贵, 但是别动。
停下来!
无论你在编程方面多么有经验, 你都有挫败自己目标的最大危险。
为何如此?让我们备份一下。
首先, 什么是代码优化?
通常, 当我们定义它时, 我们假设我们希望代码性能更好。我们说代码优化是在编写或重写代码, 因此程序使用尽可能少的内存或磁盘空间, 最大程度地减少其CPU时间或网络带宽, 或充分利用其他内核。
实际上, 有时我们会默认使用另一个定义:减少代码编写。
但是, 你为此目标而编写的先发制人的坏蛋代码更有可能成为某人的棘手。谁的?下一个不幸的人, 必须理解你的代码, 甚至可能是你自己。像你一样, 一个聪明又有能力的人可以避免自我破坏:尽管你的目标看起来毫无疑问是直觉的, 但要保持自己的目标高尚, 但要重新评估自己的能力。
因此, 代码优化是一个模糊的术语。在此之前, 我们甚至没有考虑其他可以优化代码的方法, 下面将对此进行介绍。
首先, 在我们一起探索杰克逊著名的代码优化规则时, 先听一下圣贤的建议:
- 不要做
- (仅适用于专家!)请不要这样做。
1.不做:引导完美主义
从一个很尴尬的极端例子开始, 那是很久以前的事, 那时候我只是在美妙而又吃饱了SQL的世界中忙得不可开交。问题是, 我接着踩了蛋糕, 不想再吃了, 因为蛋糕已经湿了, 开始闻起来像脚。
我只是在美妙而又吃不完的SQL世界中wet之以鼻。问题是, 我接着踩了蛋糕……
等待。让我回过头来讲这个比喻的汽车残骸。
我当时正在为Intranet应用程序进行研发, 希望有一天能够成为我所工作的小型企业的完全集成的管理系统。它会为他们跟踪所有内容, 并且与当时的系统不同, 它将永远不会丢失其数据, 因为它将得到RDBMS的支持, 而不是其他开发人员使用过的片状本地平面文件。我想从一开始就将所有内容设计得尽可能聪明, 因为我一无所有。我想到这个系统的想法像烟火一样爆炸, 然后我开始设计表格—联系人以及CRM, 会计模块, 库存, 采购, CMS和项目管理的许多上下文变化, 而我很快就会想到这些。
由于…你猜到了优化, 所有这些都因开发和性能而停顿了。
我看到对象(表示为表行)在现实世界中可能具有许多不同的相互关系, 并且我们可以从跟踪这些关系中受益:我们将保留更多信息, 并最终可以在整个地方实现业务分析自动化。将此视为工程问题, 我做了一些事情, 似乎在优化系统的灵活性。
此时, 请务必当心你的脸, 因为如果你的手掌受伤, 我将不承担任何责任。准备?我创建了两个表:relationship, 其中一个表有一个对Relationship_type的外键引用。关系可以引用整个数据库中任何位置的任何两行, 并描述它们之间关系的性质。
天啊。我刚刚优化了灵活性, 真是太可惜了。
实际上太多了。现在, 我遇到了一个新问题:给定的Relationship_type在行的每个给定组合之间自然没有意义。虽然一个人与一家公司有受雇关系可能很有意义, 但是从语义上讲, 这永远不会等同于两个文档之间的关系。
好, 没问题我们将只在两列中添加” relationship_type”, 以指定可以将此关系应用于哪些表。 (如果你猜想我考虑过通过将这两列移到一个新的表中并引用Relationship_type.id来规范化此操作, 那么奖金就指向此处, 以便可以在语义上应用于多于一对表的关系不会重复使用表名。毕竟, 如果我需要更改表名而忘了在所有适用的行中对其进行更新, 那么它可能会创建一个bug!回想起来, 至少有bug可以为居住在我的头骨上的蜘蛛提供食物。)
值得庆幸的是, 在走过这条路之前, 我在线索风暴中被昏迷了。当我醒来时, 我意识到自己已经或多或少地重新实现了RDBMS内部与外键相关的表。通常情况下, 我会大声疾呼地宣布”我很元”, 但不幸的是, 这并不是其中之一。忘了无法扩展-这种设计的可怕结果使我仍然很简单的应用程序成为后端, 该应用程序的DB几乎没有任何测试数据, 几乎无法使用。
让我们备份一秒钟, 然后看一下这里发挥作用的多个指标中的两个。一是灵活性, 这是我所说的目标。在这种情况下, 我的优化(本质上是架构性的)甚至还不成熟:
(我们在我最近发表的文章”如何避免过早优化的诅咒中对此有更多的介绍。”尽管如此, 我的解决方案由于过于灵活而失败了。另一项指标是可扩展性, 我什至没有考虑过, 但是却设法破坏了至少同等程度的附带损害。
是的, “哦。”
对于我来说, 这是一次关于如何使优化完全出错的有力教训。我的完美主义彻底崩溃了:我的聪明才智使我产生了我所做出的最客观, 最不明智的解决方案之一。
优化你的习惯, 而不是你的代码
当你甚至在拥有可用的原型和测试套件以证明其正确性之前就开始倾向于重构时, 请考虑在其他方面可以利用这种冲动。 Sudoku和Mensa很棒, 但是实际上可以直接使你的项目受益的一些东西会更好:
- 安全
- 运行时稳定性
- 清晰度和风格
- 编码效率
- 测试效果
- 剖析
- 你的工具箱/ DE
- 干(不要重复自己)
但是要当心:优化其中任何一项的技巧都会以其他为代价。至少, 这是以时间为代价的。
在这里, 很容易看出编写代码中有多少艺术品。对于上述任何一种情况, 我都可以告诉你有关错误或错误选择的故事。谁在这里进行思考也是上下文的重要组成部分。
例如, 关于DRY:我做过一份工作, 我继承了一个至少包含80%冗余语句的代码库, 因为它的作者显然不知道如何以及何时编写函数。其余20%的代码是令人困惑的自相似。
我受命添加一些功能。其中一种功能需要在要实施的所有代码中重复进行, 并且将来的任何代码都必须仔细复制以利用新功能。
显然, 仅出于我自己的理智(高价值)和任何未来的开发人员的需要而对其进行重构。但是, 由于我是代码库的新手, 因此我首先编写了测试, 以确保我的重构不会引入任何回归。实际上, 他们就是这样做的:我在脚本产生的所有gobbledygook输出中一无所获地发现了两个bug。
最后, 我认为我做得不错。重构后, 我用几行简单的代码实现了被认为是一项困难的功能, 给老板留下了深刻的印象。此外, 该代码的整体性能要高一个数量级。但是不久之后, 同一位老板告诉我我的进度太慢了, 该项目应该已经完成。译文:编码效率是重中之重。
当心:优化任何特定方面都需要付出其他代价。至少, 这是以时间为代价的。
我仍然认为我在那儿学习了正确的课程, 即使当时我的老板没有直接欣赏代码优化。如果没有重构和测试, 我认为实际上需要花费更长的时间-也就是说, 专注于编码速度实际上会挫败它。 (嘿, 那是我们的主题!)
将此与我在我的一个小型项目中所做的一些工作进行对比。在该项目中, 我正在尝试一个新的模板引擎, 并且即使从一开始就尝试养成良好的习惯, 即使尝试新的模板引擎并不是该项目的最终目标。
我注意到我添加的几个块彼此非常相似, 此外, 每个块都需要引用同一变量三次, 所以DRY钟声在我脑海中震荡, 我开始寻找合适的块与此模板引擎一起执行操作的方法。
事实证明, 经过几个小时毫无结果的调试之后, 模板引擎目前无法按照我想象的方式实现。不仅没有完美的DRY解决方案;根本没有任何DRY解决方案!
试图优化我的这一价值, 我完全降低了编码效率和幸福感, 因为这种绕行使我的项目失去了当日的进度。
即使那样, 我还是完全错了吗?有时值得花一些投资, 尤其是在新技术的情况下, 以便尽早了解最佳实践。更少的代码重写和不良习惯可以撤销, 对吗?
不, 我认为即使在寻找一种减少代码重复的方法也是不明智的, 这与我以前的轶事中的态度形成鲜明对比。原因是上下文无所不包:我在一个小型游戏项目中探索了一项新技术, 而不是长期接受。额外的几行和重复不会伤害任何人, 但是失去焦点会伤害我和我的项目。
等等, 所以寻求最佳做法可能是个坏习惯?有时。如果我的主要目标是学习新引擎或进行一般学习, 那将是值得花费的时间:修补, 寻找极限, 通过研究发现无关的功能和陷阱。但是我忘记了这不是我的主要目标, 这使我付出了代价。
就像我说的那样, 这是一门艺术。提醒你, 不要去做, 这门艺术的发展便会受益。至少可以让你考虑在工作中发挥哪些价值, 以及哪些价值对你而言最重要。
那第二条规则呢?我们什么时候才能真正优化?
2.还没做:有人已经做过了
好的, 无论是你还是其他人, 都发现你的体系结构已经设置好, 数据流已经过考虑并记录在案, 现在该进行编码了。
让我们将”不要做”更进一步:甚至都不要编写代码。
这本身可能听起来像是过早的优化, 但这是一个重要的例外。为什么?为避免可怕的NIHS或”此处未发明”综合症—假设你的优先事项包括代码性能并缩短开发时间。如果不是, 如果你的目标完全是面向学习的, 则可以跳过本节。
尽管人们可能会彻底摆脱狂妄, 但我相信诚实的, 谦虚的人(例如你和我)会因为不了解我们所有可用选项而犯下此错误。知道堆栈中每个API和工具的每个选项, 并随着它们的成长和发展掌握它们, 无疑是一项艰巨的工作。但是, 投入这个时间可以使你成为专家, 并使你不再是CodeSOD上的千百万人, 因为他们迷人的日期时间计算器或字符串操纵器所留下的毁灭性痕迹使他们免受诅咒和嘲笑。
检查你的标准库, 检查框架的生态系统, 检查是否已解决你的问题的FOSS
你正在处理的概念很可能具有相当标准的名称和知名名称, 因此快速的Internet搜索将为你节省大量时间。
举例来说, 我最近准备对棋盘游戏的AI策略进行一些分析。我一天早上醒来, 意识到如果我简单地使用我记得的某种组合概念, 就可以更有效地完成我正在计划的分析。目前, 我本人并没有兴趣为这个概念找出算法, 而是已经知道要搜索的正确名称, 因此已经领先。但是, 我发现经过大约50分钟的研究并尝试了一些初步代码之后, 我仍未设法将发现的半成品伪代码转换为正确的实现。 (你是否可以相信那里有一篇博客文章, 其中的作者假设算法输出不正确, 算法的实现不符合假设, 注释者指出了这一点, 然后几年后仍未解决?)那时, 我的早茶参加, 我搜索了[概念名称] [我的编程语言]。 30秒后, 我从GitHub上正确地验证了代码, 然后继续进行我真正想做的事情。只是具体和包括语言, 而不是假设我必须自己实现它, 就意味着一切。
是时候设计数据结构和实现算法了
……再次, 不要打高尔夫。在实际项目中优先考虑正确性和清晰度。
好的, 你已经看过了, 目前还没有解决你的问题的工具链或网络上的免费许可证。你自己推出。
没问题。建议很简单, 按以下顺序进行:
- 设计它, 以便向新手程序员轻松解释。
- 编写符合该设计产生的期望的测试。
- 编写你的代码, 以便新手程序员可以轻松地从中收集设计。
简单, 但也许很难遵循。这是编码习惯和编码气味以及工艺和优雅发挥作用的地方。此时, 你所做的工作显然涉及工程方面, 但是同样, 不要玩代码高尔夫。在实际项目中优先考虑正确性和清晰度。
如果你喜欢视频, 则可以按照以下步骤来或多或少地按照以下步骤操作。对于厌恶视频的内容, 我将总结一下:这是Google求职面试中的算法编码测试。受访者首先以易于交流的方式设计算法。在编写任何代码之前, 有一些工作设计预期的输出示例。然后, 代码自然会跟随。
至于测试本身, 我知道在某些领域, 测试驱动的开发可能会引起争议。我认为部分原因是它可以过分地奉行, 以至于牺牲了开发时间。 (同样, 从一开始就尝试过多地优化一个变量来使自己步履蹒跚。)即使是肯特·贝克(Kent Beck)也没有将TDD推到极致, 他发明了极限编程并在TDD上写了这本书。因此, 从简单的事情开始, 以确保你的输出正确。毕竟, 无论如何, 你将在编码后手动进行操作, 对吗? (我很抱歉, 如果你是这样的摇滚明星程序员, 你甚至在第一次编写代码后都没有运行代码。在这种情况下, 也许你会考虑让代码的未来维护者进行测试, 以便你知道他们不会破坏你的出色实现。)因此, 你无需进行手动的视觉对比, 而只需进行适当的测试, 就可以让计算机为你完成这项工作。
在实施算法和数据结构的相当机械的过程中, 避免进行逐行优化, 甚至不要考虑使用自定义的较低级语言extern(如果使用C进行编码, 则汇编, 如果使用C, 则使用C此时在Perl等中进行编码)。原因很简单:如果你的算法被完全替换, 并且直到该过程的后期才发现是否需要这样做, 那么你的低级优化工作最终将不会起作用。
ECMAScript示例
最近, 在出色的社区代码审查站点exercism.io上, 我发现了一个练习, 该练习明确建议尝试针对重复数据删除或清晰度进行优化。我对重复数据删除进行了优化, 目的只是说明如果你采用DRY(如上所述, 这是另外一种有益的编码思路)会带来多大的荒谬。这是我的代码:
const zeroPhrase = "No more";
const wallPhrase = " on the wall";
const standardizeNumber = number => {
if (number === 0) { return zeroPhrase; }
return '' + number;
}
const bottlePhrase = number => {
const possibleS = (number === 1) ? '' : 's';
return standardizeNumber(number) + " bottle" + possibleS + " of beer";
}
export default class Beer {
static verse(number) {
const nextNumber = (number === 0) ? 99 : (number - 1);
const thisBottlePhrase = bottlePhrase(number);
const nextBottlePhrase = bottlePhrase(nextNumber);
let phrase = thisBottlePhrase + wallPhrase + ", " + thisBottlePhrase.toLowerCase() + ".\n";
if (number === 0) {
phrase += "Go to the store and buy some more";
} else {
const bottleReference = (number === 1) ? "it" : "one";
phrase += "Take " + bottleReference + " down and pass it around";
}
return phrase + ", " + nextBottlePhrase.toLowerCase() + wallPhrase + ".\n";
}
static sing(start = 99, end = 0) {
return Array.from(Array(start - end + 1).keys()).map(offset => {
return this.verse(start - offset);
}).join('\n');
}
}
几乎没有任何重复的字符串!通过这种方式编写, 我为啤酒歌(但仅针对啤酒歌)手动实现了一种文本压缩形式。好处是什么?好吧, 假设你想唱歌的是从罐子而不是瓶子里喝啤酒。我可以通过将单个瓶子实例更改为罐来实现。
真好!
…对?
不, 因为所有测试都失败了。好的, 这很容易解决:我们将进行搜索并替换单元测试规范中的瓶子。首先, 这与对代码本身进行操作一样容易, 并且具有无意间破坏代码的风险。
同时, 我的变量之后会被奇怪地命名, 诸如bottlePhrase之类的东西根本与瓶子无关。避免这种情况的唯一方法是准确预测将进行的更改类型, 并使用更通用的术语(例如容器或容器)代替我的变量名中的bottle。
以这种方式证明未来发展的智慧值得怀疑。你根本想改变什么的几率是多少?如果这样做的话, 你所做的更改会很方便吗?在bottlePhrase示例中, 如果要本地化为具有两种以上复数形式的语言怎么办?是的, 重构时间很长, 之后的代码看起来可能更糟。
但是, 当你的需求确实发生变化时, 你不仅在尝试预期它们, 那么也许该是重构的时候了。或者, 也许你仍然可以推迟:实际上, 你将添加多少种船只类型或位置?无论如何, 当你需要在重复数据删除与清晰度之间取得平衡时, 值得观看卡特里娜·欧文(Katrina Owen)的演示。
回到我自己的丑陋示例:不用说, 重复数据删除的好处在这里甚至还没有得到实现。同时, 费用是多少?
除了首先需要花费较长的时间编写之外, 现在, 读取, 调试和维护它的琐事要少得多。想象一下可读性级别, 允许适度的复制。例如, 阐明四个经节的每一个。
但是我们还没有优化!
现在, 你的算法已实现, 并且你已经证明其输出是正确的, 恭喜!你有基线!
最后, 是时候……进行优化了, 对吧?不, 仍然不做。现在该确定你的基准并做一个不错的基准。为你的期望设定一个阈值, 并将其保留在测试套件中。然后, 如果某件事突然使该代码变慢(即使它仍然可以工作), 你将在代码发布之前就知道了。
仍然要坚持优化, 直到你实现了全部相关的用户体验。在那之前, 你可能会针对完全不需要的代码部分。
如果尚未完成应用程序(或组件), 请随时进行设置, 以设置所有算法基准。
一旦完成, 这是创建和测试涵盖系统最常见实际使用场景的端到端测试的绝佳时机。
也许你会发现一切都很好。
或者, 也许你已经确定, 在现实生活中, 某些东西太慢或占用了太多内存。
好, 现在你可以优化
只有一种客观的方法。是时候推出火焰图和其他轮廓分析工具了。有经验的工程师可能比新手更经常猜不到, 但这不是重点:要确定的唯一方法就是剖析。在优化代码以提高性能的过程中, 这始终是第一件事。
你可以在给定的端到端测试期间进行分析, 以了解真正产生最大影响的因素。 (随后, 在部署之后, 监视使用模式是掌握未来系统中与测量最相关的方面的好方法。)
请注意, 你并不是在尝试使用探查器的全部功能, 而是在寻找功能级别的探查, 而不是语句级别的探查, 通常是因为这时你的目标只是找出瓶颈所在的算法。 。
既然你已经使用概要分析来确定系统的瓶颈, 那么现在你就可以尝试进行优化, 并确信优化是值得做的。借助你在此过程中所做的那些基准测试, 你还可以证明你的尝试有效(或无效)。
整体技术
首先, 请记住保持尽可能长的时间:
你知道吗?最终的通用优化技巧, 适用于所有情况:-减少消耗-更新较少-Lars Doucet(@larsiusprime)2017年3月30日
在整体算法级别上, 一种技术是强度降低。但是, 在减少公式的循环的情况下, 请注意不要留下注释。不是每个人都知道或记得每个组合公式。另外, 在使用数学时要格外小心:有时, 你认为可能导致强度降低的并不是最终结果。例如, 假设x *(y + z)在算法上有明确的含义。如果出于某种原因, 你的大脑已经过训练, 可以自动取消类似术语的分组, 那么你可能会倾向于将其重写为x * y + x * z。一方面, 这在读者和已经存在的清晰算法含义之间设置了障碍。 (更糟糕的是, 由于需要额外的乘法运算, 它实际上实际上效率较低。这就像循环展开只是使裤子松了结。)在任何情况下, 快速记录一下你的意图将大有帮助, 甚至可以帮助你了解自己的想法。提交错误之前, 请先确认自己的错误。
无论你是使用公式, 还是只是将基于循环的算法替换为另一种基于循环的算法, 你都可以测量差异。
但是也许你只需更改数据结构即可获得更好的性能。了解你需要对所使用的结构以及任何其他选择进行的各种操作之间的性能差异。也许哈希在你的上下文中工作时会显得有些混乱, 但是在数组上值得花费大量的搜索时间吗?这些是你要权衡的折衷类型。
你可能会注意到, 这归结为你在调用便捷函数时知道正在代表你执行哪些算法。最后, 这实际上与降低强度相同。而且, 了解供应商的库在后台进行的操作不仅对性能至关重要, 而且对于避免意外错误也至关重要。
微观优化
好的, 你的系统功能已经完成, 但是从UX的角度来看, 性能可能会进一步微调。假设你已尽一切努力, 现在是时候考虑一下我们一直以来一直在避免的优化。考虑一下, 因为此优化级别仍是权衡清晰度和可维护性的折衷方案。但是你已经决定了, 现在, 既然你处于整个系统的上下文中, 那么它就很重要了, 因此继续进行语句级概要分析。
就像你使用的库一样, 为了你的利益, 在编译器或解释器级别投入了无数的工程时间。 (毕竟, 编译器优化和代码生成本身都是巨大的主题)。甚至在处理器级别上也是如此。尝试在不了解最低级别情况的情况下优化代码, 就像认为四轮驱动意味着你的车辆也可以更轻松地停车一样。
除此以外, 很难提供良好的一般性建议, 因为这实际上取决于你的技术堆栈和分析器指向的内容。但是, 由于你正在衡量, 因此如果解决方案不能从问题环境中自然而直观地向你展示, 那么你已经处于寻求帮助的绝佳位置。 (睡眠和花在思考其他事情上的时间也会有所帮助。)
此时, 根据上下文和扩展要求, Jeff Atwood可能会建议仅添加硬件, 这可能比开发人员的时间便宜。
也许你不走那条路。在这种情况下, 可能有助于探索各种类别的代码优化技术:
- 快取
- 位黑客和特定于64位环境的黑客
- 循环优化
- 内存层次优化
进一步来说:
- C和C ++中的代码优化技巧
- Java代码优化技巧
- 在.NET中优化CPU使用率
- ASP.NET Web场缓存
- SQL数据库调整或特别调整Microsoft SQL Server
- 扩展Scala的游戏!构架
- 先进的WordPress性能优化
- 使用JavaScript原型和作用域链进行代码优化
- 优化React性能
- iOS动画效率
- Android性能提示
无论如何, 我还有其他一些不适合你的地方:
不要将变量用于多个不同的目的。在可维护性方面, 这就像在没有油的情况下驾驶汽车。只有在最极端的嵌入式情况下, 这才有意义, 即使在那些情况下, 我也会认为它不再可行。这是编译器要组织的工作。自己做, 然后移动一行代码, 就引入了一个错误。节省内存的幻想对你来说值得吗?
在不知道为什么的情况下不要使用宏和内联函数。是的, 函数调用开销是成本。但是, 避免使用它通常会使你的代码难以调试, 有时甚至会使它变慢。偶尔因为一个好主意而在各处使用此技术就是一个金锤的例子。
不要手动展开循环。同样, 这种形式的循环优化几乎总是通过诸如编译之类的自动化过程来更好地进行优化, 而不是牺牲代码的可读性。
最后两个代码优化示例中具有讽刺意味的是, 它们实际上可能是反性能的。当然, 由于你正在进行基准测试, 因此可以针对你的特定代码证明或不证明这一点。但是, 即使你看到了性能上的改进, 也要回到技术方面, 看看在可读性和可维护性方面取得的收益是否值得。
属于你:优化优化
尝试优化性能可能是有益的。但是, 它常常过早地完成, 带有许多不良副作用, 最具有讽刺意味的是, 它导致性能下降。希望你对优化的艺术和科学, 以及最重要的是, 其正确的环境有更多的了解。
我很高兴这能帮助我们从一开始就摆脱编写完美代码的想法, 而改写正确的代码。我们必须记住从上至下进行优化, 证明瓶颈所在, 并在修复之前和之后进行测量。这是优化优化的最优策略。祝你好运。
评论前必须登录!
注册