C++:大型类(带有大量代码)的性能影响

13

我想知道在C++中编写"全能"类是否会影响性能,以及如何影响。

例如,如果我有一个只包含 uint x; uint y; 数据的类 Point,并且已经定义了所有数学操作的方法。其中一些方法可能非常大。 (复制-) 构造函数仅初始化这两个数据成员。

class Point
{
   int mx; int my;
   Point(int x, int y):mx(x),my(y){};
   Point(const Point& other):mx(other.x),my(other.y){};
 // .... HUGE number of methods....
};
现在我加载了一个大图像,并为每个像素创建一个Point,将它们塞到一个向量中并使用它们。(比如,所有方法都只被调用了一次)这只是一个愚蠢的例子! 是否会比没有方法但有很多实用函数的相同类更慢? 我不以任何方式谈论虚函数!我的动机是:我经常发现自己编写漂亮而相对功能强大的类,但当我必须像上面的示例一样初始化/使用大量这些类时,我会感到紧张。我认为我不应该这样。 我认为我知道的是:
  1. 方法在内存中只存在一次。(除了优化)
  2. 分配只针对数据成员进行, 并且它们是唯一被复制的东西。
所以这不应该有影响。我是否漏掉了什么?

2
参见:https://dev59.com/_HRB5IYBdhLWcg3wXWG2 - Nick Dandoulakis
9个回答

14

你说得对,方法只存在于内存中一次,它们就像普通函数一样,只是多了一个隐藏的this参数。

当然,只有数据成员会被考虑在内存分配中,继承可能会为对象大小引入一些额外的指向虚函数表(vptrs)的指针,但这不算大问题。


6
您已经得到了一些很好的技术建议。我想加入一些非技术性的建议:正如STL向我们展示的那样,仅使用成员函数可能不是最佳的方法。与其堆积参数,我推荐参考Scott Meyers关于这个主题的类文章:How Non-Member Functions Improve Encapsulation
虽然从技术角度来看应该没有问题,但您可能仍然希望从设计角度重新审视您的设计。

你指出了一篇有趣的文章。但我的担忧是这些人编码方式似乎与大多数普通人不同。我喜欢成员函数,因为在每个值得注意的IDE中,它们都会被建议给我,并告诉我该类可以做什么。这对于非成员非友元函数来说并不是真正可能的。 - AndreasT
1
如果一个自由函数没有告诉你它做什么,那么它需要一个更好的名称。 - sbi
不是我想表达的意思。 IDE 提供了类。<everythingIcandowithclass>,可以减少在手册中查找时的缓慢。自由函数在其中不会显示。我可以搜索它们,但这比输入 '.' 并等待半秒钟更麻烦。这就是人们寻求“不良风格”类库的原因,这些类库没有太多模板魔法和分散的接口。每个攀登 Boost 掌握之山的人都可以为自己的成就感到自豪,但只能羡慕那些仅通过上下文敏感工具提示信息就可以轻松使用的库。 - AndreasT
在这里,当您键入 namespace_name:: 时,IDE 将查找并建议命名空间成员,就像在键入 class_name.class_name->class_name:: 时一样。 - sbi

5
我想这可能比你想要的答案更多,但是让我来说一下...

SO上充满了关于X、Y或Z性能担忧的问题,而这种担忧是一种猜测。

如果你担心某个东西的表现,不要担心,找出原因

以下是应该做的事情:

  1. 编写程序

  2. 性能调优

  3. 从经验中学习

这教给我的东西,我一遍又一遍地看到它,就是这样:

  • 最佳实践说不要过早优化

  • 最佳实践说使用大量数据结构类,具有多层抽象,最好的大O算法,“信息隐藏”,采用事件驱动和通知式架构。

  • 性能调整揭示了时间消耗的原因,即:过度泛化,小题大做,调用函数和属性而不知道它们需要多长时间,并且使用指数时间在多个层次上进行。

  • 然后问的问题是:大O算法、事件和通知驱动架构等最佳实践背后的原因是什么。答案是:嗯,除其他外,性能

因此,在某种程度上,最佳实践告诉我们:过早优化。明白了吗?它说“不要担心性能”,又说“担心性能”,并导致了我们试图不去担心的事情。我们越是担心它,违背我们更好的判断,情况就会变得越糟。

我的建设性建议是:遵循上述步骤1、2和3。这将教你如何适度使用最佳实践,并为你提供最好的全方位设计。

2
如果您真的担心,可以告诉编译器内联构造函数。这个优化步骤应该会让您得到干净的代码和干净的执行结果。

1
这两段代码是相同的:
Point x;
int l=x.getLength();

int l=GetLength(x);

鉴于类Point有一个非虚拟的方法getLength()。第一次调用实际上调用了int getLength(Point &this),这与我们在第二个示例中编写的签名是相同的。(*)

当然,如果你调用的方法是虚拟的,这就不适用了,因为一切都会经过额外的间接层(类似于C风格的int l=x->lpvtbl->getLength(x)),更不用说每个像素需要2个int而不是3个,多出来的一个是指向虚拟表的指针。

(*)这并不完全准确,“this”指针是通过CPU寄存器之一传递而不是通过堆栈传递的,但机制可以轻松地两种方式都可行。


1

首先:不要过早地进行优化。 其次:清晰的代码比优化后的代码更易于维护。

类的方法有隐藏的this指针,但你不必担心它。大多数情况下,编译器会尝试通过寄存器传递它。

继承和虚函数在适当的调用中引入了间接性(继承=构造函数/析构函数调用,虚函数=该函数的每个函数调用)。

简而言之:

  • 你不经常创建/销毁的对象可以具有虚方法、继承等,只要它有益于设计。
  • 你经常创建/销毁的对象应该很小(少量数据成员),并且不应该有太多虚方法(最好根本没有-从性能上讲)。
  • 尝试内联小方法/构造函数。这将减少开销。
  • 采用清晰的设计,如果达不到所需的性能,则进行重构。

关于类具有大或小接口的讨论是不同的(例如,在Scott Meyers(More)Effective C++书籍中的其中一本中,他选择最小接口)。但这与性能无关。


你的第二点应该是“做”而不是“不做”。此外,在第二点中,我看不出拥有少量虚方法的优势,因为虚调用速度较慢,因此调用大量虚方法会带来一些(小)性能问题。 - Elemental
谢谢 - 当然,你是对的。如果您有少量虚拟方法,则大多数方法调用为直接调用(不经过 vtbl 的间接寻址)。如果您设法设计您的类而不使用任何虚函数,则在构造期间无需设置 vtbl。 - Tobias Langner

0

我创建了与您相同的点类,只不过它是一个模板类,并且所有函数都是内联的。我期望通过这种方式看到性能提高而不是降低。然而,大小为800x600的图像将具有480k像素,其内存印象将接近4M,没有任何颜色信息。不仅是内存,而且初始化480k个对象也需要太多时间。因此,在这种情况下,我认为这不是一个好主意。但是,如果您使用此类来转换图像的位置,或者将其用于图形基元(线条、曲线、圆等),那么就可以使用。


0

我对上面关于性能和类布局的评论表示同意,并想补充一下关于设计方面还未提及的评论。

我觉得你在超出了 Point 类的真正设计范围,过度使用了它。当然,它可以被用来做这种事情,但是应该吗?

在以前的游戏开发工作中,我经常遇到类似的情况,通常最好的结果是,在进行专门处理时(例如图像处理),为其编写专门的代码集,可以更高效地处理不同布局的缓冲区。

这也使您能够针对重要情况进行性能优化,以更清晰的方式,而不会降低基本代码的可维护性。

理论上,我相信有一种巧妙的方法可以使用复杂的组合模板代码、具体类设计等,实现几乎相同的运行时效率...但我通常不愿意进行复杂的实现交易。


0
成员函数不随对象一起复制。只有数据字段对对象的大小有贡献。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接