6

是的,我知道一条机器指令通常无关紧要。我问这个问题是因为我想了解 pimpl 成语,并以最好的方式使用它;而且有时我确实关心一条机器指令。 )

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

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

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

事情.hh:

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

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

东西.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. 这是相关的输出:

物质:: calc():
0000000000000000 MOVQ(%rdi),%RAX
0000000000000003 MOVL(%RAX),%EAX 00000000000000000005 含量
含量
000000000000000000000007 ret
calc(earthting*) 0000000000000014 ret


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

4

5 回答 5

7

一条指令很少是一件需要花很多时间担心的事情。首先,编译器可能会在更复杂的用例中缓存 pImpl,从而在实际场景中分摊成本。其次,流水线架构几乎不可能预测时钟周期的实际成本。如果您在循环中运行这些操作并对差异进行计时,您将获得更现实的成本概念。

于 2010-05-21T08:31:06.823 回答
5

不太难,只需在课堂上使用相同的技术即可。任何半途而废的优化器都将内联琐碎的包装器。

class ThingImpl;
class Thing
{
    ThingImpl *impl;
    static int calc(ThingImpl*);
public:
    Thing();
    int calc() { calc(impl); }
};
于 2010-05-21T08:30:14.390 回答
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. 实际上,您在这里有 2 次 new 调用,一个是显式的,另一个是隐式的(由Thing.
  2. 您在这里有 1 个新的,隐式的(在 2 内)

您应该在堆栈上分配Thing,尽管它可能不会更改双重取消引用指令......但可能会改变其成本(删除缓存未命中)。

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

我认为自动内存处理值得一条额外的内存指令,特别是因为如前所述,如果您多次访问取消引用的值,它可能会被缓存,因此几乎没有。

正确性比性能更重要。

于 2010-05-21T11:30:15.917 回答
2

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

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

于 2010-05-21T09:10:06.757 回答
0

让编译器担心它。它比我们更了解什么实际上更快或更慢。尤其是在如此微小的规模上。

在类中拥有项目比仅仅封装有更多的好处。PIMPL 是个好主意,如果您忘记了如何使用 private 关键字。

于 2010-05-21T12:09:49.500 回答