C ++ pimpl成语浪费了一个指令与C风格?

时间:2010-05-21 08:19:00

标签: c++ optimization pimpl-idiom

是的,我知道一台机器指令通常无关紧要。我问这个问题因为我想了解pimpl成语,并以最好的方式使用它;因为有时候我会这样做关心一台机器指令。

在下面的示例代码中,有两个类Thing和。{ OtherThing。用户将包括“thing.hh”。 Thing使用pimpl成语来隐藏它的实现。 OtherThing使用C样式 - 返回和获取的非成员函数 指针。这种风格产生稍好的机器代码。我 想知道:有没有办法使用C ++风格 - 即制作功能 成员函数 - 但仍然保存机器指令。我喜欢这种风格,因为它不会污染类外的命名空间。

注意:我只关注调用成员函数(在本例中为calc)。我不是在看对象分配。

以下是我的Mac上的文件,命令和机器代码。

thing.hh:

class ThingImpl;
class Thing
{
    ThingImpl *impl;
public:
    Thing();
    int calc();
};

class OtherThing;    
OtherThing *make_other();
int calc(OtherThing *);

thing.cc:

#include "thing.hh"

struct ThingImpl
{
    int x;
};

Thing::Thing()
{
    impl = new ThingImpl;
    impl->x = 5;
}

int Thing::calc()
{
    return impl->x + 1;
}

struct OtherThing
{
    int x;
};

OtherThing *make_other()
{
    OtherThing *t = new OtherThing;
    t->x = 5;
}

int calc(OtherThing *t)
{
    return t->x + 1;
}

main.cc(只是为了测试代码实际工作......)

#include "thing.hh"
#include <cstdio>

int main()
{
    Thing *t = new Thing;
    printf("calc: %d\n", t->calc());

    OtherThing *t2 = make_other();
    printf("calc: %d\n", calc(t2));
}

生成文件:

all: main

thing.o : thing.cc thing.hh
    g++ -fomit-frame-pointer -O2 -c thing.cc

main.o : main.cc thing.hh
    g++ -fomit-frame-pointer -O2 -c main.cc

main: main.o thing.o
    g++ -O2 -o $@ $^

clean: 
    rm *.o
    rm main

运行make,然后查看机器代码。在Mac上我使用otool -tv thing.o | c++filt。在linux上我认为它是objdump -d thing.o。以下是相关输出:

  

事::计算值():
  0000000000000000 movq(%rdi),%r​​ax
  0000000000000003 movl(%rax),%eax
  0000000000000005含%eax
  退货0000000000000007   计算值(OtherThing *):
  0000000000000010 movl(%rdi),%eax
  0000000000000012含%eax
  0000000000000014 ret

由于指针间接,请注意额外的指令。第一个函数查找两个字段(impl,然后是x),而第二个函数只需要获取x。可以做些什么?

5 个答案:

答案 0 :(得分:6)

一条指令很少花费很多时间来担心。首先,编译器可以将pImpl缓存在更复杂的用例中,从而在实际场景中分摊成本。其次,流水线架构使得几乎不可能预测时钟周期中的实际成本。如果你在一个循环中运行这些操作并计算时间差,你就可以更加真实地了解成本。

答案 1 :(得分:5)

不太难,只需在课堂上使用相同的技巧。任何中途不错的优化器都会内联 琐碎的包装。

class ThingImpl;
class Thing
{
    ThingImpl *impl;
    static int calc(ThingImpl*);
public:
    Thing();
    int calc() { calc(impl); }
};

答案 2 :(得分:2)

有一种令人讨厌的方法,就是用一个足够大的无符号字符数组替换指向ThingImpl的指针然后放置/ new重新解释强制转换/显式破坏ThingImpl对象。

或者你可以通过值传递Thing,因为它应该不大于指向ThingImpl的指针,尽管可能需要多一点{引用计数{{ 1}}会破坏优化,所以你需要一些标记'拥有'ThingImpl的方法,这可能需要在某些架构上有额外的空间。)

答案 3 :(得分:2)

我不同意你的用法:你不是在比较两件事。

#include "thing.hh"
#include <cstdio>

int main()
{
    Thing *t = new Thing;                // 1
    printf("calc: %d\n", t->calc());

    OtherThing *t2 = make_other();       // 2
    printf("calc: %d\n", calc(t2));
}
  1. 你实际上有两次调用new,一个是显式的,另一个是隐式的(由Thing的构造函数完成。
  2. 你在这里有一个新的,隐含的(在2内)
  3. 您应该在堆栈上分配Thing,尽管它可能不会更改双重解除引用指令...但可能会更改其成本(删除缓存未命中)。

    然而,重点是Thing自己管理它的内存,所以你不能忘记删除实际内存,而你绝对可以使用C风格的方法。

    我认为自动内存处理值得额外的内存指令,特别是因为据说,如果你多次访问它,可能会缓存解除引用的值,因此几乎没有。

    正确性比表现更重要。

答案 4 :(得分:1)

让编译器担心它。它比我们更了解实际上更快或更慢的内容。特别是在如此微小的范围内。

在课程中拥有项目比仅仅封装有更多的好处。如果您忘记了如何使用私人关键字,那么PIMPL是一个好主意。