C++ 虚函数基准测试

问题描述 投票:0回答:1

我认为每个 C++ 程序员都曾听说过“虚拟函数很慢”这句话。因此,我决定将虚拟函数与常规成员函数进行基准测试。

不幸的是,我在对“低级”C++ 代码进行基准测试方面没有太多经验,并且不知道如何正确避免编译器或 HW(主要是分支预测器)优化效果。

我现在面临的问题是,当我运行基准测试(我在下面提供)时,我几乎总是得到调用虚拟函数和常规函数具有相同速度的结果,有时虚拟函数工作得更快!

这就是我使用

google/benchmark
库编写基准测试的方式:

static void BM_MemberCall(benchmark::State& state) {
    Base* b = new Derived();

    for ([[maybe_unused]] auto _ : state) {
        benchmark::DoNotOptimize(b->Foo());
        benchmark::ClobberMemory();
    }
}
BENCHMARK(BM_MemberCall);

static void BM_VirtualCall(benchmark::State& state) {
    Base* b = new Derived();

    for ([[maybe_unused]] auto _ : state) {
        benchmark::DoNotOptimize(b->VirtualFoo());
        benchmark::ClobberMemory();
    }
}
BENCHMARK(BM_VirtualCall);

我的类定义如下,在

util.h
中声明:

#pragma once

// generate 10 virtual functions
#define VFOO(NAME, ID, ...) \
    __attribute__((noinline)) virtual int NAME ## ID ## _0() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _1() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _2() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _3() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _4() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _5() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _6() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _7() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _8() __VA_ARGS__; \
    __attribute__((noinline)) virtual int NAME ## ID ## _9() __VA_ARGS__;

// generate 100 virtual functions
#define VFOO100(NAME, ...) \
    VFOO(NAME, 0, __VA_ARGS__) \
    VFOO(NAME, 1, __VA_ARGS__) \
    VFOO(NAME, 2, __VA_ARGS__) \
    VFOO(NAME, 3, __VA_ARGS__) \
    VFOO(NAME, 4, __VA_ARGS__) \
    VFOO(NAME, 5, __VA_ARGS__) \
    VFOO(NAME, 6, __VA_ARGS__) \
    VFOO(NAME, 7, __VA_ARGS__) \
    VFOO(NAME, 8, __VA_ARGS__) \
    VFOO(NAME, 9, __VA_ARGS__)

struct Base {
    int Foo();
    VFOO100(A);
    __attribute__((noinline)) virtual int VirtualFoo();
    VFOO100(Z);
};

struct Derived : public Base {
    VFOO100(A, override);
    __attribute__((noinline)) int VirtualFoo() override;
    VFOO100(Z, override);
};

VFOO100
的帮助下,我生成了 200 个虚拟函数,以便使虚拟表变大,并且为了防止在寻找正确的方法时涉及到一些词汇排序,我生成了 100 个词汇顺序低于
VirtualFoo
和 100 个更大的方法.

所有方法定义都是相同的(常规方法和虚拟方法,在名为

util.cpp
的单独 TU 中定义):

{
    asm("");
    return 0;
}

基准测试结果示例:

(same)
---------------------------------------------------------
Benchmark               Time             CPU   Iterations
---------------------------------------------------------
BM_MemberCall        1.23 ns         1.23 ns    524344569
BM_VirtualCall       1.23 ns         1.23 ns    564752961

(virtual functions are worse)
---------------------------------------------------------
Benchmark               Time             CPU   Iterations
---------------------------------------------------------
BM_MemberCall        1.22 ns         1.22 ns    522068585
BM_VirtualCall       1.48 ns         1.33 ns    557440234

(virtual functions are faster!)
Benchmark               Time             CPU   Iterations
---------------------------------------------------------
BM_MemberCall        1.33 ns         1.33 ns    475669505
BM_VirtualCall       1.25 ns         1.25 ns    555128195

我的问题:

  1. 如果虚拟函数确实比常规函数慢一次,我该如何测量并亲自查看?我的方法犯了什么错误?
  2. 为什么在这个测试中我有时会发现虚拟函数比常规函数慢?

我的设置:

2024-02-12T17:41:29+03:00
Running ./src/apps/bench/function_calls/calls
Run on (12 X 2600 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB
  L1 Instruction 32 KiB
  L2 Unified 256 KiB (x6)
  L3 Unified 12288 KiB
Load Average: 3.04, 4.39, 4.52
c++ benchmarking virtual-functions microbenchmark
1个回答
0
投票

C++ 中的虚拟函数在某些方面导致速度缓慢。

  1. 它们可以阻止内联。虚函数表比直接函数调用更复杂。通过直接函数调用,知道您正在调用什么代码是很简单的;有时您需要 LTO 来跨翻译单元。对于 vtable,无害的更改可能会阻止去虚拟化。这很难分析,因为根据定义,您的简单基准很简单。

这类似于函数指针或 std 函数。

  1. 他们通过函数指针添加内存访问。函数指针只需一步即可编写代码:虚拟调用首先必须获取 vtable,然后跟随指针到达那里。这种额外的内存访问可能会导致缓存未命中,并会增加 L1 缓存压力。

  2. 它们不鼓励值类型和本地存储。由于基于 vtable 的多态性不支持开箱即用的多态存储,因此您最终会使用指向对象数据的指针,这些数据未以缓存友好的方式彼此相邻存储。

  3. 他们鼓励动态分配。

    malloc
    是 100 条指令,
    free
    是线程不友好的。 vtable 的大多数用途都使用堆。这意味着每个 objexta 的存在都背负着巨大的时间债:通常你有大物体或浪费大量时间。

  4. 它们不鼓励多态性。每个不同类型的对象都有 O(vtable size) 全局开销:并且调整了 1 个方法的对象有自己的类型。考虑到 C++ 如何实现虚拟调度,创建数百万个组合不同类型的对象是不切实际的。因此,在某个点之后,您最终会在多态代码中出现 switch 语句。

这些都代表了我在具体的商业应用程序中出于性能原因而没有使用虚拟调度的原因。

现在,很多时候,性能的欠缺是相对的。例如,不要对以 30 Hz 运行的高清视频使用基于每像素每帧的虚拟调度。

相反,将虚拟调度提升到每个扫描线点。突然之间,这个巨大的开销变得不那么重要了 1000 倍。

© www.soinside.com 2019 - 2024. All rights reserved.