【问题标题】:Is calling a function (or virtual function) a costly operation in C++在 C++ 中调用函数(或虚函数)是一项代价高昂的操作
【发布时间】:2012-03-21 19:43:03
【问题描述】:

我正在开发一个基于数学(sin、cos、sqrt 等)的应用程序。 这些函数需要一些时间才能运行,但精度很高。

我的一些客户不需要那么高的精度,但他们需要尽可能快。

所以我有我的 Sin 函数,它是一个简单的数组(在程序开始运行之前创建),它取 0 到 360 之间的度数并返回其sin(假设数组有 360 个值)。

我要创建一个界面:

interface MyMath
{
    double PreciseSin(double x);
    double PreciseCos(double x);
}

它将被继承

  1. “精确数学”实现将调用正常的 sin,cos 函数。
  2. “快速数学”将使用我之前解释的数组技巧。

我的代码将使用“mymath”类型的变量来进行计算,并且在开始时它会使用precisionMath 或fastMath 进行初始化。

最后我的问题是:

  1. 调用一个调用“Math.sin”而不是直接调用它的虚函数,我将支付多少时间惩罚?
  2. 编译器是否能够对其进行优化并理解如果我使用 PriciseMath 初始化 MyMath,我只想调用正常的 Sin 和 Cos 函数?
  3. 我可以更改我的设计以帮助编译器理解和优化我的代码吗?

【问题讨论】:

  • 也许继承不适合您的情况。在您决定采用这一技术之前,请先尝试一下其他标准 C++ 技术。
  • 需要客户端能够更改精度还是您可以提供两个不同的二进制文件?
  • 假设我可以提供不同的二进制文件

标签: c++ optimization inheritance virtual


【解决方案1】:

您的 sqrt 和 trig 函数的成本可能会比函数调用高得多,即使它是虚拟的。然而,这看起来是使用模板的理想场所。如果您正确使用它们,您可以完全消除函数调用的运行时间成本,因为它们都可以内联。

class PreciseMath{
    public:
    inline double sin(double sin){
        //code goes here
    }
    inline double cos(double sin){
        //code goes here
    }
    inline double sqrt(double sin){
        //code goes here
    }
};
class FastMath{
    public:
    inline double sin(double sin){
        //code goes here
    }
    inline double cos(double sin){
        //code goes here
    }
    inline double sqrt(double sin){
        //code goes here
    }
};
template<class T>
class ExpensiveOP{
    public:
    T math;
    void do(){
        double x = math.sin(9);
        x=math.cos(x);
        //etc
    }
}
ExpensiveOP<PreciseMath> preciseOp;
ExpensiveOP<FastMath> fasterOp;

【讨论】:

  • 你不应该把“做”也内联吗?
  • 完全没有理由写内联,因为所有函数都隐式内联。
  • @OopsUser 听起来并不重要,do 函数只会被调用几次,但如果你愿意,你可以随时内联它。
  • @ipc 确实如此,但是,尽管大多数优秀的编译器完全忽略了内联语句,但我通常出于两个原因编写它们。第一个是让其他程序员知道它被放在类声明中的原因,第二个是因为有时我无法使用好的编译器。
  • 我不想制作整个函数模板,如果 T 是模板,我该如何编写 math.cos ?我可以通过而不是 T "int" 或其他东西,然后它不会编译
【解决方案2】:

一二:

  1. 您将支付与通过函数指针调用函数相同的金额。这不是很多。

  2. 不,编译器不会将虚函数调用优化为静态函数调用,因为它不知道类型在运行时不会以某种方式改变(比如从一些外部代码中获取指针它什么都不知道)。 Delnan 在 cmets 中告诉我,在非常简单的情况下,例如 A* a = new A; a-&gt;func(),编译器可以看到 a 永远不会是 A,所以它可以执行“去虚拟化”并将虚函数调用优化为静态函数调用。但是,它可以做到这一点的情况很少见,如果你从函数的参数中获取指针,它就无法做到这一点,因为它实际上可能是派生类型。

    李>

除了“编译时虚函数”(又名 CRTP)之外,我不知道有任何设计可以使您的代码比这更快,但是如果您这样做,您将失去多态性。尝试使用虚拟功能并对其进行分析;如果它对您来说太慢,那么您可以尝试另一条路线,但不要浪费时间试图让它更快而不知道它已经有多快。

【讨论】:

  • Re 2:有时,它可以知道(参见去虚拟化,例如Foo *x = new Bar(); ... x-&gt;virtualFunc(),其中... 不会重新分配x)。但是,是的,不是很频繁,也不是在大多数有趣的用例中。
  • @delnan 啊,我不知道去虚拟化。我会更新我的答案,非常感谢。
【解决方案3】:

调用一个调用“Math.sin”而不是直接调用它的虚函数,我将支付多少时间惩罚?

虚拟调用是通过取消引用虚拟表指针,从虚拟表中的适当偏移量获取函数指针并通过该指针调用函数来实现的。

这比静态调用稍微贵一些,但对于“正常”使用来说仍然被认为非常便宜。如果您需要压缩每一次性能下降,请考虑在编译时指定所有类型以允许使用非虚拟函数。

编译器能否对其进行优化并理解如果我使用 PriciseMath 初始化 MyMath,我只想调用正常的 Sin 和 Cos 函数?

如果编译器可以(向自己)证明一个对象在运行时将具有特定类型,那么它将能够发出静态函数调用,即使函数本身被声明为虚拟。

然而,编译器并不能保证足够聪明地真正做到这一点。保证静态调用的唯一方法是使用非虚函数。

我可以更改我的设计以帮助编译器理解和优化我的代码吗?

  • 消除虚拟调用开销:如果不需要在运行时更改实现,则在编译时指定类型并完全停止使用虚拟函数。模板可能是必不可少的通用方式。
  • 消除静态函数调用开销:在头文件中提供函数体,允许编译器内联函数调用。

RTS's answer 很好地说明了这两种技术。)


最后,如果性能对您来说真的很重要,不要只依赖其他人(包括我)的建议 - 始终自己执行测量

【讨论】:

    【解决方案4】:

    如果您可以提供不同的二进制文件,只需执行conditional compilation:

      namespace MyMath {
    #ifdef FAST_MATH
        double sin(double x) { /* do it fast */ }
        double sos(double x) { /* do it fast */ }
    #else
        double sin(double x) { /* do it precise */ }
        double sos(double x) { /* do it precise */ }
    #endif
      }
    

    然后使用-DFAST_MATH 调用您的编译器以生成快速二进制文件,而无需精确二进制文件。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-12-24
      • 1970-01-01
      • 1970-01-01
      • 2014-08-28
      • 2010-11-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多