访问堆成员和堆栈对象之间的性能差异?

时间:2014-04-13 05:46:03

标签: c++ class pointers member

目前我正在使用' - >'运算符取消引用类中的成员。我的问题是它比正常成员访问更快。例如:

Class* myClsPtr = new Class();
myClsPtr->foo(bar);

Vs以上。

Class myCls;
myCls.foo(bar);

可以在没有性能差异的情况下使用两种方式吗?

4 个答案:

答案 0 :(得分:4)

首先,

Class myCls = new Class();

是无效的代码......我们假设您的意思是

Class myCls;

几乎没有明显的区别,但是您可以通过在循环中迭代数百万次来自己进行基准测试,并在计算两个执行时间时调用任一变量。

我刚刚在我的笔记本电脑上做了一个快速而肮脏的基准测试,迭代次数如下:

堆栈对象

struct MyStruct
{
    int i;
};

int main()
{
    MyStruct stackObject;

    for (int i = 0; i < 100000000; ++i)
        stackObject.i = 0;

    return 0;
}

然后我跑了:

g++ main.cpp && time ./a.out

结果是:

sreal   0m0.301s
user    0m0.303s
sys 0m0.000s

堆对象

struct MyStruct
{
    int i;
};

int main()
{
    MyStruct *heapObject = new MyStruct();

    for (int i = 0; i < 100000000; ++i)
        heapObject->i = 5;

    return 0;
}

然后我跑了:

g++ main.cpp && time ./a.out

结果是:

real    0m0.253s
user    0m0.250s
sys 0m0.000s

正如您所看到的,对于 100 数百万次迭代,我的机器上的堆对象稍快一些。即使在我的机器上,这对于少得多的物品也是不可能的。有一点值得注意的是,尽管后续运行的结果略有不同,但堆对象版本在我的笔记本电脑上总是表现得更好。但是,不要把它作为保证。

答案 1 :(得分:2)

与许多表演问题一样,答案复杂多变。使用堆的潜在缓慢来源是:

  • 分配和取消分配对象的时间。
  • 对象不在缓存中的可能性。

这两个都意味着堆上的对象起初可能很慢。但是如果你在一个紧密的循环中多次使用该对象,这并不重要:很快,对象将最终进入CPU缓存,无论它是存在于堆还是堆栈中。

相关问题是包含其他对象的对象是否应使用指针或副本。如果速度是唯一的问题,那么存储副本可能会更好,因为每个新的指针查找都是潜在的缓存未命中。

答案 2 :(得分:0)

由于a-> b等同于(* a).b(这确实是编译器必须创建的,至少在逻辑上是这样) - &gt;如果有的话,可能会慢于。在实践中,编译器可能会将一个地址存储在寄存器中并立即添加偏移量b,跳过(* a)部分并在内部有效地将其减少到a.b.

顺便说一下,-O3 gcc 4.8.2消除了整个循环。如果我们从main返回最后一个MyStruct :: i,它甚至会这样做 - 循环是无副作用的,并且结束值是可以计算的。只是另一个板凳评论。

然后它不是关于堆上的对象,而是关于使用地址而不是立即使用对象。对同一个对象的逻辑是相同的:

MyStruct m;
mp = &m;

然后用m或mp运行你的两个循环。对象的位置(就其所在的内存页面而言)可能比直接或通过指针访问它更重要,因为 locality 往往是在现代架构中很重要(具有缓存和并行性)。如果某个内存已经位于缓存的内存位置(堆栈可能已被缓存),则访问速度要比某些必须首先加载到缓存中的位置(某些任意堆位置)快得多。在任一循环中,对象所在的内存可能会保持缓存,因为那里没有太多其他事情,但在更现实的场景中(迭代向量中的指针:指针指向哪里?分散或连续的内存?)这些考虑将远超过便宜的解除引用。

答案 3 :(得分:0)

我发现结果令人费解,所以我进一步调查了一下。首先,我通过使用chrono并添加一个通过指针访问局部变量(而不是堆上的内存)的测试来增强示例prog。这确保了时间差异不是由对象的位置引起的,而是由访问方法引起的。

其次我在结构中添加了一个虚拟成员,因为我注意到直接成员目标使用了一个偏移到堆栈指针,我怀疑它可能是罪魁祸首;指针版本通过寄存器访问存储器而没有偏移。假人把那里的田地夷为平地。但它并没有什么不同。

通过指针访问堆和本地对象的速度明显加快。这是来源:

#include<chrono>
#include<iostream>

using namespace std;
using namespace std::chrono;

struct MyStruct { /* offset for i */ int dummy; int i; };

int main()
{
    MyStruct *heapPtr = new MyStruct;
    MyStruct localObj;
    MyStruct *localPtr = &localObj;

    ///////////// ptr to heap /////////////////////
    auto t1 = high_resolution_clock::now();
    for (int i = 0; i < 100000000; ++i)
    {
        heapPtr->i = i;
    }
    auto t2 = high_resolution_clock::now();
    cout << "heap ptr: " 
        << duration_cast<milliseconds>(t2-t1).count() 
        << " ms" << endl;

    ////////////////// local obj ///////////////////////
    t1 = high_resolution_clock::now();
    for (int i = 0; i < 100000000; ++i)
    {
        localObj.i = i;
    }
    t2 = high_resolution_clock::now();
    cout << "local: " 
        << duration_cast<milliseconds>(t2-t1).count() 
        << " ms" << endl;

    ////////////// ptr to local /////////////////
    t1 = high_resolution_clock::now();
    for (int i = 0; i < 100000000; ++i)
    {
        localPtr->i = i;
    }
    t2 = high_resolution_clock::now();
    cout << "ptr to local: " 
        << duration_cast<milliseconds>(t2-t1).count() 
        << " ms" << endl;

    /////////// have a side effect ///////////////
    return heapPtr->i + localObj.i;
}

这是一个典型的运行。堆和本地ptr之间的差异在两个方向上都是随机的。

heap ptr: 217 ms
local: 236 ms
ptr to local: 206 ms

这是指针的反汇编和直接访问。我假设heapPtr的堆栈偏移量是0x38,因此第一个mov将它的内容(即它指向的堆上的对象的地址)移动到%rax。这用作在第三次移动中将值移动到的地址(由于前面的虚拟成员而导致4字节偏移)。

第二个移动得到i的值(我显然是在堆栈偏移4C,如果计算所有介入的定义,它排成一行)到%edx(因为最后一个mov最多可以有一个内存操作数,是对象,所以i中的值必须进入寄存器)。

最后一个mov在寄存器%edx中将i的值输入对象的地址,现在在%rax中,加上由于虚拟对象的偏移量为4。

                heapPtr->i = i;
  3e:   48 8b 45 38             mov    0x38(%rbp),%rax
  42:   8b 55 4c                mov    0x4c(%rbp),%edx
  45:   89 50 04                mov    %edx,0x4(%rax)

正如预期的那样,直接访问时间更短。变量的值(不同的本地i,此时堆栈偏移量为0x48)被加载到寄存器%eax中,然后写入堆栈偏移量-0x60的地址(我不知道为什么有些本地对象是存储在正偏移处,而其他存储在负偏移处)。底线是这是一条比指针访问短的指令;基本上,指针访问的第一条指令(它将指针的值加载到地址寄存器中)丢失了。这正是我们所期望的 - 这是解除引用。 尽管如此,直接访问需要更多时间。我不知道为什么。由于我排除了大多数可能性,我必须假设使用%rbp比使用%rax(不太可能)慢,或者负偏移会减慢访问速度。是这样吗?

                localObj.i = i;
  d6:   8b 45 48                mov    0x48(%rbp),%eax
  d9:   89 45 a0                mov    %eax,-0x60(%rbp)

应该注意,当打开优化时,gcc将赋值移出循环。因此,这对于关注性能的人来说是一个幻想问题。此外,这些微小的差异将被任何事物淹没,真实的&#34;发生在循环中。但它仍然出乎意料。