我可以列出一份常见的 C++ 优化实践吗?
我所说的优化是指您必须修改源代码才能更快地运行程序,而不是更改编译器设置。
我将回应其他人所说的:更好的算法将在性能提升方面获胜。
也就是说,我从事图像处理工作,作为一个问题域,它可能更具粘性。例如,很多年前我有一段代码看起来像这样:
void FlipBuffer(unsigned char *start, unsigned char *end)
{
unsigned char temp;
while (start <= end) {
temp = _bitRev[*start];
*start++ = _bitRev[*end];
*end-- = temp;
}
}
它将 1 位帧缓冲区旋转 180 度。_bitRev 是一个 256 字节的反转位表。这段代码尽可能地紧凑。它在 8MHz 68K 激光打印机控制器上运行,打印一张合法大小的纸大约需要 2.5 秒。为了省去你的细节,客户不能忍受2.5秒。解决方案是与此相同的算法。不同之处在于
所以 5x:没有算法改变。
关键是您还需要了解您的问题域以及瓶颈的含义。在图像处理中,算法仍然是王道,但是如果您的循环正在做额外的工作,那么将该工作乘以几百万,这就是您付出的代价。
编写更好程序的两种方法:
充分利用语言
分析您的应用程序
没有太多语言特定的优化可以做 - 它仅限于使用语言结构(从#1中学习)。主要的好处来自上面的#2。
不要忘记一些事情:
-“我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源。” (c) Donald Knuth
- 如果我们优化算法而不是代码,我们可以获得更多。- 我们将仅优化现有
代码
的慢速部分,这些部分将被分析器或其他特殊工具检测到。
Agner Fog 在分析几个编译器关于 C++ 结构的输出方面做得很好。你可以在这里找到他的作品:http ://www.agner.org/optimize/ 。
英特尔也提供了一个很棒的文档——“英特尔® 64 和 IA-32 架构优化参考手册”,您可以在http://www.intel.com/products/processor/manuals/index.htm找到该文档。虽然它主要针对 IA-32 架构,但它包含可应用于大多数平台的一般建议。显然,它和 Agner Fog 的指南确实有一点交叉。
正如其他答案中所提到的,在分析和算法选择之后,微优化显然是您想要使程序更快的最后一步。
您可能对此感兴趣:优化 C++ Wikibook
我没有想到一个网站,但是 Sutter 的“Exceptional C++”一书对于 C/C++ 开发来说是极好的。我强烈建议每位 C++ 程序员阅读这本书,因为它不仅对优化而且对语言的智能使用提供了深刻的见解,因此您将编写出真正出色的程序。
在其他工程学科中,为系统的组件分配预算是很常见的。例如,垂直起降飞机的发动机设计为提供一定的升力,因此重量必须在限制范围内。在较高的水平上,飞机的每个部分都被赋予了它应该满足的一部分重量预算。
之所以这样做是自上而下,而不是等到它太臃肿而无法从甲板上下来,然后称重每个零件并从最重的部分锉掉一点,部分原因是更换制造组件的成本。但很大一部分原因是,如果你创建的系统到处都超出预算,你就不能只在一个地方修复它。
经典的软件示例是SGI Indy Irix 5.1,这也是图形密集型用户现在拥有 Mac 和 Windows 机器而不是 SGI 机器的部分原因。
“5.1 的表现最可怕的是没人知道它到底去了哪里。如果你开始四处打听,你会得到很多指责和理论,但事实却很少。在 5 月的报告中,我提出了一个“5% 理论” ,其中指出我们添加的每个小东西(Motif、国际化、拖放、DSO、多种字体等)大约花费机器的 5%。在其中 15 或 20 个之后,大部分性能都消失了。”
经常在讨论性能时,据说5%微不足道,建议是等到出现问题后再寻找单个瓶颈。对于大型系统,等到出现问题可能只会让您失去主要业务。
++p 通常比 p++ 快,而 --p 比 p-- 快,尤其是对于具有重载前缀和后缀递增和递减运算符的类型的对象,因为前缀形式只是递增或递减某些内容并返回新值,而后缀形式增加或减少某些东西,但必须将旧值保留在某个地方才能返回它。也就是说,而不是(在此处将 int 替换为您最喜欢的类)
for ( int i ( 0); i < x; i++)
总是写
for ( int i ( 0); i < x; ++i)
您要求提供包含优化智慧的站点/资源。
已经推荐了一些好的。
我可能会补充一点,他们几乎都会说,如果不是定位性能问题的唯一方法,分析是最好的。
我不确定这种民间智慧的起源或它的合理性,但有更好的方法。
添加:
确实,“错误的算法”会降低性能,但这肯定不是唯一的方法。
我做了很多性能调整。在大型软件上,通常会影响性能的是太多的数据结构和太多的抽象层。
对抽象对象的看似无辜的单行方法调用会诱使您忘记该调用可能会花费您什么。将这种趋势乘以几个抽象层,你会发现,例如,当带有索引的简单数组已经足够(并且同样可维护)但不太“合适”时,将所有时间都花在分配和收集迭代器和集合类之类的东西上”。
这就是“常识”的问题。它往往与智慧完全相反。
大多数优化与语言无关。了解您的代码,了解您正在运行的硬件,您可以进行大多数低级优化。
了解您的问题域和合适的算法,您可以进行任何高级优化。
我能想到的唯一特定于 C++ 的优化建议是“了解您的代码的含义”。了解 C++ 何时复制临时对象,了解何时调用了哪些构造函数和析构函数。
并且更喜欢函子而不是函数指针,因为前者可以由编译器内联。一般来说,尽可能多地转移到编译时而不是运行时。使用模板来完成繁重的工作。
当然,在您分析并发现 1) 优化是必要的,以及 2) 需要优化什么之前,不要尝试优化。
编辑:一条评论询问了内联的仿函数与函数指针。这是解释:
函数通常是单独编译的。那么编译器对以函数指针 FP 作为参数的函数 F 了解多少呢?没什么,它必须查找调用 F 的位置,也许在那里它可以找到关于 FP 指向哪个函数的明确线索。如果它可以确定当从这里调用时,FP 将始终指向函数 G,那么是的,它可以为这个特定的调用站点制作一个内联版本的 F,其中内联 G。但它不能简单地内联 G 而不内联 F,因为 F 也可能从其他地方调用,在那里它被传递一个不同的函数指针。即便如此,它也需要一些昂贵的全局优化来确定任何东西都可以内联。
想象一下,你传递了一个这样的函子:
struct Ftor {
void operator()() { ... }
};
所以函数 F 看起来像这样:
void F(const FTor& ft) {
...
ft();
...
}
现在编译器知道调用了哪个函数:函数中的第 2 行调用 Ftor::operator() 。因此可以轻松地内联调用。
当然,在实践中,您通常会对其进行模板化,因此可以使用任何函子类型调用该函数:
template <typename F>
void F(const F& ft) {
...
ft();
...
}
抱歉,我没有任何参考资料可供您参考,但我确实有另一个轶事要补充。
我使用 Microsoft 的 CString 对象作为键生成了一个相当大的 std::map。性能是不可接受的。由于我的所有字符串的长度都相同,因此我围绕老式的固定大小的字符数组创建了一个类包装器,以模拟 CString 的接口。不幸的是,我不记得确切的加速,但它很重要,并且由此产生的性能绰绰有余。
有时您需要了解一些您所依赖的库结构。
模板元编程可用于从动态多态转移到编译时多态,在此过程中生成极其优化的代码。Alexandrescu 的Modern C++ Design是一本深入介绍 TMP 的好书。并非每一页都与优化有关,但它是程序设计中经常出现的考虑因素。
与许多人所说的相反,您可以进行许多特定于语言的优化。这是 Wikibooks 上的极好资源。在设计代码时请记住这一点,然后是配置文件、配置文件、配置文件。
可以得到的最佳优化是重新审视设计,并在分析应用程序的性能相关部分/算法之后。这通常不是特定于语言的。
我的意思是(只是一个想法)如果您通过选择稍微更好的算法(或集合/容器类)可以获得 30% 的性能提升,那么您可以从 C++ 相关的重构中获得的提升最多为 2%。设计改进可以为您带来 30% 以上的收益。
如果您有一个具体的应用程序,最好的策略是测量和分析该应用程序。分析通常可以最直接地了解哪些部分与性能相关。
这里有几个捕获所有优化路径。
优化问题没有一种方法……它们总是根据硬件/软件/系统考虑因素进行手动调整。
假设你有最好的算法:
此处看到的示例:在 C 中交换值的最快方法是什么?
一般提示:
http://www.ceet.niu.edu/faculty/kuo/exp/exp6/figuree6-1.jpg:
http://www.asc.edu/seminars/optimization/fig1.gif:
您可以做很多事情来优化 C++ 代码。上面列出了一些更广泛的建议。
几个具体的例子是:
一般来说,遵循诸如面向数据编程之类的基本编程范式将产生更高的性能,因为 DOP 是专门为关注性能而制定的(在所有形式中:内存布局、缓存一致性、运行时成本等)
更多信息:https ://people.cs.clemson.edu/~dhouse/courses/405/papers/optimize.pdf