【问题标题】:Can vtable overhead be avoided using a static_cast?可以使用 static_cast 避免 vtable 开销吗?
【发布时间】:2012-12-28 23:59:31
【问题描述】:

这是我的问题。我有一个基类和一个派生类,它覆盖了基类中的一些方法。为简单起见,请考虑以下示例:

struct base
{
  virtual void fn()
  {/*base definition here*/}
};

struct derived : base 
{
  void fn()
  {/*derived definition here*/}
};

在我的实际程序中,这些类作为参数传递给其他类并在其他方法中调用,但为了简单起见,让我们创建一个简单的函数,将基类或派生类作为参数。我可以简单地写

void call_fn(base& obj)
{obj.fn();}

由于虚函数,对相应函数的调用将在运行时解决。

但是,我担心如果 call_fn 被调用一百万次(在我的情况下,它会因为我的实际应用程序是一个模拟实验),我会得到很大的开销,我想避免。

所以,我想知道使用 static_cast 是否真的可以解决这个问题。也许是这样的:

template <typename T>
void call_fn(base& obj)
{(static_cast<T*>(&obj))->fn();}

在这种情况下,函数调用将作为call_fn&lt;base&gt;(obj) 调用基方法或call_fn&lt;derived&gt;(obj) 调用派生方法。

此解决方案会避免 vtable 开销还是会受到影响?提前感谢您的任何回复!

顺便说一下,我知道 CRTP,但不是很熟悉。这就是为什么我想先知道这个简单问题的答案:)

【问题讨论】:

  • 您对重大开销的定义是什么?您可能会对调用数百万次虚函数的开销感到惊讶。
  • 您是否真的证明了 vtable 开销是您的代码的问题?如果是这样,你能证明做相关的“如果(这个类)做这个,否则做那个”更快吗?我怀疑不是,除非它使编译器内联函数,这样可以节省很多精力。
  • 我还没有证明...这就是为什么我想首先创建一个避免使用 vtable 的方法,以便我可以比较两者并更清楚地了解开销可能有多大是:)
  • @linuxfever:动态调度的成本大致是额外的间接性。 IE。对于非动态函数调用,编译器会将跳转 (call) 注入到固定地址。在虚函数的情况下,它将通过 vtable 执行间接调用。许多处理器都有特定的指令来计算地址(lea 和类似的......)。除非您的函数基本上是空的,否则函数实际主体的成本将推动操作的总体成本。

标签: c++ vtable overhead static-cast


【解决方案1】:

此解决方案会避免 vtable 开销还是仍会受到影响?

它仍然会使用动态调度(是否会导致任何明显的开销是一个完全不同的问题)。您可以通过限定函数调用来禁用动态调度,如下所示:

static_cast<T&>(obj).T::fn();

虽然我什至不会尝试这样做。离开动态调度,然后测试应用程序的性能,做一些分析,做进一步的分析。再次分析以确保您了解分析器告诉您的内容。只有到那时,才考虑进行一次更改并再次配置文件以验证您的假设是否正确。

【讨论】:

  • +1 这就是答案。如果您完全确定过多的 CPU 是大型循环中的一个小虚拟功能并且您无法正确修复它,那么这是一种有效的技术。它将for (...) { (vtbl-&gt;foo)(); } 转换为if (a) { for (...) A::foo(); } else { for (...) B::foo(); }。不太可能有太大的不同。
  • @linuxfever 如果它不是围绕 1 行左右的虚函数的紧密循环,那么 vtbl 调用开销将可以忽略不计。紧密循环的好处是分支预测适用于现代(优于大约 1997 年的 AMD K6)CPU 上的间接调用。假设您的编译器没有生成太多使用参数传递的代码,它会很快。 C++ 使用 vtbls 是因为它们是调用未知事物的最快方式。
  • @linuxfever 另一方面,如果编译器内联直接(非虚拟)调用,它可能能够将大量公共子表达式提升出循环。
【解决方案2】:

这并不是您实际问题的真正答案,但我很好奇“调用虚函数与​​调用常规类函数的开销究竟是什么”。为了让它“公平”,我创建了一个 classes.cpp,它实现了一个非常简单的功能,但它是一个在“main”之外编译的单独文件。

classes.h:

#ifndef CLASSES_H
#define CLASSES_H

class base
{
    virtual int vfunc(int x) = 0;
};

class vclass : public base
{
public:
    int vfunc(int x);
};


class nvclass
{
public:
    int nvfunc(int x);
};


nvclass *nvfactory();
vclass* vfactory();


#endif

classes.cpp:

#include "classes.h"

int vclass:: vfunc(int x)
{
    return x+1;
}


int nvclass::nvfunc(int x)
{
    return x+1;
}

nvclass *nvfactory()
{
    return new nvclass;
}

vclass* vfactory()
{
    return new vclass;
}

这是从以下位置调用的:

#include <cstdio>
#include <cstdlib>
#include "classes.h"

#if 0
#define ASSERT(x) do { if(!(x)) { assert_fail( __FILE__, __LINE__, #x); } } while(0)
static void assert_fail(const char* file, int line, const char *cond)
{
    fprintf(stderr, "ASSERT failed at %s:%d condition: %s \n",  file, line, cond); 
    exit(1);
}
#else
#define ASSERT(x) (void)(x)
#endif

#define SIZE 10000000

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


void print_avg(const char *str, const int *diff, int size)
{
    int i;
    long sum = 0;
    for(i = 0; i < size; i++)
    {
    int t = diff[i];
    sum += t;
    }

    printf("%s average =%f clocks\n", str, (double)sum / size);
}


int diff[SIZE]; 

int main()
{
    unsigned long long a, b;
    int i;
    int sum = 0;
    int x;

    vclass *v = vfactory();
    nvclass *nv = nvfactory();


    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();

    x = 16;
    sum+=x;
    b = rdtsc();

    diff[i] = (int)(b - a);
    }

    print_avg("Emtpy", diff, SIZE);


    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();

    x = 0;
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    x = v->vfunc(x);
    ASSERT(x == 4); 
    sum+=x;
    b = rdtsc();

    diff[i] = (int)(b - a);
    }

    print_avg("Virtual", diff, SIZE);

    for(i = 0; i < SIZE; i++)
    {
    a = rdtsc();
    x = 0;
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    x = nv->nvfunc(x);
    ASSERT(x == 4);     
    sum+=x;
    b = rdtsc();
    diff[i] = (int)(b - a);
    }
    print_avg("no virtual", diff, SIZE);

    printf("sum=%d\n", sum);

    delete v;
    delete nv;

    return 0;
}

代码的真正区别在于: 虚拟通话:

40066b: ff 10                   callq  *(%rax)

非虚调用:

4006d3: e8 78 01 00 00          callq  400850 <_ZN7nvclass6nvfuncEi>

结果:

Emtpy average =78.686081 clocks
Virtual average =144.732567 clocks
no virtual average =122.781466 clocks
sum=480000000

请记住,这是每个循环 16 次调用的开销,因此调用函数和不调用函数之间的差异是每次迭代大约 5 个时钟周期 [包括将结果和所需的其他处理相加],并且虚拟调用增加了每次迭代 22 个时钟,因此每次调用大约 1.5 个时钟。

我怀疑你会注意到,假设你在你的函数中做了一些比 return x + 1 更有意义的事情。

【讨论】:

  • +1 不错的分析,尽管您应该在“真正的差异”中也包含查找。如果您显示的两行实际上是全部差异,那么虚拟实现不会更慢!
  • +1,还要注意,在实际代码中,函数可能不止一次加法,调用的差异会比函数的成本低很多。
  • 这两行的区别在于,一个使用间接寻址*(%rax)的方法,另一个使用直接寻址400850。如果它的调用频率比紧密循环中的调用频率低,则需要更多的工作来查找 vtable 等,但是您还有更多其他代码需要担心它对调用的影响。
  • 我真正应该添加的是“if (a) use_cast_to_get_rid_of_volatile(virtual_a) else use_cast_to_get_rid_of_volatile(virtual_b);”并将其与交替调用 virtual_a 和 virtual_b 进行比较。
【解决方案3】:

VTable 位于您的类中。如果您有虚拟成员,他们将通过 VTable 访问。强制转换不会影响 VTable 是否存在,也不会影响成员的访问方式。

【讨论】:

  • 不正确。如果编译器在运行时知道变量的静态类型,它可以直接调用函数而不是通过 vtable。虚函数只能与指针和引用一起使用。
  • @MarkRansom 所有对虚函数的调用都通过 vtbl,无论编译器是否“知道”类型。全局优化器可以优化该场景,但除非它正在执行配置文件引导的优化,否则它可能不会那么激进。通过绕过 vtbl 调用获得性能的关键是内联。除非编译器确定大多数调用是针对某种类型的,否则它可能只会进行间接调用。
  • @doug65536,查看生成的汇编程序以调用局部变量(不是指针或引用)上的虚函数,然后告诉我你看到了什么。我同意避免使用 vtable 可能不会买太多。
  • @MarkRansom 通常,如果您有一个虚函数,那么您在任何地方都使用基类型指针并且它必须是虚拟的(基类型具有该方法的纯虚拟声明)。不过你是对的,编译器可以知道确切的类型,比如真实类型的局部变量。
  • 是的,我最初关于所有呼叫都是虚拟的断言是错误的。我把我通常做的事情和你能做的事情搞混了。
【解决方案4】:

如果您有一个多态数组,其中元素是多态的,但所有元素都具有相同的类型,您也可以将 vtable 外部化。这允许您查找该函数一次,然后直接在每个元素上调用它。在这种情况下,C++ 对您没有帮助,您必须手动完成。

如果您正在对事物进行微优化,这也很有用。我相信Boost的功能使用了类似的技术。它只需要 vtable 中的两个函数(调用和释放引用),但编译器生成的一个还包含 RTTI 和其他一些东西,这可以通过手动编写一个只有这两个函数指针的 vtable 来避免。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多