通过在std::variant
中仔细观察__do_visit
,我对std::variant
多态方法的性能产生了好奇心
我编写了一个小型测试程序,比较旧式继承和std::variant
继承
#include <variant>
#include <vector>
#include <iostream>
#include <string>
#include <chrono>
int i = 0;
// Polymorphism using variants
class circle
{
public:
void draw() const { i++; }
};
class line
{
public:
void draw() const { i++; }
};
using v_t = std::variant<circle, line>;
void variant_way(const std::vector<v_t>& v)
{
for (const auto &var : v)
std::visit([](const auto& o) {
o.draw();
}, var);
}
// old school
class shape
{
public:
virtual void draw() const = 0;
virtual ~shape() { }
};
class circle_in : public shape
{
public:
virtual void draw() const { i++; }
};
class line_in : public shape
{
public:
virtual void draw() const { i++; }
};
void inherit_way(const std::vector<shape*>& v)
{
for (const auto var : v)
var->draw();
}
// call and measure
template <typename F, typename D>
void run(F f, const D& data, std::string name)
{
auto start = std::chrono::high_resolution_clock::now();
f(data);
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << name << ": "<< elapsed.count() << std::endl;
}
int main()
{
constexpr int howmany = 100000;
{
std::vector<v_t> v {howmany};
run(variant_way, v, "variant");
}
{
std::vector<shape*> v;
for (int i = 0; i < howmany; i++)
v.push_back(new circle_in());
run(inherit_way, v, "inherit_way");
// deallocate
}
return 0;
}
在我的机器(i7,16GB RAM)上,我得到了以下结果:
variant: 7487
inherit_way: 1302
我怀疑这个结果反映了std::variant
方法在每次迭代时都会创建vtable
,而继承方法只做一次。
这个解释对吗?
有没有办法减少开销?
1条答案
按热度按时间htrmnn0y1#
这个问题有几点是错误的;从技术上讲,这不一定是一个答案,但我认为它提供了一些有益的思考。
首先,多态类和类型联合体(变体)中的类型都没有数据,因此这些类型的大小对于非OOP "plain old data"类型是1字节,对于OOPy类型是8字节。
至少要构造一个合理的示例,您必须实际上使类型保留一些数据;如果不是,那又有什么意义呢?你并不是在衡量你认为你在衡量的东西。
例如:在没有优化的情况下运行此示例,得到的结果与您在此处提供的结果相同。
但是突然用
clang++ -std=c++20 -O3
编译,结果却大大地偏向了std::variant
:我敢打赌,编译器足够聪明,能够看到,变体case只是一个元素向量,大小为1字节,但值永远不会改变,完全优化了它,因为POD类型的成员函数只是递增一个全局变量,它永远不会被读取,它实际上什么也不用做,所以结果是0。
下面是一个更好的"示例问题"--虽然同样是人为设计的问题,但它实际上花了时间来衡量我认为你实际上在追求什么。
使用
clang++ -std=c++20 -O3
或gcc等效项编译该代码,现在会产生如下结果其中
inherit_way
的波动速度比std::variant
慢3.5倍。我怀疑这与shuffle最终的有利程度有关。然而,std::variant
从未显示过这种大的波动。因此,公平地说,对于像这样简单的东西,std::variant的性能远远优于OOP。但是在编写这样的"基准测试"时必须非常小心,因为即使在我的例子中,我也很确定我错过了大量的事情,这些事情最终导致了结果的误导。例如,如果不对保存数据的向量进行 Shuffle ,突然OOP版本以微小的优势获胜,这是由于(可能)CPU能够推测:嘿,每一个其他的虚拟调用间接都以极高的确定性到达这个特定的地址,但是现实世界中的数据最终是这样布局的(顺便说一句,如果你知道数据是这样布局的,你甚至不会在这里使用
std::variant
,只使用2个向量,一个保存line
,一个保存circle
)。对于这个人为的例子,
std::variant
比OOP方法快得多的原因之一与间接有关。对于std::vector<shape*>
的每个元素,至少有2个间接查找。一个用于元素的指针,一个用于虚拟表查找,而对于std::variant
,每个元素在向量中连续地布局。使用缓存和"we"本质上只是对某个类型值执行switch
操作,然后根据类型调用正确的函数,正如结果所示,这比虚拟调用快得多。