【问题标题】:Overhead of creating a new class创建新类的开销
【发布时间】:2011-05-20 03:06:00
【问题描述】:

如果我有这样定义的类:

class classWithInt
{
public:
    classWithInt();
...
private:
    int someInt;
...
}

someIntclassWithInt 中唯一且唯一的一个成员变量,那么声明这个类的新实例比只声明一个新整数要慢多少?

如果你在课堂上有 10 个这样的整数呢? 100?

【问题讨论】:

  • 类是魔鬼。你永远不应该使用它们。你所有的代码都应该放在一个包含很多函数的巨大模块中,并且应该在全局变量中使用简单的类型。在相关说明上;当我感到特别讽刺时,我应该停止访问这个网站。 :p
  • 为什么不尝试创建 100 个类和 100 个整数呢?甚至一百万个整数!
  • 您是否尝试过并对其进行了分析?为什么这有关系?你为什么要这样做?我敢打赌它会慢数百倍,但你的编译器可能会做一些花哨的事情。您有什么用例使这具有相关性?
  • 答案取决于您的 classWithInt() 构造函数是否计算 pi 并将第 100 万位小数存储到 someInt 中。 :-)
  • 在编译好的面向对象语言(如 C++)中,类声明只会减慢编译速度——而且可能不足以让您注意到甚至无法衡量。在运行时,它会根据实例大小、基类的数量以及构造函数的作用而有所不同。像 int 这样的灵长类类型,它不是一个类,它非常轻量级,并且可以编译和运行,因为语义是内置的并且接近硬件自然所做的。

标签: c++ class optimization int


【解决方案1】:

好吧,让我们测试一下。我可以通过完全优化编译一个更完整的示例,如下所示:

void use(int &);

class classWithInt
{
public:
    classWithInt() : someInt(){}
    int someInt;
};
class podWithInt
{
public:
    int someInt;
};

int main() {
  int foo;
  classWithInt bar;
  podWithInt baz;

  use(foo);
  use(bar.someInt);
  use(baz.someInt);

  return 5;

}

这是我从gcc-llvm得到的输出

; ModuleID = '/tmp/webcompile/_21792_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-linux-gnu"

%struct.classWithInt = type { i32 }

define i32 @main() {
entry:
  %foo = alloca i32, align 4                      ; <i32*> [#uses=1]
  %bar = alloca %struct.classWithInt, align 8     ; <%struct.classWithInt*> [#uses=1]
  %baz = alloca %struct.classWithInt, align 8     ; <%struct.classWithInt*> [#uses=1]
  %0 = getelementptr inbounds %struct.classWithInt* %bar, i64 0, i32 0 ; <i32*> [#uses=2]
  store i32 0, i32* %0, align 8
  call void @_Z3useRi(i32* %foo)
  call void @_Z3useRi(i32* %0)
  %1 = getelementptr inbounds %struct.classWithInt* %baz, i64 0, i32 0 ; <i32*> [#uses=1]
  call void @_Z3useRi(i32* %1)
  ret i32 5
}

declare void @_Z3useRi(i32*)

每种情况都有一些差异。在最简单的情况下,POD type 与普通 int 仅在一个方面不同,它需要不同的对齐方式,它是 8 字节对齐而不是 4 字节。

另一个值得注意的事情是 POD 和裸 int 没有被初始化。它们的存储空间就像从堆栈的荒野中一样使用。非 pod 类型具有非平凡的构造函数,会导致在实例可用于其他任何内容之前存储一个零。

【讨论】:

    【解决方案2】:

    使用不是由凌晨喝醉的大学生编写的编译器,开销为零。至少在你开始使用virtual 函数之前;那么你必须为虚拟调度机制付出代价。或者,如果您在类中没有数据,在这种情况下,类仍然需要占用一些空间(这反过来又是因为每个对象在内存中必须具有唯一的地址)。

    函数不是对象数据布局的一部分。它们只是对象的心理概念的一部分。函数被翻译成以对象实例作为附加参数的代码,对成员函数的调用也被相应地翻译成传递对象。

    数据成员的数量无关紧要。比较苹果和苹果;如果你有一个包含 10 个整数的类,那么它会占用与 10 个整数相同的空间。

    在堆栈上分配东西实际上是免费的,无论它们是什么。编译器将所有局部变量的大小相加并立即调整堆栈指针以为它们腾出空间。在内存中分配空间成本,但成本可能更多地取决于分配的数量而不是分配的空间量。

    【讨论】:

    • 精神状态成本。被调用的构造函数需要在 CPU 周期方面不是免费的函数调用。您提到了函数,但我认为您应该单独解释构造函数部分,因为构造函数存在于问题中并且没有内联。
    • @Basilevs 是什么让您认为构造函数调用不会在普通函数调用无法实现的任何地方内联?此外,根据类的 POD 状态,默认构造可能实际上不会对所有内容进行零初始化。不过,我永远记不起确切的规则,因为我有更大的优化需要担心大约 100% 的时间。此外,如果使用类会给您带来真正的“心理状态”开销,那么 C++ 可能不适合您,也不是比 C 更高级的语言。虽然我真的不明白怎么会有人这么想。
    • 只有在 header 中实现的函数才能被内联。问题中没有内联实现。构造函数可能在单独的编译模块中实现。构造动作根本不重要,我的观点是构造函数调用自身成本。使用内联函数可以避免心理状态成本。同样重要的是要记住,像这样的优化只在高负载应用程序中才有意义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 2011-01-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多