【问题标题】:C++ performance of accessing member variables versus local variables访问成员变量与局部变量的 C++ 性能
【发布时间】:2008-10-26 20:08:15
【问题描述】:

类访问成员变量还是局部变量更有效?例如,假设您有一个(回调)方法,其唯一职责是接收数据,对其执行计算,然后将其传递给其他类。在性能方面,拥有一个方法在接收数据时填充的成员变量列表是否更有意义?还是每次调用回调方法时都声明局部变量?

假设这个方法每秒会被调用数百次......

如果我不清楚,这里有一些简单的例子:

// use local variables
class thisClass {
    public:
        void callback( msg& msg )
        {
            int varA;
            double varB;
            std::string varC;
            varA = msg.getInt();
            varB = msg.getDouble();
            varC = msg.getString();

            // do a bunch of calculations
         }

};

// use member variables
class thisClass {
    public:
        void callback( msg& msg )
        {
             m_varA = msg.getInt();
             m_varB = msg.getDouble();
             m_varC = msg.getString();

             // do a bunch of calculations
        }

    private:
        int m_varA;
        double m_varB;
        std::string m_varC;

};

【问题讨论】:

  • 任何差异几乎肯定会被首先检索值的成本所掩盖。每秒几百次也不算什么。 call 您的函数的堆栈帧的成本可能会使您显示的任何内容相形见绌。闻起来像过早的优化。

标签: c++ performance


【解决方案1】:

执行摘要:几乎在所有场景中,都无所谓,但局部变量有一点优势。

警告:您正在进行微优化。您最终将花费数小时试图理解本应赢得一纳秒的代码。

警告:在您的场景中,性能问题不应该是问题,而是变量的作用 - 它们是临时的,还是 thisClass 的状态?

警告:优化的第一、第二和最后一条规则:测量!


首先,查看为 x86 生成的典型程序集(您的平台可能会有所不同):

// stack variable: load into eax
mov eax, [esp+10]

// member variable: load into eax
mov ecx, [adress of object]
mov eax, [ecx+4]

一旦对象的地址被加载到寄存器中,指令是相同的。加载对象地址通常可以与较早的指令配对,并且不会影响执行时间。

但这意味着 ecx 寄存器不可用于其他优化。 然而,现代 CPU 做了一些巧妙的技巧来减少这个问题。

此外,当访问许多对象时,这可能会花费您额外的费用。 然而,这还不到一个周期的平均值,而且配对指令的机会通常更多。

内存局部性:这是堆栈大获全胜的机会。栈顶实际上总是在 L1 缓存中,因此加载需要一个周期。对象更有可能被推回二级缓存(经验法则,10 个周期)或主内存(100 个周期)。

不过,您只需为首次访问付费。如果您只有一次访问,则 10 或 100 个周期是不明显的。如果您有数千次访问,对象数据也会在 L1 缓存中。

总而言之,增益是如此之小,以至于将成员变量复制到局部变量中以获得更好的性能几乎没有意义。

【讨论】:

  • C++ 没有特定的 ABI。但是我读过的那些总是为 this 指针保留一个。所以没有收获。另外,您忘记了复制本地人的成本。
  • 由于类数据是连续放入内存的,当你第一次访问任何成员数据时,几乎所有类的状态都应该作为缓存行加载到L1中。之后,访问应该是相同的。
  • 如前所述,“但是,您只需为首次访问付费”。 ;-)
  • 虽然您没有考虑到对象当前可能没有加载到 CPU 缓存中。访问该内存可能会导致需要从 Ram 获取内存的页面错误。因此,即使可能几乎没有任何差异,但取决于缓存命中未命中,它可能会变得更加重要。
  • @Tolga:不要不要i 设为会员,因为这样可以节省您输入int i=0 20 次甚至100 次的时间。它可能会使用更多空间而不是更快,但这甚至不是重点:使其成为成员会引入状态(即,可能会影响类行为的变量)。如果 看到i 是成员,我希望它会被这样使用。更改类时,有人可能会无意中破坏代码。 --- 附:您可能想在 codereview.stackexchange.com 发布您的课程 - 听起来有些问题,一些反馈会有所帮助。
【解决方案2】:

我更喜欢一般原则上的局部变量,因为它们可以最大限度地减少程序中的邪恶可变状态。至于性能,你的分析器会告诉你所有你需要知道的。对于 int 和其他内置函数,Locals 应该更快,因为它们可以放在寄存器中。

【讨论】:

    【解决方案3】:

    这应该是你的编译器问题。相反,优化可维护性:如果信息只在本地使用,则将其存储在本地(自动)变量中。我讨厌阅读充斥着成员变量的类,这些成员变量实际上并没有告诉我关于类本身的任何信息,而只是关于一堆方法如何协同工作的一些细节:(

    事实上,如果局部变量无论如何都没有更快,我会感到惊讶 - 它们一定会在缓存中,因为它们靠近其余的函数数据(调用帧)并且对象指针可能完全在某个地方否则 - 但我只是在这里猜测。

    【讨论】:

      【解决方案4】:

      愚蠢的问题。
      这完全取决于编译器及其优化功能。

      即使它确实有效,你得到了什么?混淆代码的方法?

      变量访问通常通过指针和偏移量来完成。

      • 指向对象的指针 + 偏移量
      • 指向堆栈帧 + 偏移量的指针

      不要忘记将变量移动到本地存储然后将结果复制回来的成本。所有这些都可能意义不大,因为编译器可能足够聪明,可以优化大部分内容。

      【讨论】:

      • 将变量移动到本地存储然后将结果复制回来的成本是什么意思?这是问题的一部分......将值复制到成员变量而不是局部变量是否有任何性能提升?
      【解决方案5】:

      其他人没有明确提及的几点:

      • 您可能会在代码中调用赋值运算符。 例如 varC = msg.getString();

      • 每次设置功能框架时都会浪费一些周期。您正在创建变量,调用默认构造函数,然后调用赋值运算符将 RHS 值放入本地。

      • 将局部变量声明为 const-refs,当然还要对其进行初始化。

      • 成员变量可能在堆上(如果您的对象被分配在那里),因此会受到非局部性的影响。

      • 即使节省了几个周期也很好 - 如果可以避免的话,为什么还要浪费计算时间。

      【讨论】:

        【解决方案6】:

        如有疑问,请进行基准测试并亲自查看。并确保它首先产生影响 - 每秒数百次对现代处理器来说并不是一个巨大的负担。

        也就是说,我认为不会有任何区别。两者都是指针的常量偏移量,局部变量来自堆栈指针,成员来自“this”指针。

        【讨论】:

          【解决方案7】:

          在我看来,它不应该影响性能,因为:

          • 在您的第一个示例中,变量是通过堆栈上的查找来访问的,例如[ESP]+4 表示当前栈尾加上四个字节
          • 在第二个示例中,通过相对于 this 的查找来访问变量(请记住,varB 等于 this->varB)。这是类似的机器指令。

          因此,差别不大。

          但是,您应该避免复制字符串;)

          【讨论】:

            【解决方案8】:

            与您在算法实现中表示数据的方式相比,您将与之交互的数据量对执行速度的影响更大。

            处理器并不真正关心数据是在堆栈上还是在堆上(除了堆栈顶部可能会在处理器缓存中,正如 peterchen 提到的那样),但为了获得最大速度,数据将有以适应处理器的缓存(L1 缓存,如果您有不止一级缓存,几乎所有现代处理器都有)。来自 L2 缓存的任何负载 - 或 $DEITY 禁止,主内存 - 都会减慢执行速度。因此,如果您正在处理一个大小为几百 KB 且每次调用都有机会的字符串,则甚至无法测量差异。

            请记住,在大多数情况下,最终用户几乎察觉不到程序中 10% 的加速(除非您设法将隔夜批处理的运行时间从 25 小时缩短到 24 小时以下),所以这不是除非您确定并且有分析器输出来支持此特定代码位于对程序运行时有重大影响的 10%-20% 的“热区”内,否则值得担心。

            其他考虑因素应该更重要,例如可维护性或其他外部因素。例如,如果上面的代码是多线程代码,使用局部变量可以使实现更容易。

            【讨论】:

              【解决方案9】:

              这取决于,但我希望绝对没有区别。

              重要的是:使用成员变量作为临时变量将使您的代码不可重入 - 例如,如果两个线程尝试在同一个对象上调用 callback(),它将失败。使用静态局部变量(或静态成员变量)更糟糕,因为如果两个线程尝试在 any thisClass 对象或后代对象上调用 callback(),您的代码将失败。

              【讨论】:

              • 重入!=并发。你做了两个由误导性的“即”连接的真实陈述。即使在单线程代码中,不可重入代码也可能出错,例如,如果回调调用了再次调用它的东西,或者它是从中断它的信号处理程序中调用的。无论如何 +1。
              • 将两者混为一谈的问题在于,有时天真的人认为通过添加锁,或者只有一个线程,他们可以使他们的代码重入安全。不是这样:这只会使其并发安全,并且还有其他原因会导致重入。
              • “重入!=并发”。你是对的 - 谢谢。我已将“即”更改为“例如”。
              【解决方案10】:

              使用成员变量应该稍微快一些,因为它们只需要分配一次(在构造对象时),而不是每次调用回调时。但与您可能正在做的其他工作相比,我预计这将是一个非常小的百分比。对两者进行基准测试,看看哪个更快。

              【讨论】:

                【解决方案11】:

                此外,还有第三种选择:静态局部变量。这些不会在每次调用函数时重新分配(事实上,它们会在调用中保留),但它们不会用过多的成员变量污染类。

                【讨论】:

                • 他仍然必须每次都初始化它们才能获得相同的行为。并且局部变量的“分配”相当于不同的烘焙堆栈指针增量。所以无论哪种方式,成本都是初始化的成本。
                猜你喜欢
                • 2012-09-17
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-10-15
                • 2013-03-22
                • 2010-12-17
                • 2015-10-21
                • 2014-11-17
                相关资源
                最近更新 更多