【问题标题】:Does C++ compiler optimize virtual member calls?C++ 编译器是否优化虚拟成员调用?
【发布时间】:2019-04-29 13:14:27
【问题描述】:

我正在考虑创建一个新的大型 C++ 项目。开始很容易 - 只是一个简单的窗口,可能是 SDL2,可能是 SFML,甚至可能是 WIN32。嗯,我应该拿什么?使用我想要的任何窗口不是更好吗?不用修改太多代码让其他类独立于这个窗口?

说完了!使用简单的窗口界面,每个类都知道像窗口这样的东西,我可以在不同的类型之间进行选择。唯一的要求是将 IWindow 作为基类。

class IWindow {
  public:
    IWindow(std::string title, int posX, int posY, int width, int height);
    IWindow getHandle();
    void loop();
    bool toggleFullscreen();
    bool toggleFullscreen(bool fullscreen);
    int getWidth();
    int getHeight();
    int getPosX();
    int getPosY();
    //And so on ...
};

但是现在,由于我必须使用虚拟方法,所以每次我的虚拟函数循环都会被游戏循环调用。并且虚函数更慢。大约 10%,我读过。

编译器不能看到我的窗口是什么吗?它将来自哪种类型?难道它看不到“Jeah,这个程序员在这个应用程序中创建了一个 SDL 窗口,所以在任何地方都使用它的方法。”?我的意思是,我在主循环期间定义我的窗口,它永远不会改变。这不是动态的。这是可以预见的。

那么编译器是否能够优化我的可预测虚函数调用?这些将在每个游戏循环周期中进行评估?就像下面这段话一样?

int main(int argc, char* argv[]) {
  //Creates a window derived from IWindow
  SDL::Window myWindow("Title", 0, 0, 300, 100);
  //Storing it as IWindow in a wrapper class
  Game myGame(&myWindow);
  //Game loop
  //myGame.run() calls the window's loop
  while (myGame.run()) {
    //... doing game stuff
  }
}

使用这样的游戏类:

class Game {
  protected:
    IWindow* window;
  public:
    bool run() {
      //Calls the window's virtual loop method.
      //Will it be optimized? Any way to do so?
      this->window->loop();
    }
};

很高兴听到您的想法和经验。

达斯月亮

【问题讨论】:

  • 如果编译器可以确定你的对象是给定类型的,那么它可以去虚拟化。但在你的第二种情况下,不,它不能。不过,TLB 将能够预测它,因此您不会因为预测保留在其表中而承担全部成本。
  • TLB?什么意思?
  • 对于第一种情况(main),是的,它可以去虚拟化。第二,它很可能不能。由于IWindow 是一个高级对象,我不会担心这一点。
  • @DarthMoon 不,您的游戏不需要尽可能快。可能有大约 0 个程序尽可能快。
  • @DarthMoon:这几乎不会花费您任何费用,这就是原因。一个简单的 getter 内联函数会比一个虚函数的成本低很多(对于一个简单的 getter 函数,它不仅仅是 10%,而是更多),这是毫无疑问的。但是,如果这个函数只被调用~10 次/帧,那么就没有关系了。

标签: c++ inheritance code-generation compiler-optimization virtual-functions


【解决方案1】:

让我们总结一下我们的cmets。

虚拟调用成本很高,但如果处理器能够检测到某种模式,由于现代处理器内部的预测器,它们的调用成本会降低。

现在,让我们检查您的代码:

int main(int argc, char* argv[]) {
  //Creates a window derived from IWindow
  SDL::Window myWindow("Title", 0, 0, 300, 100);
  //Storing it as IWindow in a wrapper class
  Game myGame(&myWindow);
  //Game loop
  //myGame.run() calls the window's loop
  while (myGame.run()) {
    //... doing game stuff
  }
}

假设Game 有一个虚拟的run。在这种情况下,编译器知道myGameGame 类型,并且可以直接调用run 函数而不是通过虚拟表。

现在你在另一个文件中有这个:

class Game {
  protected:
    IWindow* window;
  public:
    bool run() {
      //Calls the window's virtual loop method.
      //Will it be optimized? Any way to do so?
      this->window->loop();
    }
};

不幸的是,在这种情况下,编译器通过查看此文件一无所知,因此对SDL::Window 的调用将通过来自IWindow 的虚拟run

现在使用lto(链接时间优化),编译器可能能够解决它并对代码进行去虚拟化,但它可能不会,因为优化选项的数量也会随着文件数量的增加而增加作为组合的数量。

【讨论】:

  • TLB 与这个问题关系不大。您也可以在非虚拟呼叫中获得 TLB 未命中。 值得一提的是分支预测器、堆栈引擎和 uop 缓存。
【解决方案2】:

C++ 编译器是否优化了虚拟成员调用?

是的,如果编译器可以在编译时确定具体类型,则编译器可能能够对虚函数调用进行去虚拟化。

不,C++ 编译器将无法对所有虚函数调用进行去虚拟化。

而且虚函数更慢。约 10%

假设 10% 的差异是正确的,请考虑函数调用开销可能在几纳秒的数量级。几纳秒的 10% 并不是很多。您可以在像游戏这样的软实时模拟的单次迭代中适应许多纳秒。

编译器不能看到我的窗口是什么吗?

那么编译器是否能够优化我的可预测虚函数调用?

也许吧。

首先,调用run 必须在分配指针的上下文中内联展开。否则它不能对指向的对象做出任何假设。为了内联扩展,它必须在与调用函数的位置相同的翻译单元中定义(除了 LTO 可能能够解除此要求)。

此外,编译器必须能够证明window 在执行期间的任何时候都没有被修改以指向另一个对象。根据你的循环的样子,这个证明可能是不可能的,但是有一个简单的方法可以让它变得简单:声明指针 const。

至于你的编译器是否优化它...我不知道。但是您的编译器可以,所以我建议将您的问题指向您的编译器(即要求它编译您的程序并查看它的作用)。

【讨论】:

    猜你喜欢
    • 2020-10-19
    • 1970-01-01
    • 2021-03-19
    • 1970-01-01
    • 2015-06-16
    • 2018-11-21
    • 1970-01-01
    • 2017-06-26
    相关资源
    最近更新 更多