我想了解优秀的代码优化方法和方法。
答案 0 :(得分:39)
对于您建议的各种优化,您应该编写代码以便清晰,而不是优化它们,直到您有证据证明它们是瓶颈。
尝试像这样的微优化的一个危险是,你可能会使事情更慢,因为编译器比你在很多时候更聪明。
采取“优化”:
const int windowPosX = (screenWidth * 0.5) - (windowWidth * 0.5);
世界上没有严肃的编译器不知道除以2的最快方法是向右移动。乘以浮点数0.5实际上更昂贵,因为它需要转换为浮点数和返回值,并进行两次乘法(比移位更昂贵)。
但是不要相信我的话。看看编译器实际上做了什么。 gcc 4.3.3在32位Ubuntu(-O3,-msse3,-fomit-frame-pointer)上编译:
int posx(unsigned int screen_width, unsigned int window_width) {
return (screen_width / 2) - (window_width / 2);
}
到此:
00000040 <posx>:
40: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4]
44: 8b 54 24 08 mov edx,DWORD PTR [esp+0x8]
48: d1 e8 shr eax,1
4a: d1 ea shr edx,1
4c: 29 d0 sub eax,edx
4e: c3
两个班次(使用立即操作数)和减法。非常便宜。另一方面,它汇编了这个:
int posx(unsigned int screen_width, unsigned int window_width) {
return (screen_width * 0.5) - (window_width * 0.5);
}
到此:
00000000 <posx>:
0: 83 ec 04 sub esp,0x4
3: 31 d2 xor edx,edx
5: 8b 44 24 08 mov eax,DWORD PTR [esp+0x8]
9: 52 push edx
a: 31 d2 xor edx,edx
c: 50 push eax
d: df 2c 24 fild QWORD PTR [esp]
10: 83 c4 08 add esp,0x8
13: d8 0d 00 00 00 00 fmul DWORD PTR ds:0x0
15: R_386_32 .rodata.cst4
19: 8b 44 24 0c mov eax,DWORD PTR [esp+0xc]
1d: 52 push edx
1e: 50 push eax
1f: df 2c 24 fild QWORD PTR [esp]
22: d8 0d 04 00 00 00 fmul DWORD PTR ds:0x4
24: R_386_32 .rodata.cst4
28: de c1 faddp st(1),st
2a: db 4c 24 08 fisttp DWORD PTR [esp+0x8]
2e: 8b 44 24 08 mov eax,DWORD PTR [esp+0x8]
32: 83 c4 0c add esp,0xc
35: c3 ret
您所看到的是转换为浮点数,乘以数据段中的值(可能在缓存中,也可能不在缓存中),并转换回整数。
当你想要执行像这样的微优化时,请想一想这个例子。它不仅为时过早,而且可能根本没有帮助(在这种情况下,它会受到严重伤害!)
说真的:不要这样做。我认为一个黄金法则永远不会做这样的优化,除非你像我在这里那样定期检查编译器的输出。
答案 1 :(得分:27)
答案 2 :(得分:17)
编辑这个答案最初出现在另一个问题(已经合并)中,其中OP列出了一些他认为必须有效的优化技术。所有这些都严重依赖于假设(例如x << 1
总是比x * 2
更快)。下面我试图指出这种假设的危险。
由于你的所有观点都可能是错误的,这表明存在这种过早和微不足道的优化的危险。把这样的决定留给编译器,除非你知道非常你正在做什么,这很重要。
否则它 - 只是 - 不重要。
更重要的是(并且根本不是过早)是一般程序结构中的优化。例如,一遍又一遍地重新生成相同的大批量数据可能非常糟糕,因为在许多地方需要它。相反,必须将一些想法放入设计中以允许共享此数据,从而仅计算一次。
了解您正在使用的域名也非常重要。我来自生物信息学背景,并在C ++中进行了大量的核心算法工作。我经常处理大量数据。目前,我正在用Java创建一个应用程序,每次创建一个容器副本时我都会 cringe ,因为我习惯于避免这样的操作。但是对于我看中的Java GUI来说,这些操作完全是微不足道的,对用户来说并不是那么明显。哦,好吧,我必须克服自己。
顺便说一下:
声明它们时初始化常量(如果可以)...
好吧,在许多语言(例如C ++)中,常量(即标记为const
的标识符)有在定义时被初始化,因此您实际上没有选择。但是, 是遵循该规则的好主意,不仅适用于常数,而且适用于一般情况。不过,原因不一定是表现。它只是更清晰的代码,因为它清楚地将每个标识符绑定到目的而不是让它飞来飞去。
答案 3 :(得分:14)
优化俱乐部的规则:
http://xoa.petdance.com/Rules_of_Optimization_Club
规则#3是让大多数人绊倒的规则。如果您的程序等待磁盘写入或网络传输,那么计算的速度并不重要。
规则#6和#7:始终进行测试。如果你正在优化,你就是重构,如果没有一个可靠的测试套件,你不想重构。
答案 4 :(得分:8)
记住“成本”的东西总是好的。一些C#示例:
字符串连接总是创建一个新字符串,因为字符串是不可变的。因此,对于重复连接,StringBuilder
更有效。
重复或大量内存分配通常是您应该注意的事项。
抛出例外是非常昂贵的。这是异常仅应用于特殊情况的原因之一。
除此之外的大多数事情都是过早优化。如果速度很重要,请使用分析器。
关于“优化”:
我怀疑浮点运算(* 0.5
)比整数除法(/ 2
)更快。
如果你需要一个大小为300的数组,你应该初始化一个大小为300的数组。对于2的幂,没有什么“神奇”可以使256大小的数组更有效。
“代码中需要2次调用”是错误的。
答案 5 :(得分:5)
确保您有明确定义的效果目标和测试,以衡量这些目标,以便您可以快速找出是否有问题。
从设计角度考虑性能而不是从编码角度考虑 - 优化性能低下的设计只会导致代码速度变慢
当您遇到性能问题时,请使用分析器等工具来识别问题 - 您可以猜出瓶颈在哪里,通常猜错了。
在开发早期修复性能问题而不是将其关闭 - 随着时间的推移和功能使其成为产品修复性能问题只会变得越来越困难。
答案 6 :(得分:4)
我们有一个分包商为非平凡程序写了一些非常重要的代码。显然他们总是使用大量的数据。所以..
答案 7 :(得分:4)
我使用的头号规则是DRY(不要重复自己)。我发现这个规则很好地突出了可以修复的问题区域而不会损害程序的清晰度。它还可以在您发现瓶颈后更轻松地修复瓶颈。
答案 8 :(得分:4)
早期优化并不总是为时过早 - 只有在没有正当理由的情况下损害其他兴趣(可读性,维护,实施时间,代码大小......)时才会出现问题。
在stackoverflow上,早期优化是新的goto
,不要因此而气馁。 任何早期出错的决定都很难在以后解决。优化是特殊的,因为经验表明它通常可以在本地修复,而糟糕的代码需要大规模的更改。
对不起,呐喊,现在你的实际问题:
了解您的环境!
这包括所有低级细节 - 例如非线性内存访问,编译器可以优化的东西等等。这个诀窍并不是很烦恼,你不应该担心,只要注意。
衡量措施措施!
实际优化尝试的结果通常是令人惊讶的,特别是如果你改变看似不相关的因素。这也是对表现形成放松态度的最佳方式 - 大部分时间它都无关紧要。
在考虑实施细节之前先考虑算法。
大多数低级优化给你一个因子1.1,一个不同的算法可以给你一个因子10.一个好的(!)缓存策略可以给你一个因子100.弄清楚你真的不需要打电话给你Warp 10。
这通常会让我思考如何组织数据:什么是潜在的瓶颈或可扩展性问题?
答案 9 :(得分:4)
答案 10 :(得分:3)
我甚至会说积分类型,而不是乘以0.5,你可以将它们向右移一位(不要忘记有符号/无符号移位)。
我很确定,至少在C#的情况下,编译器会优化很多。
例如:
Guid x = Guid.NewGuid();
以及
Guid x; x = Guid.NewGuid();
都转换为以下CIL:
call System.Guid.NewGuid
stloc.0
预先计算诸如(4 + 5)之类的常量表达式以及“Hello”+“World”之类的字符串连接。
我主要关注可读和可维护的代码。除了特殊的边缘情况之外,这种微优化不太可能产生很大的不同。
在我的情况下,(有时候感知到的)表现的最大收获是如下所示:
当然,这只是我从C#的经验,但输入/输出操作是常见的瓶颈。
答案 11 :(得分:3)
我想说,你在旅途中可以做的一点点优化正是那些没有多大意义的。如果要进行优化,请先编写代码,然后再进行配置文件,然后然后优化需要花费太多时间的部分。即便如此,通常算法需要优化,而不是实际代码。
答案 12 :(得分:2)
您可以随时进行优化,例如在C ++(C++ Optimization Strategies and Techniques)中通过const引用传递大对象。第一个建议(“不要除以2”)可能属于“炸弹战略” - 假设某些操作比其他操作更快。
我认为一个过早(?)优化是将昂贵对象的声明移出循环。例如,如果循环每次迭代需要一个新的向量,我经常这样做:
std::vector<T> vec;
while (x) {
vec.clear(); //instead of declaring the vector here
...
}
我曾经在一个成员函数static本地使用了一个向量作为优化(为了减少内存分配 - 这个函数经常调用),但是当我决定使用多个这些对象在多个线程中。
答案 13 :(得分:2)
我不知道你是否会买它,但是early optimization is not the root of all evil
答案 14 :(得分:2)
这需要经验,我担心。当您构思问题的解决方案时,您可能会考虑类层次结构,或者您可能会考虑哪些信息进入,哪些信息出来,需要多长时间才能持续存在。我推荐后者。
无论如何,人们所说的大多是好建议 - 保持干净简洁,摆脱性能问题,因为他们会进来。
我离开公司的地方我发现度量对定位性能问题非常有帮助compared to this method.
但无论你使用什么方法,希望经验会教会你在开发软件时不做什么。我一直在解决性能问题很长一段时间,而现在,最受欢迎的一个性能杀手是疾驰的普遍性。没有人喜欢听到他们最喜欢的信仰受到质疑,但是一次又一次,特别是在大软件中,什么是杀戮性能使用火箭筒拍苍蝇。
奇怪的是,经常给出这种过度设计的原因是猜猜是什么? 性能。
在你可能学会编程的任何场所,你很可能已经学到了所有关于复杂数据结构,抽象类层次结构,棘手的编译器优化技术等学术知识 - 所有最新的有趣且有趣的东西,以及我和任何人一样喜欢。他们没有教你的是什么时候使用它,这几乎不是。
所以我建议你做的是:获得经验。这是最好的老师。
答案 15 :(得分:1)
您可能希望了解哪些编译器可以优化掉 - 许多编译器可以优化尾递归之类的东西,而大多数其他次要优化通过比较是微不足道的。我的方法是写东西,使它们尽可能可读/可管理,然后,如果我需要,看看生成的汇编代码是否需要某种优化。这样就不需要花时间来优化不需要优化的事情了。
答案 16 :(得分:1)
个人资料,个人资料,个人资料。如果可以,请使用valgrind
(以及kcachegrind
可视化工具),否则请使用尊敬的gprof
。
我的最佳表现点击:
其他一切都在喧闹中。
答案 17 :(得分:0)
如果我正在考虑性能,我如何避免过早优化。
优化的重要性,以及它如何影响可读性和可维护性?
如何在代码中找到瓶颈?
在脑海中完成其执行流程。了解所采取措施的成本。随着时间的推移,在此上下文中评估现有实现。您还可以使用调试器来完成另一个视角。考虑并实际尝试替代解决方案。
与流行的方法相矛盾,在编写程序后或者一旦出现问题时进行剖析是天真的 - 这就像在准备不足的餐中添加酱汁来掩盖不愉快的事情。它也可以比作总是要求解决方案而不是实际确定原因(并在此过程中学习原因)的人之间的差异。如果你已经实现了一个程序,然后花时间进行分析,然后做了简单的修复,并使其在此过程中加快了20%...如果性能和资源利用很重要,那通常不是一个“好”的实现,因为所有的小问题都有积累将在分析器的输出中产生高噪声。好的实现比偶然构造函数的实现好5,10或甚至25倍并不罕见。
我如何确保随着时间的推移我的程序不会变慢?
这取决于很多事情。一种方法涉及持续集成和实际执行程序。然而,即使在严格控制的环境中,这也可能是移动目标。通过专注于第一次创建良好的实现来最小化更改... :)
要避免哪些常见的性能错误(例如;我知道在try {} catch {}块的catch部分内返回某些语言是不好的
我将补充:多线程经常被用于错误的原因。它常用于加剧糟糕的实施,而不是找出现有设计中存在的问题/弱点。
答案 18 :(得分:0)
code optimization methods (language-independent) on github(ps i&#39;作者)
的摘要<强>概要强>
答案 19 :(得分:0)
上面已经提到了,但值得谈论更多:衡量!你必须进行衡量,以确保你正在优化正确的事情。你必须衡量,以了解你是否已经改善,或改善了足够多,以及改善了多少。记录您的测量结果!
此外,通常您会将例程视为占据总时间的75%。值得花些时间来分析更精细的数据......通常你会发现在中的大部分时间都只花费在代码的很小一部分上。
答案 20 :(得分:0)
1,2和3有相同的答案:个人资料。获得一个好的分析器,并在您的应用程序上运行它,无论是侵入式还是采样模式。这将显示您的瓶颈在哪里,它们有多严重,并且定期执行此操作将向您显示每周的性能变差。
您还可以简单地将秒表添加到您的应用中,以便它可以告诉您,确切地说,加载文件需要多少秒;你会注意到这个数字是否会变大,特别是如果你记录它。
4是一个很大的问题,从高级算法设计一直到tiny details of a particular CPU's pipeline。那里有很多资源,但总是从高级开始 - 在开始担心整数操作码延迟等之前,将外部循环从O(N ^ 2)转到O(N log N)。