【问题标题】:Empty base class construction overhead?空基类构造开销?
【发布时间】:2012-12-18 00:02:13
【问题描述】:

考虑以下代码:

#include <iostream>
#include <type_traits>

class Base
{
    public: static int f() {return 42;}
};

class Derived : public Base
{
    protected: int x;
};

class NotDerived
{
    protected: int x;
};

int main()
{
    std::cout<<sizeof(Base)<<std::endl;
    std::cout<<sizeof(Derived)<<std::endl;
    std::cout<<sizeof(NotDerived)<<std::endl;
    return 0;
}

使用 g++ 4.7 -O3,它会打印:

1
4
4

如果我理解得很好,这意味着启用了空基类优化。

但我的问题涉及运行时开销:与NotDerived 对象相比,创建(和破坏)Derived 对象是否有任何开销,因为Derived 应该构造/销毁相应的Base 对象?

【问题讨论】:

  • 我会这么认为,但我也不认为你应该担心。您应该使用适合您工作的任何工具。在分析阶段处理任何优化。
  • 这是我的代码的一个关键方面。使用空基类在设计方面对我有很大帮助,但由于我从事高性能计算工作,并且由于在执行过程中将创建数十亿此类对象,我应该关心这一点......
  • 你应该关心,但我的问题是你是否应该这么早关心?既然你有数字,你有没有做过一些预测来看看你会在哪里?看起来,如果一开始就很担心,那么您能做的最好的事情就是提出最大可接受命中的硬数字,然后将它们与您的编译器进行比较,记住不同的编译器可能会产生不同的结果。 ..
  • 这是个好问题。逻辑会规定,如果没有分配额外的内存,则不会有任何额外的内容要销毁,因此不会产生任何影响。汇编输出支持这一点,但我想由于这没有以任何方式标准化,你得到的任何答案都将取决于编译器。

标签: c++ optimization inheritance c++11 compiler-optimization


【解决方案1】:

虽然标准不保证在那里我会考虑一个编译器在这些情况下做了一些不同的事情略有缺陷。

实际上不需要做任何事情来初始化基础:不需要初始化内存,不需要设置虚拟调用机制。不应为其生成任何代码。

但是,如果这对您来说真的很重要,您应该始终在不平凡的设置中检查某些程序集。

【讨论】:

    【解决方案2】:

    对此的任何答案都将取决于实现,因为标准仅指定语义。

    但是,如果启用任何现代编译器和优化,我预计不会有任何区别。

    没有额外的内存要分配,没有额外的代码要运行,在构造过程中没有要更改的 vtable 指针,因为额外的基础不是虚拟的。您的 DerrivedNotDerrived 构造函数很可能是指令对指令相同的。

    关闭所有优化后,您可能会在某些平台上调用空的 Base::Base() 函数,但您真的不应该担心未优化构建的性能。


    我在 gcc.godbolt.org 上整理了一个小演示:http://tinyurl.com/cg8ogym

    总之

        extern void marker______________________________________();
        // ... 
        marker______________________________________();
        new NotDerived;
        marker______________________________________();
        new Derived;
        marker______________________________________();
    

    编译为

    call    marker______________________________________()@PLT
    movl    $4, %edi
    call    operator new(unsigned long)@PLT
    call    marker______________________________________()@PLT
    movl    $4, %edi
    call    operator new(unsigned long)@PLT
    call    marker______________________________________()@PLT
    

    如果你把它切换到 clang,它甚至会优化内存分配

    【讨论】:

      【解决方案3】:
      #include <type_traits>
      #include <unistd.h>
      
      class SlowBase
      {
          SlowBase() { ::sleep(5); }
      
          public: static int f() {return 42;}
      };
      static_assert( std::is_empty<SlowBase>::value , "SlowBase is empty" );
      

      Base 类需要 五秒钟来构建!!

      #include <type_traits>
      
      class FatBase
      {
          FatBase() = default;
          int data[1024*1024];    
          public: static int f() {return 42;}
      };
      static_assert( !std::is_empty<FatBase>::value , "FatBase is not empty" );
      

      但是这个不需要时间!

      我的观点是构造开销与类的大小无关,它与构造函数的作用有关。 SlowBase 是一个空类,但构造起来很慢。 FatBase 大小为 1 MB,但数组元素甚至不归零,因此无事可做。

      在您的 Base 示例中,有一个隐式声明的、简单的默认构造函数,因此它无关紧要。

      【讨论】:

        猜你喜欢
        • 2018-06-03
        • 1970-01-01
        • 1970-01-01
        • 2023-03-03
        • 1970-01-01
        • 1970-01-01
        • 2016-07-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多