【问题标题】:Avoiding virtual function calls in numerical C++避免数字 C++ 中的虚函数调用
【发布时间】:2014-04-03 23:37:34
【问题描述】:

我正在用 C++ 编写一些数值模拟代码。在这个模拟中,有些东西是“局部的”,在二维网格上的每个点都有一个浮点值,而另一些是“全局的”,只有一个全局浮点值。

除了这种差异之外,这两种类型的对象的行为相似,因此我希望能够拥有一个包含这两种类型的对象的数组。但是,因为这是一个数值模拟,所以我需要以以下方式执行此操作:(a) 尽可能避免虚函数调用开销,并且 (b) 允许编译器尽可能多地使用优化 - 特别是,允许编译器在可能的情况下进行 SIMD 自动矢量化。

目前我发现自己正在编写这样的代码(我现在意识到,这实际上不会按预期工作):

class Base {};

class Local: public Base {
public:
    float data[size];
    // plus constructors etc.
};

class Global: public Base {
public:
    float data;
    // ...
};

void doStuff(Local a, Local b) {
    for (int i; i<size; ++i) {
        a.data[i] += b.data[i];
    }
}

void doStuff(Local a, Global b) {
    for (int i; i<size; ++i) {
        a.data[i] += b.data;
    }
}

void doStuff(Global a, Local b) {
    for (int i; i<size; ++i) {
        a.data += b.data[i];
    }
}

void doStuff(Global a, Global b) {
    a.data += b.data*size;
}

我的代码比这复杂一点——数组是二维的,有几个doStuff 类型的函数有三个而不是两个参数,所以我必须为每个函数编写八个特化。

这不能按预期工作的原因是doStuff 的参数类型在编译时实际上并不知道。我想要做的是拥有一个Base * 数组并在其两个成员上调用doStuff。然后,我希望针对其参数的特定类型调用 doStuff 的正确专业化。 (doStuff 中是否涉及虚拟方法调用并不重要——我只是想在内部循环中避免它们。)

这样做而不是(例如)重载operator[] 的重点是编译器可以(希望)对doStuff(Local, Local)doStuff(Local, Global) 进行SIMD 自动矢量化,我可以完全失去循环doStuff(Global, Global)。也许在这些函数中也可能发生其他编译器优化。

但是,不得不编写这样重复的代码很烦人。因此,我想知道是否有办法使用模板来实现这一点,这样我就可以只编写一个函数doStuff(Base, Base) 并生成与上述等效的代码。 (我希望 gcc 足够聪明,可以在 doStuff(Global, Global) 的情况下优化掉循环。)

我强调以下解决方案不是我正在寻找的,因为它涉及在循环的每次迭代中调用虚函数,这会增加开销并且可能会阻止许多编译器优化.

class Base {
    virtual float &operator[](int) = 0;
};

class Local: public Base {
    float data[size];
public:
    float &operator[](int i) {
        return data[i];
    }
    // …
};

class Global: public Base {
    float data;
public:
    float &operator[](int i) {
        return data;
    }
    // ...
};

void doStuff(Base a, Base b) {
    for (int i; i<size; ++i) {
        a[i] += b[i];
    }
}

我想实现与上述类似的效果,但没有通过内部循环在每次迭代时调用虚函数的开销。 (除非我完全错了,编译器实际上可以优化掉所有的虚函数调用并生成类似上面的代码。在这种情况下,告诉我这个可以为我节省很多时间!)

我确实看过CRTP,但由于doStuff 的多个重载参数,如何使其适应这种情况并不明显,至少对我来说不是。

【问题讨论】:

  • 对我来说听起来像是模板的工作。
  • @EJP 我也是!这就是为什么我在问题中这么说的原因。但对我来说,如何使用模板来做到这一点一点都不明显。

标签: c++ templates optimization


【解决方案1】:

你几乎已经有了答案。像这样的模板函数应该可以工作(虽然我不知道size 来自哪里):

template<typename A, typename B>
void doStuff(A & a, B & b) {
    for (int i; i<size; ++i) {
        a[i] += b[i];
    }
}

这里有一个重载的operator[],但它不是虚拟的。


如果你在调用时不知道你有什么类型,但你有固定数量的派生类型,那么创建静态调度是一种选择

void doStuff( Base & a, Base & b ) {
    Local * a_local = dynamic_cast<Local*>(&a);
    Global * a_global = dynamic_cast<Global*>(&a);
    //same for b
    if( a_local && b_local ) {
        doStuffImpl(*a, *b); {
    } else if( a_local && b_global ) {
        doStuffImpl(*a, *b):
    } ...
}

您会注意到 if 块中的代码对于每个条件都是相同的,假设 doStuffImpl 是一个模板函数。我建议将其包装在一个宏中以减少代码开销。您可能还希望自己跟踪类型而不使用dynamic_cast。在您的 Base 类中有一个明确列出类型的枚举。这是一种安全机制,基本上可以防止未知派生类出现在doStuff

不幸的是,这种方法是必需的。这是从动态类型转换为静态类型的唯一方法。如果您想使用模板,则需要静态模板。

【讨论】:

  • 如果 A 和 B 的类型在编译时已知,这将正常工作,但在我的情况下它们不是。我意识到这个问题不是很清楚 - 我会编辑它。
  • size 只是在某个标头中定义的常量。)
  • 派生类型的数量是恒定的还是无限的?也就是说,你能知道doStuff中所有可能的静态类型吗?
  • 常量 - 据我所知,只有两种派生类型,全局和本地。
  • 感谢更新版本。在这种情况下,似乎应该有一些巧妙的方法来避免使用模板的重复。我不太明白怎么做,但我已将其作为另一个问题发布:stackoverflow.com/questions/22139344/…
【解决方案2】:

您的代码中是否可以让Local 了解GlobalGlobal 了解Local

如果上述问题的答案是肯定的,您可以通过一个虚函数调用和一对动态转换来避免域中每个点的虚函数成本。

class Base {
    public:
       virtual void doStuff(Base& b) = 0;
};

class Local: public Base {
    public:
       virtual void doStuff(Base& b);
       float data[size];
       // plus constructors etc.
};

class Global: public Base {
    public:
       virtual void doStuff(Base& b);
       float data;
       // ...
};

void Local::doStuff(Base& b) {
    Local* lb = NULL;
    Global* gb = NULL;
    if ( NULL != (lb = dynamic_cast<Local*>(&b)) )
    {
       // Do Local+Local stuff.
    }
    else if ( NULL != (gb = dynamic_cast<Global*>(&b)))
    {
       // Do Local+Global stuff.
    }

}

void Global::doStuff(Base& b) {
    Local* lb = NULL;
    Global* gb = NULL;
    if ( NULL != (lb = dynamic_cast<Local*>(&b)) )
    {
       // Do Global+Local stuff.
    }
    else if ( NULL != (gb = dynamic_cast<Global*>(&b)))
    {
       // Do Global+Global stuff.
    }

}

void doStuff(Base a, Base b) {
    a.doStuff(b);
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-15
    • 2014-06-10
    • 2013-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-30
    • 1970-01-01
    相关资源
    最近更新 更多