我知道基准测试是一个非常微妙的主题,简单、没有经过深思熟虑的基准测试对于性能比较来说大多没有意义,但我现在拥有的实际上是一个很小、很做作的例子,我认为应该很容易解释。所以,即使这个问题看起来毫无帮助,它至少会帮助我理解基准测试。
所以,我来了。
我尝试在C中使用简单的API设计,通过void *
使用运行时多态性行为。然后我将它和在C++中使用常规虚拟函数实现的相同东西进行了比较。这是代码:
#include <cstdlib>
#include <cstdio>
#include <cstring>
int dummy_computation()
{
return 64 / 8;
}
/* animal library, everything is prefixed with al for namespacing */
#define AL_SUCCESS 0;
#define AL_UNKNOWN_ANIMAL 1;
#define AL_IS_TYPE_OF(animal, type)
strcmp(((type *)animal)->animal_type, #type) == 0
typedef struct {
const char* animal_type;
const char* name;
const char* sound;
} al_dog;
inline int make_dog(al_dog** d) {
*d = (al_dog*) malloc(sizeof(al_dog));
(*d)->animal_type = "al_dog";
(*d)->name = "leslie";
(*d)->sound = "bark";
return AL_SUCCESS;
}
inline int free_dog(al_dog* d) {
free(d);
return AL_SUCCESS;
}
typedef struct {
const char* animal_type;
const char* name;
const char* sound;
} al_cat;
inline int make_cat(al_cat** c) {
*c = (al_cat*) malloc(sizeof(al_cat));
(*c)->animal_type = "al_cat";
(*c)->name = "garfield";
(*c)->sound = "meow";
return AL_SUCCESS;
}
inline int free_cat(al_cat* c) {
free(c);
return AL_SUCCESS;
}
int make_sound(void* animal) {
if(AL_IS_TYPE_OF(animal, al_cat)) {
al_cat *c = (al_cat*) animal;
return dummy_computation();
} else if(AL_IS_TYPE_OF(animal, al_dog)) {
al_dog *d = (al_dog*) animal;
return dummy_computation();
} else {
printf("unknown animaln");
return 0;
}
}
/* c style library finishes here */
/* cpp library with OOP */
struct animal {
animal(const char* n, const char* s)
:name(n)
,sound(s)
{}
virtual int make_sound() {
return dummy_computation();
}
const char* name;
const char* sound;
};
struct cat : animal {
cat()
:animal("garfield", "meow")
{}
};
struct dog : animal {
dog()
:animal("leslie", "bark")
{}
};
/* cpp library finishes here */
我有一个叫做dummy_computation
的东西,只是为了确保在基准测试中进行一些计算。对于这样的例子,我通常会实现不同的printf
调用,用于吠叫、meowing等,但printf
在quick-benchmarks.com中不容易进行基准测试。我真正想基准测试的是运行时多态性实现。这就是为什么我选择制作一些小函数,并在C和C++实现中使用它作为填充。
现在,在quick-benchmarks.com中,我有一个基准如下:
static void c_style(benchmark::State& state) {
// Code inside this loop is measured repeatedly
for (auto _ : state) {
al_dog* d = NULL;
al_cat* c = NULL;
make_dog(&d);
make_cat(&c);
int i1 = make_sound(d);
benchmark::DoNotOptimize(i1);
int i2 = make_sound(c);
benchmark::DoNotOptimize(i2);
free_dog(d);
free_cat(c);
}
}
// Register the function as a benchmark
BENCHMARK(c_style);
static void cpp_style(benchmark::State& state) {
for (auto _ : state) {
animal* a1 = new dog();
animal* a2 = new cat();
int i1 = a1->make_sound();
benchmark::DoNotOptimize(i1);
int i2 = a2->make_sound();
benchmark::DoNotOptimize(i2);
delete a1;
delete a2;
}
}
BENCHMARK(cpp_style);
我添加了DoNotOptimize
调用,这样虚拟调用就不会被优化掉。
整个基准可以在这里找到,如果重新创建它看起来很痛苦的话。
https://quick-bench.com/q/ezul9hDXTjfSWijCfd2LMUUEH1I
现在,令我惊讶的是,C版本的结果快了27倍。我预计C++版本可能会有一些性能上的改进,因为它是一个更精细的解决方案,但肯定不是27倍。
有人能解释一下这些结果吗?与C相比,虚拟函数调用真的会产生这么多开销吗?还是我建立这个基准测试实验的方式完全没有意义?如果是这样的话,如何更正确地衡量这些问题?
这是因为您没有实现相同的东西。如果你在C中做switch
-链的if
-链,那么(数学上(你有一个可判别的并集,它是C++中的std::variant
。
如果您希望将C++版本移植到C,那么您需要函数指针。它很可能会同样缓慢。背后的原因是,virtual
意味着前向兼容:任何代码,包括稍后加载的库,都可以从您的基础中派生并实现virtual
方法。这意味着,有时您甚至不知道在编译基模块时它可能需要处理什么(子代(类(类型系统是开放的(。这种前向兼容性不提供给std::variant
,它是封闭的(限于固定的类型列表(。