【问题标题】:C++ pimpl idiom wastes an instruction vs. C style?C++ pimpl idiom 浪费了一条指令与 C 风格?
【发布时间】:2011-02-22 06:10:34
【问题描述】:

(是的,我知道一条机器指令通常无关紧要。我问这个问题是因为我想了解 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 *);

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。以下是相关输出:

事物::calc():
0000000000000000 movq (%rdi),%rax
0000000000000003 movl (%rax),%eax
0000000000000005 包括 %eax
0000000000000007 ret
计算(其他事物*):
0000000000000010 movl (%rdi),%eax
0000000000000012 包括 %eax
0000000000000014 ret

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

【问题讨论】:

  • 您是否在运行全面优化?
  • @the_drow:看看 Makefile。不,他不是。 @Rob:尝试使用 -O3 进行编译……你有什么理由使用完全优化?
  • 必须在某个时候取消对指针的引用。没有办法绕过它。
  • 使用 -O3 我得到相同的结果。
  • 谢谢大家。我接受了 MSalters 的回答,因为它直接回答了我的问题。但正如马塞洛所说,该指令可能不会花费任何费用。我无法在我的计算机上检测到运行时间的差异。但我喜欢知道如何摆脱指令,以防万一我遇到测量表明它很重要的情况。 (如果我需要与 C 程序员讨论性能问题。:)

标签: c++ optimization pimpl-idiom


【解决方案1】:

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

【讨论】:

  • 我不确定用于分析的最佳功能是什么。但是我#include'd 并调用了clock(),运行时间似乎没有区别。
  • 一些仔细的编码和很长的循环可能会出现一些差异,但你的结果并不让我感到惊讶。
【解决方案2】:

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

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

【讨论】:

  • 这并不能避免双重间接:this-&gt;impl-&gt;x
  • @Marcelo:它并没有避免它,但它应该导致生成的程序集匹配他的calc(OtherThing*) 例程,因此间接是“隐藏的”。当然,没有理由用一堆静态包装函数来混淆类定义,因为它们可以隐藏在实现文件中。
  • +1, this-&gt;impl 间接应该基本上消失在您的代码中。不过,您实际上不必这样做,只需启用链接时代码生成即可。
  • 编译器并没有摆脱额外的间接寻址,它只是将它从calc() 重新定位到main()。把它放在堆栈上没有区别;您仍然需要加载impl 指向的地址,然后从中加载x。我刚刚与g++ ... -S main.cc 确认movq 在调用calc 之前立即执行,然后调用movl
  • @Marcelo:谢谢,我没有意识到这一点。 gcc 确实只是将间接移动到 main.
【解决方案3】:

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

或者您可以只传递 Thing 的值,因为它不应大于指向 ThingImpl 的指针,但可能需要多一点(ThingImpl 的引用计数会击败优化,因此您需要某种方式来标记“拥有”Thing,这可能在某些架构上需要额外空间。

【讨论】:

  • 我听说过这种丑陋的 reinterpret_cast 技术。我刚刚尝试过,它确实删除了额外的指令。但是你说的第二件事——按价值传递——似乎没有帮助。在堆栈分配的 Thing 上调用 t.calc() 会使用额外的指令调用相同的函数。
  • 您可以在 ThingImpl 上使用侵入式引用计数,因为它无论如何都是私有的。这可能不会破坏优化,因为您将引用计数器放在 ThingImpl 的(公共基类)中,而不是 Thing
  • @MSalters 我怀疑它会,因为每次将 Thing 传递给函数时,您都需要增加计数,并且增加计数可能需要比您节省的操作更多的操作,因为没有额外的指针间接。
  • @Rob N 如果比较传递值函数参数和引用或指针参数的代码会发生什么?该值应该只有一个间接,指针和引用两个。
  • Pete,我想你是对的,但现在我只对这种情况感兴趣,其中函数参数是隐式的“this”指针。
【解决方案4】:

我不同意你的用法:你不是在比较两个相同的东西。

#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 风格的方法绝对可以。

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

正确性比性能更重要。

【讨论】:

  • 您错过了我的问题中的一行:“注意:我只关注调用成员函数(在本例中为 calc)。我不关注对象分配。”跨度>
  • 我在挑战你的问题的必要性。 Pimpl 带来 RAII 以及绝缘,而 C-style 仅带来绝缘。因此,我的结论是,我准备在每次函数调用时放弃我的 CPU 中的一个周期(对于任何好的编译器如果在紧密循环中调用时都会缓存的值)以换取 RAII(从而确保正确性)。
【解决方案5】:

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

在类中包含项目的好处远远超过封装。 PIMPL 是个好主意,如果您忘记了如何使用 private 关键字。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-10-26
    • 2015-05-01
    • 1970-01-01
    • 2012-01-13
    • 1970-01-01
    • 1970-01-01
    • 2011-01-21
    相关资源
    最近更新 更多