C ++性能提示和经验法则是谁?

时间:2008-11-24 20:37:19

标签: c++ performance cross-platform

编码时,在性能方面要记住哪一个好的经验法则?有很多方法可以针对特定平台和编译器进行优化,但我正在寻找在编译器和平台上同样适用(或几乎)的答案。

26 个答案:

答案 0 :(得分:16)

想到一句名言:

“我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源。” (Knuth,Donald。结构化编程,参见陈述,ACM期刊计算调查,第6卷,第4期,1974年12月。第268页。)

但也许你不应该按价值传递大型数据结构......: - )

编辑:也许还可以避免 O (N ^ 2)或更复杂的算法......

答案 1 :(得分:12)

数字#1性能提示是尽早和经常分析您的代码。有很多一般的“不要这样做”的提示,但很难保证这会影响你的应用程序的性能。为什么?每个应用程序都不同。如果你有很多元素但你的程序甚至使用了一个向量(你可能应该......),很容易说按值传递向量是不好的?

分析是了解应用程序性能的唯一方法。我遇到过很多情况,人们“优化”了代码但却没有描述过。 “优化”最终引入了许多错误,甚至不是代码路径中的热点。浪费每个人的时间。

编辑:

有几个人对我答案的“早期”部分发表了评论。我不认为你应该从第1天开始进行剖析。但是你也不应该等到船上1个月。

我通常首先介绍一下我有几个明确的端到端场景,或者在一个更大的项目中,一个主要功能组件。我需要一两天(通常与QA合作)将一些大型场景聚集在一起并将其抛在代码中。这是一个很好的现场检查,以及早发现明显的性能问题。在这一点上修复它们会容易一些。

在一个典型的项目中,我发现我的代码符合此标准的30%-40%通过项目(100%在客户手中)。我很早就把这个时间分类了。

答案 2 :(得分:11)

  • 尽可能使用ifswitch而不是通过函数指针调用。澄清:void doit(int m) { switch(m) { case 1: f1(); break; case 2: f2(); break; } }代替void doit(void(*m)()) { m(); }可以内联电话。
  • 如果可能且没有造成伤害,请更喜欢CRTP到虚拟功能
  • 如果可能,请避免使用C字符串并使用String类。它会经常更快。 (恒定时间长度“测量”,附加摊销的常数时间,......)
  • 始终通过引用const(T const&)而不是复制值来传递用户定义的类型值(除了它没有意义的地方。例如迭代器)。
  • 对于用户定义的类型,请始终使用++t而不是t++
  • 经常提早使用const。最重要的是提高可读性。
  • 尽量将new保持在最低限度。如果可能的话,总是更喜欢自动变量(在堆栈上)
  • 如果您想要零,则不要自己填充数组,而是使用T t[N] = { };之类的空初始值设定项列表进行初始化。
  • 尽可能经常使用构造函数初始值设定项列表,尤其是在初始化用户定义的类型成员时。
  • 使用仿函数(operator()重载的类型)。它们比通过函数指针调用更好地内联。
  • 如果您的固定大小的数量没有增长,请不要使用std::vectorstd::string等类。使用boost::array<T, Size>或裸阵列并正确使用它。

事实上,我几乎忘了它:

过早优化是万恶之源

答案 3 :(得分:11)

有人提到了函数指针(为什么你应该使用if)。好吧,甚至更好:改为使用仿函数,它们内联并且通常具有零开销。仿函数是一个结构(或类,但通常是前者),它重载了运算符(),其实例可以像普通函数一样使用:

template <typename T>
struct add {
    operator T ()(T const& a, T const& b) const { return a + b; }
};

int result = add<int>()(1, 2);

几乎可以在可以使用普通函数或函数指针的每个上下文中使用它们。它们通常来自std::unary_functionstd::binary_function,但这通常不是必需的(实际上只是为了继承一些有用的typedef)。

编辑上述代码中需要显式<int>类型限定。类型推断仅适用于函数调用,而不适用于实例创建。但是,通常可以通过使用make辅助函数来省略它。这是在pair s的STL中完成的:

template <typename T1, typename T2>
pair<T1, T2> make_pair(T1 const& first, T2 const& second) {
    return pair<T1, T2>(first, second);
}

// Implied types:
pair<int, float> pif = make_pair(1, 1.0f);

有人在评论中提到,仿函数有时被称为“functionoids”。是 ish - 但不完全。实际上,“functor”是“函数对象”的缩写(有些奇怪)。函数在概念上是相似的,但是通过使用虚函数来实现(尽管它们有时被同义地使用)。例如,functionoid可能看起来像这样(以及必要的接口定义):

template <typename T, typename R>
struct UnaryFunctionoid {
    virtual R invoke(T const& value) const = 0;
};

struct IsEvenFunction : UnaryFunctionoid<int, bool> {
    bool invoke(int const& value) const { return value % 2 == 0; }
};

// call it, somewhat clumsily:
UnaryFunctionoid const& f = IsEvenFunction();
f.invoke(4); // true

当然,由于虚函数调用,这会失去仿函数所具有的任何性能优势。因此,它在不同的上下文中使用,实际上需要多态(有状态)运行时函数。

关于此主题的C ++常见问题解答more to say

答案 4 :(得分:8)

在需要之前不要打扰优化。要查明是否需要,请查看个人资料。不要猜;有证据。

此外,算法优化通常比微算具有更大的影响。使用A-star而不是暴力寻路会更快,就像Bresenham圈子比使用sin / cos更好。当然,这些都是例外,但它们非常(非常)罕见(<0.1%)。如果您有一个好的设计,更改算法只会更改代码中的一个模块。容易。

答案 5 :(得分:7)

使用已使用和重复使用的现有已审核代码。 (示例:STL,boost vs roll your own container and algos)

由于评论而更新: 正确使用已使用和重复使用的现有已审查代码。

答案 6 :(得分:4)

另一点:最快的代码是不存在的代码。这意味着您的项目需要的功能越强大,功能越充分,代码就越慢。底线:尽可能省略绒毛,同时确保您仍然符合要求。

答案 7 :(得分:4)

就性能而言,你可以做的最好的事情是从一个可靠的架构和线程模型开始。其他一切都将建立在这个基础上,所以如果你的基础很糟糕,那么你的成品将只会那么好。分析发布的时间稍晚,甚至晚于微优化(一般来说,这些都是微不足道的,并且使代码比任何东西都复杂。)

故事的道德是:从一个有效的基础开始,建立在认识不做彻底愚蠢和缓慢的事情之上,你应该没事。

答案 8 :(得分:3)

C ++的两个最佳技巧:

购买Effective C ++,作者:Scott Meyers。

然后购买Scott Meyers的更有效的C ++。

答案 9 :(得分:2)

尽可能保持代码清洁。如今,编译器是无懈可击的。然后,如果你确实有一个性能问题,那就是个人资料。

所有这一切都是在为您的问题选择了最佳算法之后。

答案 10 :(得分:2)

以下是一些:

  • 有效利用Inlining(取决于您的平台)。

  • 尽量避免使用temporaries(并知道它们是什么)

    x = y + z;

    如果写成:

    ,会更好地进行优化

    X = Y;

    X + = Z;

另外,请避免使用virtual functions并仅在需要使用它们时创建对象。

如果您有心情,请查看Efficient C++。我上学的时候,我在家里有一份副本。

答案 11 :(得分:2)

Wikibooks has some things.

要做的好事就是要知道你所使用的效率。乘法的加法速度有多快,矢量与正常阵列的比较速度有多快,或者某些算法的比较程度如何。这允许您为任务选择最有效的工具

答案 12 :(得分:2)

使用通用算法是一个很好的优化技巧 - 不是在运行时而是在编码时间方面。知道你可以排序(开始,结束)并期望范围 - 无论是数据库的两个指针还是迭代器 - 都将被排序(而且,所使用的算法也将是运行时效率)。通用编程是使C ++独一无二且功能强大的原因,您应始终牢记这一点。您不必编写许多算法,因为版本已经存在(并且可能比您编写的任何内容更快或更快)。如果您有其他考虑因素,那么您可以专门研究algos。

答案 13 :(得分:1)

基本上,通过算法改进可以获得最大的性能提升。这意味着使用最有效的算法,反过来,使用最有效的数据项容器。

有时候很难知道什么是最好的权衡,但幸运的是STL的设计者正是考虑到这个用例,因此STL容器通常足够灵活,允许容器根据需要混合和匹配满足应用程序的要求。

要完全实现这一好处,您需要确保不要将内部设计选择作为类/模块/接口的一部分公开。您的所有客户都不应依赖来使用std::vector。至少提供他们(和你)可以使用的typedef,这应该允许根据你的需要将向量更改为列表(或其他)。

同样确保您可以随意选择最广泛的调试算法和容器。如今,Boost和/或TR1是很多必需品。

答案 14 :(得分:1)

考虑使用memory pool

答案 15 :(得分:1)

我提到的一本C ++书籍(Bulka和Mayhew的高效C ++性能技术),其中明确谈到了C ++性能方面。其中一个是;

在定义构造函数时...也初始化其他构造函数;有点像;

class x {

x::x(char *str):m_x(str) {} // and not as x::x(char *str) { m_str(str); }

private:
std::string m_x;

};

以上是引起我注意的一些事情,并帮助我改进了我的编码风格......这本书有更多内容可以分享这个有趣的表演主题。

答案 16 :(得分:1)

粗略设计

在性能关键区域,将您的类和数据结构设计保持在较粗糙的一侧,而不是细粒度的一面。对性能至关重要,我的意思是这样测量,或者你可以肯定地预测大量输入被重复处理(例如:每一帧)。这样做的目的是在将来为任何必要的优化留出足够的喘息空间。否则,我们可能会看到需要对代码库中心区域进行大量重新设计/重写的瓶颈,这需要对所有依赖项进行级联重写。

让我们来看看其他遇到热点的人遇到的一些粒状设计的例子。

一大堆字符串

// Stores millions of strings.
std::vector<std::string> boatload_of_strings;

这是一个粒度设计,因为我们在数百万个实例中存储了一个完整的字符串容器/类。数百万个存储的每一个小小的字符串都是使用一个完整的,可变大小的内存管理容器std::string来表示的。这最终要么在内存使用方面具有爆炸性,要么需要更多的内存,要么使用比必要更多的堆分配,或两者的组合。通过最近的小字符串优化,最小sizeof(std::string)可以大到24个字节,如果我们的大多数字符串只有几个字符长,这是非常具有爆炸性的内存使用。如果字符串是中等长度的,那么每个字符串仍然可能会产生单独的堆分配。无论哪种方式,它都转化为大量缓存未命中。相反,如果你让设计更粗糙:

class BoatloadOfStrings
{
public:
     // Returns the nth string.
     const char* operator[](int n) const
     {
         return buffer.data() + string_start[n];
     }

     // Inserts a string.
     void insert(const char* str)
     {
         string_start.push_back(buffer.size());
         buffer.insert(buffer.end(), str, str + strlen(str)+1);
     }

private:
     // Stores all the characters of all null-terminated
     // strings in one giant buffer.
     std::vector<char> buffer;

     // Stores the starting position of each null-terminated
     // string.
     std::vector<size_t> string_start;
};

...现在我们最终使用更少的内存,只有当这两个向量中的一个超过容量(摊销成本)时才会面临堆分配。即使我们使用存储数百万std::vector的{​​{1}} std::string的{​​{1}}开始使用上述解决方案,使用这种更粗略的BoatloadOfStrings类设计为我们提供了更多的喘息空间来优化我们有一大堆依赖于前者的代表。

抽象像素的载荷

class IPixel
{
public:
     virtual ~IPixel() {}

     // Abstract pixel operations.
     ...
};

如果我们正在设计一个图像/视频处理应用程序,它必须反复循环通过像素来执行自定义视频过滤器等操作,那么上述设计非常浪费,但也没有让我们喘不过气来如果整个系统依赖于这样的界面,那就进一步优化它。

通常,动态调度和虚拟指针之类的东西的成本是便士,但如果他们每帧多次支付数百万次便士便宜。以每像素为基础支付虚拟调度的成本确实变得相对非常昂贵,并且虚拟指针的大小,尽管在64位系统上只有8个字节,四倍考虑到它的64位对齐要求(64位vptr + 32位像素数据+ 32位填充对齐),当我们考虑它的大小和填充它将添加到结构时,内存使用32位RGBA图像vptr的)。

在这种情况下我的建议解决方案相同。设计更粗糙。如果您需要多态性,请查看是否可以在较粗略的图像级别进行抽象:

class IImage
{
public:
     virtual ~IImage() {}

     // Abstract image operations.
     ...
};

虚拟指针和虚拟调度的成本突然变得非常便宜,因为它们只为整个图像支付一次,这可能很好地由数百万像素作为像素容器组成。现在你有空间做一些事情,比如将SIMD instrinsics应用到中央级别的并行循环中的图像操作,而无需重写代码库的大部分内容。

生物的船载

class Creature
{
public:
     virtual ~Creature() {}

     // Abstract creature operations.
     ...
};

class Human: public Creature
{
     ...
};

class Orc: public Creature
{
     ...
};

class Undead: public Creature
{
     ...
};

让我们说我们有一个实时的战争模拟,其中包含一大堆抽象生物,在单个游戏环节中有大量单位,指环王式。生物可以在任何特定时刻被移除,因为它们可能会死亡,例如我们在每帧中应用的大多数关键循环实际上都是顺序循环,它们可以像沿着给定路径移动所有生物一样。

在这种情况下,上述表示可能成为一个瓶颈,几乎没有进一步优化的喘息空间,因为使用多态基本指针Creature*的代码迫使我们一次处理一个生物。除此之外,每个生物可能有不同的大小和对齐要求,这意味着它们通常不能连续存储在内存中(例如:我们可能无法将orc的数据交错到人类旁边&#39 ; s数据在内存中,即使我们想处理一个人类跟随的兽人。)

在这些情况下,您可以通过为每个生物子类型使用单独的分配器来改善性能,例如每个生物子类型的单独空闲列表,其在连续块中分配特定类型的生物,以及基数排序您的多态基本指针按地址改进了引用的局部性(相同类型的相邻生物的空间局部性和vtable上的时间局部性,例如)。然而,在有限的空间内进行优化需要付出很多努力。同时,如果你只是使用这样的粗糙设计:

class CreatureHorde
{
public:
     virtual ~CreatureHorde() {}

     // Abstract creature horde operations.
     ...
};

class HumanHorde: public CreatureHorde
{
     ...
};

class OrcHorde: public CreatureHorde
{
     ...
};

class UndeadHorde: public CreatureHorde
{
     ...
};

......现在我们正在设计整个大量生物的水平,就像我们将抽象像素界面变成抽象图像界面一样。现在我们有各种各样的空间可以轻松优化,因为我们的多态基本指针(CreatureHorde*)将指向要处理的整个部落生物,而不是一次只有一个生物。在每个部落中,我们可能对缓存友好的顺序处理,例如,人类单位中的人类单位的所有数据在连续的矢量/数组中按顺序存储和处理。

<强>结论

所以无论如何,这是我的头号建议,它与设计不仅仅是实现有关。当您预期系统中的大型,性能关键的循环区域时,设计在较粗糙的级别。使用较粗糙的数据结构,较粗糙的类接口和较粗略的抽象。不要将大量的东西作为独立的类和独立的数据结构代表它们自己的权利,或者至少不是更粗略设计的私有实现细节。

在让设计足够粗糙以便为您提供优化的呼吸空间之后,您可能会考虑让自己获得一些不错的分析工具,提高您对内存层次结构的理解,并行化您的代码(使用更粗糙的设计也变得更容易了,矢量化(使用更粗糙的设计也变得更容易)等。

但是,当我们考虑提高效率时,我的首要任务是设计效率,因为缺乏这一点,即使在我们描述和准确发现我们的热点时,我们甚至无法有效地优化我们的实施。设计效率通常只归结为具有足够的呼吸空间来优化设计,并且有效地归结为更粗糙的设计,不会尝试对最微小的对象和数据结构进行建模。

答案 17 :(得分:1)

不要使用效率极低的算法,在编译器中启用优化,不要优化任何事情,除非探查器显示它是瓶颈,并且当你尝试改进测试时看看你是否做得好还是不好。还要记住,库函数通常比人们更好地优化了。

与其相比,其他一切都很小。

答案 18 :(得分:1)

  1. 总是试着考虑你的内存看起来如何 - 例如,一个数组是大小为numOfObjects X sizeof(object)的连续内存行。二维数组是n X m X sizeof(object),每个对象的索引为n + m X n,所以

    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            arr[i,j] = f();
    

    比单一过程好得多:

    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            arr[j,i] = f();
    

    因为数组以连续的块的形式进入缓存,所以第一个片段在获取其余部分之前在缓存中的所有单元格上运行,而第二个片段需要一遍又一遍地将新的数组单元格提取到单元格中< / p>

  2. 当您的应用程序开始放慢使用性能基准测试以找到确切的瓶颈时 即使是简单的GetTickCount调用也可用于确定组件运行所需的时间。 在更大的项目中,在开始优化之前使用适当的分析器,这样您就可以在最重要的地方花费最多的优化工作。

答案 19 :(得分:1)

一个简单的消化就是习惯做++ i,而不是i ++。 i ++制作副本,这可能很昂贵。

答案 20 :(得分:0)

无论您采取什么行动来节省几个周期,请记住:不要试图比编译器更聪明 - 测量以验证增益。

答案 21 :(得分:0)

虽然没有解决确切问题,但有些建议是:

始终为接口编写代码(当涉及算法时),以便您可以通过有效的(通过任何请求方式)顺利地替换它们。

答案 22 :(得分:0)

我同意有关过早优化的建议。但是,我希望在设计过程中遵循一些指导原则,因为以后可能很难进行优化:

  • 尝试在启动时分配所有对象,在运行时最小化new的使用。
  • 设计您的数据结构,以便您的算法可以很好地成为O(1)。
  • 和往常一样,模块化,以便你可以撕掉并在以后更换。这意味着您拥有一套全面的单元测试,可以让您确信新解决方案是正确的。
  • 为您的单元测试套件添加性能测试,以确保您不会无意中得到一些O(n * n)代码: - )

答案 23 :(得分:0)

  • 规则1:不要
  • 规则2:衡量

答案 24 :(得分:0)

选择性能最佳的算法,使用更少的内存,使用更少的分支,使用快速操作,使用少量迭代。

答案 25 :(得分:0)

“过早的优化是所有邪恶的根源”(Knuth,Donald)

这实际上取决于您编写的代码类型,这是典型用法。