C++ 中的高性能代码(继承、指向函数的指针、if)



假设你有一个非常大的图,它的节点上有很多处理(比如每个节点几十亿次操作)。每个节点的核心例程相同,但根据内部条件还有一些额外的操作。可以有 2 个这样的条件,产生 4 种情况 (0,0)、(1,0)、(0,1)、(1,1)。例如(1,1)表示两个条件都成立。条件在程序中建立一次(每个节点独立设置一组),从那时起,永远不会改变。不幸的是,它们是在运行时以完全不可预测的方式确定的(基于通过HTTP从外部服务器接收的数据)。

在这种情况下最快的是什么?(考虑到我不知道的现代编译器优化)

  • 只需使用"IF":如果(条件 X)执行附加操作 X。
  • 使用继承从基类派生四个类(公开方法 OPERATION)以获得适当的操作并节省数十万的"ifs"。[但我不确定这是否是真正的储蓄,继承也必须有其开销)
  • 使用指向函数的指针根据条件分配函数一次。

我需要很长时间才能达到可以自己测试它的地步(我还没有这么大的数据,这将集成到更大的项目中,因此测试所有版本并不容易)。

阅读答案:我知道我可能必须尝试一下。但除此之外,这是一个更快的问题:

数十万的 IF 语句和正常的静态已知函数调用 VS 函数指针调用 VS 继承,我认为在这种情况下这不是最好的主意,我正在考虑将其从进一步检查中消除 感谢您的任何建设性回答(不是说我不应该关心这些小事;)

除了测量实际代码之外,没有真正的答案真实数据。 有时,在过去,我不得不处理这样的问题,在我实际测量的情况下,虚拟的函数比 if 的更快。 但这并没有多大意义,因为我测量的案例在不同的程序中(因此与你的上下文不同。 例如,虚拟函数调用通常会阻止内联,而 if 是本质上是内联的,内联可能会打开额外的优化可能性。

此外,我在处理虚拟功能上测量的机器也很漂亮井;我听说其他一些机器(例如惠普的 PA)在实施间接跳转方面非常无效(不仅包括虚函数调用,还包括返回从功能---再次,失去内联的机会成本)。

如果你绝对必须有最快的方法,并且节点的处理顺序不相关,那么制作四种不同的类型,每种情况一种,并为每个类型定义一个流程函数。然后在容器类中,有四个向量,每个节点类型一个。创建新节点后,获取创建节点所需的所有数据,包括条件,并创建正确类型的节点并将其推送到正确的向量中。然后,当您需要处理所有节点时,逐种处理它们,首先处理第一个向量中的所有节点,然后处理第二个向量,依此类推。

为什么要这样做:

  • 状态切换没有 ifs
  • 没有虚拟桌
  • 无函数间接寻址

但更重要的是:

  • 没有指令缓存抖动(您不会为每个下一个节点跳转到代码的不同部分)
  • 状态切换 ifs 没有分支预测遗漏(因为没有)

即使你继承了虚函数,从而通过vtables间接地对节点进行了继承,简单地在向量中按节点的类型对节点进行排序可能已经对性能产生了天有独钟,因为任何可能的指令缓存抖动基本上都会消失,并且根据分支预测的方法,分支预测的失误也可以减少。

另外,不要制作指针向量

,而是制作对象向量。如果它们是指针,您有一个额外的寻址间接寻址,这本身并不那么令人担忧,但问题是,如果对象几乎随机分布在整个内存中,则可能会导致数据缓存抖动。另一方面,如果你的对象直接放入向量中,处理基本上会线性地通过内存,缓存基本上每次都会命中,缓存预取实际上可能能够做得很好。

请注意,如果您做得不正确,您将在数据结构创建中付出高昂的代价,但如果可能的话,当让向量立即为所有节点保留足够的容量时,每次向量空间不足时重新分配和移动可能会变得昂贵。

哦,是的,正如詹姆斯提到的,总是,总是测量!您认为最快的方法可能不是,有时事情非常违反直觉,具体取决于各种因素,例如优化,流水线,分支预测,缓存命中/未命中,数据结构布局等。我上面写的是一种非常通用的方法,但它不能保证是最快的,而且肯定有办法做错。测量,测量,测量。

附言虚函数的继承大致相当于使用函数指针。虚函数通常由类头的 vtable 实现,它基本上只是一个函数指针表,指向对象实际类型的给定虚拟实现。ifs 是否比虚拟更快,或者相反是一个非常非常难以回答的问题,完全取决于所使用的实现、编译器和平台。

实际上,

我对分支预测的有效性印象深刻,只有 if 解决方案允许内联,这也可能是戏剧性的。 虚拟函数和指向函数的指针也涉及从内存加载,并可能导致缓存未命中

但是,您有四个条件,因此分支未命中可能会很昂贵。

没有测试和验证的能力,答案真的无法回答。 特别是因为甚至不清楚这是否是一个足以保证优化工作的性能瓶颈。

在这种情况下。我会在可读性和易于调试方面犯错误,并使用 if

许多程序员上课并阅读了关于某些最喜欢的主题的书籍:流水线、缓存、分支预测、虚函数、编译器优化、大 O 算法等,以及这些主题的性能。

如果我能比作划船,这些事情包括修剪重量、调整功率、调整平衡和流线型,假设你从一艘已经接近最佳的快艇开始。

没关系,您可能实际上从玛丽皇后号开始,并且您假设这是一艘快艇。

很可能

有一些方法可以通过很大因素来加速代码,只要你知道它在哪里,就可以减少脂肪(伪装成好的设计)。好吧,你不知道它在哪里,猜测在哪里是浪费时间,除非你重视错误。

当人们说"测量和使用剖析器"时,他们指向正确的方向,但还不够远。这是我如何做到这一点的一个例子我制作了一个粗略的视频,FWIW。

除非这些属性有明确的模式,否则不存在可以有效地为您预测此数据相关条件的分支预测器。在这种情况下,您最好避免控制推测(并支付分支错误预测的代价),只需等待实际数据到达并解决控制流(更有可能使用虚函数发生)。当然,您必须进行基准测试以验证这一点,因为它取决于实际模式(例如,如果您甚至有一小组类似的"标记"元素)。

上面建议的排序很好,但请注意,它将一个普通的 O(n) 问题转换为 O(logn) 问题,因此对于大尺寸,除非您可以排序一次 - 遍历多次,或者廉价地维护排序状态,否则您将丢失。

请注意,某些预测器也可能尝试预测函数调用的地址,因此您可能在那里遇到相同的问题。

但是,我必须同意有关早期优化的评论 - 您确定控制流是您的瓶颈吗?如果从内存中获取实际数据需要更长的时间怎么办?一般来说,你的元素似乎可以并行处理,所以即使你在单个线程上运行它(如果你使用多个内核,则更多) - 你应该是带宽限制而不是延迟限制。

最新更新