指针解引用开销vs分支/条件语句

Pointer dereferencing overhead vs branching / conditional statements

本文关键字:条件 语句 分支 vs 引用 开销 指针      更新时间:2023-10-16

在重循环中,例如在游戏应用中发现的循环,可能有许多因素决定执行循环体的哪个部分(例如,角色对象将根据其当前状态进行不同的更新),所以不要这样做:

void my_loop_function(int dt) {
  if (conditionX && conditionY) 
    doFoo();
  else
    doBar();
  ...
}

我习惯使用函数指针指向与字符当前状态相对应的某个逻辑函数,如:

void (*updater)(int);
void something_happens() {
  updater = &doFoo;
}
void something_else_happens() {
  updater = &doBar;
}
void my_loop_function(int dt) {
  (*updater)(dt);
  ...
}

在我不想做任何事情的情况下,我定义了一个虚拟函数,并在需要时指向它:

void do_nothing(int dt) { }

现在我真正想知道的是:我是否不必要地痴迷于这个?上面的例子当然很简单;有时候我需要检查很多变量来确定我需要执行哪段代码,所以我发现使用这些"状态"函数指针确实是更优的,对我来说也是自然的,但是我正在处理的一些人非常不同意。

那么,使用(虚拟)函数指针的收益是否值得,而不是用条件语句填充我的循环来流动逻辑?

Edit:为了澄清指针是如何被设置的,它是通过基于每个对象的事件处理来完成的。当一个事件发生时,比如说,那个字符有自定义逻辑附加到它,它将在该事件处理程序中设置更新指针,直到另一个事件发生,这将再次改变流。

谢谢

函数指针方法让您可以异步转换。不只是将dt传递给updater,还要传递对象。现在更新程序可以自己负责状态转换。这将状态转换逻辑本地化,而不是将其全球化到一个大而丑陋的if ... else if ... else if ...函数中。

至于这种间接的代价,你在乎吗?您可能会关心更新程序是否非常小,以至于解引用和函数调用的成本超过了执行更新程序代码的成本。如果更新程序非常复杂,那么这种复杂性将超过这种增加的灵活性所带来的成本。

我想我会同意这里的非信徒。在这种情况下,金钱问题是如何设置指针值?

如果您可以以某种方式索引到map并生成一个指针,那么这种方法可能通过降低代码复杂性来证明其合理性。然而,这里更像是一个分布在几个函数中的状态机。

考虑到something_else_happens在将指针设置为另一个值之前必须检查指针的前一个值。something_different_happens也是如此。实际上,您已经将状态机的逻辑分散到各处,使其难以遵循。

现在我真正想知道的是:我是否不必要地痴迷于这个?

如果你没有实际运行你的代码,并且发现它实际上运行得太慢,那么是的,我认为你可能过早地担心性能。

Herb Sutter and Andrei Alexandrescu inc++编码标准:101条规则、指导方针和最佳实践在第8章专门讨论了这一点,叫做"不要过早优化",他们总结得很好:

鞭策不甘心的马(拉丁谚语):过早的优化是上瘾的,因为它是无效的。优化的第一条规则是:不要这么做。优化的第二条规则(仅适用于专家)是:先不要做。测量两次,优化一次。

第9章也值得一读:"不要过早悲观"

测试一个条件是:

  • 获取值
  • 比较(减去)
  • 0(或非0)时跳转

执行间接操作是:

  • 获取地址

它可能会更有性能!

实际上,你在另一个地方做了"比较",以决定调用什么。结果是一样的。您所做的不过是一个与编译器在调用虚函数时所做的相同的分派系统。事实证明,在现代编译器上,避免使用虚函数通过交换机实现调度并不能提高性能。

在大多数情况下,"不要使用间接/不要使用虚/不要使用函数指针/不要动态强制转换等"只是基于早期编译器和硬件架构的历史限制的神话。

性能差异取决于硬件和编译器优化器。间接调用在某些机器上可能非常昂贵对别人很便宜。真正好的编译器可能会优化甚至间接调用,基于剖析器的输出。直到你真正在您的实际目标硬件和编译器和编译器选项在最终发布代码中使用,它是不可能。

如果间接调用最终开销太大,您仍然可以提升通过设置enum和使用switch在循环中,或者通过为每个组合实现循环设置,并在开始时选择一次。(如果功能你点实现完整的循环,这几乎是肯定的比每次通过循环测试条件要快,即使)