28

现代 CPU 具有广泛的流水线,也就是说,它们在实际执行指令之前很久就加载了必要的指令和数据。

有时,加载到管道中的数据会失效,必须清除管道并重新加载新数据。重新填充管道所需的时间可能相当长,并导致性能下降。

如果我在 C 中调用函数指针,管道是否足够智能,可以意识到管道中的指针是函数指针,并且它应该跟随该指针以执行下一条指令?还是有一个函数指针会导致管道清理并降低性能?

我在 C 中工作,但我想这在 C++ 中更为重要,因为许多函数调用都是通过 v-tables 进行的。


编辑

@JensGustedt 写道:

要真正影响函数调用的性能,您调用的函数必须非常简短。如果您通过测量代码观察到这一点,您肯定应该重新审视您的设计以允许内联该调用

不幸的是,这可能是我落入的陷阱。

出于性能原因,我编写了小而快的目标函数。

但是它被函数指针引用,因此可以很容易地用其他函数替换它(只需让指针引用不同的函数!)。因为我通过函数指针引用它,所以我认为它不能被内联。

所以,我有一个非常简短的非内联函数。

4

4 回答 4

13

在某些处理器上,间接分支总是会至少清除部分管道,因为它总是会错误预测。对于有序处理器尤其如此。

例如,我在我们开发的处理器上运行了一些计时,比较了内联函数调用、直接函数调用和间接函数调用(虚拟函数或函数指针;它们在这个平台上是相同的)的开销。

我编写了一个很小的函数体,并在数百万次调用的紧密循环中对其进行了测量,以确定调用惩罚的成本。“内联”函数是一个控制组,仅测量函数体的成本(基本上是单个加载操作)。直接函数测量了正确预测的分支的惩罚(因为它是一个静态目标,PPC 的预测器总是可以做到这一点)和函数序言。间接函数测量bctrl间接分支的惩罚。

614,400,000 次函数调用

inline:   411.924 ms  (   2 cycles/call )
direct:  3406.297 ms  ( ~17 cycles/call )
virtual: 8080.708 ms  ( ~39 cycles/call )

如您所见,直接调用比函数体多花费 15 个周期,而虚拟调用(完全等同于函数指针调用)比直接调用多花费 22 个周期。这恰好是在管道开始(指令获取)和分支 ALU 结束之间大约有多少个管道阶段。因此,在此架构上,间接分支(也称为虚拟调用)会在100% 的时间内清除 22 个流水线阶段。

其他架构可能会有所不同。您应该通过直接的经验测量或 CPU 的流水线规格来做出这些决定,而不是假设处理器“应该”预测什么,因为实现是如此不同。在这种情况下,会发生管道清除,因为分支预测器在退出之前无法知道 bctrl 将去哪里。充其量它可以猜测它与最后一个 bctrl 的目标相同,而这个特定的 CPU 甚至不会尝试这种猜测。

于 2012-06-04T23:44:30.043 回答
9

调用函数指针与调用 C++ 中的虚方法没有根本不同,就此而言,它与返回也没有根本不同。向前看,处理器将识别出一个通过指针的分支即将出现,并将决定它是否可以在预取管道中安全有效地解析指针并遵循该路径。这显然比遵循常规的相对分支更加困难和昂贵,但是,由于间接分支在现代程序中如此普遍,因此大多数处理器都会尝试这样做。

正如 Oli 所说,只有在条件分支上出现错误预测时才需要“清除”管道,这与分支是通过偏移量还是通过变量地址无关。但是,处理器可能具有根据分支地址的类型进行不同预测的策略——通常,由于地址错误的可能性,处理器不太可能积极地遵循条件分支的间接路径。

于 2012-05-25T16:38:12.987 回答
3

通过函数指针进行的调用不一定会导致管道清除,但可能会,具体取决于场景。关键是CPU能否提前有效预测分支的目的地。

现代“大”乱序核心处理间接调用1的方式大致如下:

  • 一旦你执行了几次间接分支,间接分支预测器将尝试预测将来发生分支的地址。
  • 第一个间接分支预测器非常简单,只能“预测”单个固定位置。
  • 后来的预测器(包括大多数现代 CPU 上的预测器)要复杂得多,通常能够很好地预测间接跳转的重复模式,并将跳转目标与早期条件或间接分支的方向相关联。
  • 如果预测成功,间接调用的代价与普通直接调用相似,而这个代价很大程度上与其余代码“脱节”(即不参与依赖链),因此对除非调用非常密集,否则代码的最终运行时间可能会很小。
  • 另一方面,如果预测不成功,则会得到完全错误预测,类似于分支方向错误预测。你不能给这种错误预测的成本设定一个固定的数字,因为它取决于周围的代码,但它通常会在前端造成大约 20 个周期的泡沫,并且运行时的总体成本通常以相似的方式结束。

因此,鉴于这些基础知识,我们可以对某些特定场景中发生的情况做出一些有根据的猜测:

  1. 函数指针总是指向同一个函数,几乎总是1可以很好地预测并且成本与常规函数调用大致相同。
  2. 在几个目标之间随机交替的函数指针几乎总是会被错误预测。充其量,我们可以希望预测器总是预测任何最常见的目标,所以在最坏的情况下,目标是在目标之间均匀随机选择N的,预测成功率的范围是1 / N(即,随着 N 趋于无穷大,趋于零)。在这方面,间接分支的最坏情况行为比条件分支更差,条件分支的最坏情况误预测率通常为 50% 2
  3. 具有中间某处行为的函数指针的预测率,例如,有些可预测(例如,遵循重复模式),将在很大程度上取决于硬件的细节和预测器的复杂性。现代英特尔芯片具有相当好的间接预测因子,但细节尚未公开。传统观点认为,他们正在使用TAGE 预测器的一些间接变体,也用于条件分支。

1即使对于单个目标也会发生错误预测的情况包括第一次(或几次)遇到该函数,因为预测器无法预测它尚未见过的间接调用!另外,CPU中预测资源的大小是有限的,所以如果函数指针有一段时间没有被使用,最终预测资源会被其他分支使用,下次调用时会出现错误预测.

2确实,一个非常简单的条件预测器可以简单地预测最近最常见的方向,在完全随机的分支方向上应该有 50% 的预测率。要获得显着低于 50% 的结果,您必须设计一种对抗性算法,该算法基本上对预测器进行建模,并始终选择与模型相反的方向分支。

于 2018-05-27T22:39:52.537 回答
1

除了额外的间接级别之外,函数指针调用和“正常”调用之间没有太大区别。因此可能会涉及更大的延迟;如果目标地址尚未在缓存或寄存器中,则 CPU 在从主内存中检索它时可能必须等待。

所以答案是;是的,管道可能会停止,但这与正常的函数调用没有什么不同。和往常一样,分支预测和乱序执行等机制可以帮助减少损失。

于 2012-05-25T15:46:32.887 回答