【问题标题】:High-performance code in c++ (inheritance, pointers to functions, if)c++ 中的高性能代码(继承、函数指针、if)
【发布时间】:2013-11-14 13:44:46
【问题描述】:

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

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

  • 只需使用“IFs”:如果(条件 X)执行附加操作 X。
  • 使用继承从基类派生四个类(暴露方法OPERATION)以进行正确操作并节省数千万“ifs”。 [但我不确定这是否是真正的节省,继承也必须有它的开销)
  • 使用函数指针根据条件分配函数一次。

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

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

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

【问题讨论】:

  • Related stackoverflow.com/q/11957632/332733 另外这个问题太宽泛了,请缩小到可以回答的范围
  • 看这里:stackoverflow.com/questions/1955074/…函数指针应该比虚拟方法快
  • 您可以通过对您的选择进行基准测试来获得准确的答案。
  • @BlueMonkMN 函数指针或虚函数可能比if 语句慢。通过if 语句,CPU 可以使用分支预测和推测执行。
  • @MaximYegorushkin 如果您担心管道停顿......您应该首先解决所有算法问题。如果事先优化,这个问题很糟糕。

标签: c++ performance inheritance optimization


【解决方案1】:

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

我在处理虚拟功能时测量的机器也很漂亮 好;我听说其他一些机器(例如 HP 的 PA) 在执行间接跳转时非常无效 (不仅包括虚函数调用,还包括返回 从功能上---再次失去了内联的机会 费用)。

【讨论】:

  • BTW,有一个比较新的gcc命令行开关-fdevirtualize-speculatively
  • 如果您没有资源来衡量实际代码,您会怎么做。只选择最易读的方法,直到您可以测试性能?假设它不令人困惑,是否值得猜测一个可能表现更好的解决方案?
  • @BlueMonkMN 使用最自然的方法,直到您知道您有性能问题,并且分析器会告诉您在哪里。 (而且我知道在这种情况下,这是一个尴尬的答案,因为更改策略可能需要重写大块代码。但是,在现实生活条件下,如果不进行测量,就不可能说哪种策略会更快。甚至像每种类型的实例数量会影响结果。)
【解决方案2】:

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

你为什么要这样做:

  • 没有用于状态切换的 ifs
  • 没有 vtables
  • 没有函数间接

但更重要的是:

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

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

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

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

哦,是的,正如詹姆斯所说,总是,总是测量!你认为最快的方法可能不是,有时事情很反直觉,取决于各种因素,如优化、流水线、分支预测、缓存命中/未命中、数据结构布局等。我上面写的是相当通用的方法,但不能保证它是最快的,而且肯定有方法做错了。测量,测量,测量。

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

【讨论】:

    【解决方案3】:

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

    但是,您有四个条件,因此分支未命中可能代价高昂。

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

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

    【讨论】:

      【解决方案4】:

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

      如果我可以用划船做个类比的话,这些就是减轻重量、调整功率、调整平衡和流线型,假设您从一艘已经接近最佳状态的快艇开始。

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

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

      当人们说“测量和使用分析器”时,他们指的是正确的方向,但还不够远。 Here's an example of how I do itI made a crude video of it, FWIW.

      【讨论】:

        【解决方案5】:

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

        上面建议的排序很好,但请注意,它将一个简单的 O(n) 问题转换为 O(logn) 问题,所以对于大尺寸,除非你可以排序一次,否则你会输掉 - 遍历很多次,或者以其他方式廉价地维护排序状态。

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-09-08
          • 1970-01-01
          • 2016-06-24
          • 1970-01-01
          • 2012-04-12
          • 1970-01-01
          相关资源
          最近更新 更多