惰性构造 - 虚拟方法与 if-then 存根 getter
Lazy construction - virtual method vs. if-then to stub a getter
我的问题的背景是我试图创建一个惰性网格结构,其中网格区域仅在需要时实例化,否则它们 return 在查询时为默认值。
稍微归结一下问题,考虑以下我的情况模型:
struct Container {
std::vector<Base> data;
float get(int indexOuter, int indexInner) {
return data[indexOuter].get(indexInner);
}
}
我想在某些情况下将 Base::get
函数存根以始终 return 相同的值,而在其他情况下我想 return 某个数组中的值。我想象两种可能的解决方案。
第一个解决方案是在 Base
上使用标志,即
struct Base {
std::vector<float> data;
float get(int indexInner) {
if (data.empty()) return 0;
return data[indexInner];
}
}
此解决方案将涉及在对象进入 "stub mode" 时销毁 data
(将大小调整为零),并在对象再次具体化时重新创建 data
。
我能想到的第二种方案是继承,即
struct Container {
std::vector<Base*> data;
float get(int indexOuter, int indexInner) {
return data[indexOuter]->get(indexInner);
}
}
struct Base {
virtual float get(int indexInner) = 0;
}
struct Stub : public Base {
float get(int indexInner) {
return 0;
}
}
struct Concrete : public Base {
std::vector<float> data;
float get(int indexInner) {
return data[indexInner];
}
}
然后在 Container
的 data
数组中必要时将 Concrete
的实例替换为 Stub
的实例,反之亦然(注意确保彻底破坏)。
问题是性能问题之一。 Container::get
每秒将被调用 1000 次,甚至更多。但是,Base
s entering/exiting "stub mode" 出现的频率要低得多。
对于上面提出的两种解决方案,第一种解决方案涉及一个额外的 if-then 子句,而第二种解决方案涉及从 Base
指针到对象的间接寻址,以及从抽象方法到派生的实现。
这些解决方案中的哪一个可以提供最佳性能?还有其他我没有考虑过的性能更高的解决方案吗?
大多数现代处理器不喜欢条件代码,特别是如果它不是 "predictable"(换句话说,您的数据有时会被填充,有时不会。
通过指向函数的指针跳转通常比条件分支更快(考虑到您还需要检查它是否被存根 [虽然您可以使用 data.empty()
这将很容易检查编译器]).
因此,如果不对这两个解决方案进行基准测试,我的猜测是虚拟 Stub
和 Concrete
会更快。但是,这将有点取决于 use-cases.
Stub
实现也将占用较少的数据。另一方面,如果这是在 std::vector<Base*> data;
中使用指针的唯一原因,那么您可能要考虑使用 isStubbed
(或 data.empty()
)并使用 std::vector<Base>
相反 - 整体上节省了一定程度的间接寻址 - 这可能会更好,具体取决于具体情况。
最终,如果它对性能很重要,您将希望同时实施 运行 具有不同 load-patterns 的基准测试并测量时间。还要查看每个 运行 的分析数据,以了解代码将时间花在哪里。
我的问题的背景是我试图创建一个惰性网格结构,其中网格区域仅在需要时实例化,否则它们 return 在查询时为默认值。
稍微归结一下问题,考虑以下我的情况模型:
struct Container {
std::vector<Base> data;
float get(int indexOuter, int indexInner) {
return data[indexOuter].get(indexInner);
}
}
我想在某些情况下将 Base::get
函数存根以始终 return 相同的值,而在其他情况下我想 return 某个数组中的值。我想象两种可能的解决方案。
第一个解决方案是在 Base
上使用标志,即
struct Base {
std::vector<float> data;
float get(int indexInner) {
if (data.empty()) return 0;
return data[indexInner];
}
}
此解决方案将涉及在对象进入 "stub mode" 时销毁 data
(将大小调整为零),并在对象再次具体化时重新创建 data
。
我能想到的第二种方案是继承,即
struct Container {
std::vector<Base*> data;
float get(int indexOuter, int indexInner) {
return data[indexOuter]->get(indexInner);
}
}
struct Base {
virtual float get(int indexInner) = 0;
}
struct Stub : public Base {
float get(int indexInner) {
return 0;
}
}
struct Concrete : public Base {
std::vector<float> data;
float get(int indexInner) {
return data[indexInner];
}
}
然后在 Container
的 data
数组中必要时将 Concrete
的实例替换为 Stub
的实例,反之亦然(注意确保彻底破坏)。
问题是性能问题之一。 Container::get
每秒将被调用 1000 次,甚至更多。但是,Base
s entering/exiting "stub mode" 出现的频率要低得多。
对于上面提出的两种解决方案,第一种解决方案涉及一个额外的 if-then 子句,而第二种解决方案涉及从 Base
指针到对象的间接寻址,以及从抽象方法到派生的实现。
这些解决方案中的哪一个可以提供最佳性能?还有其他我没有考虑过的性能更高的解决方案吗?
大多数现代处理器不喜欢条件代码,特别是如果它不是 "predictable"(换句话说,您的数据有时会被填充,有时不会。
通过指向函数的指针跳转通常比条件分支更快(考虑到您还需要检查它是否被存根 [虽然您可以使用 data.empty()
这将很容易检查编译器]).
因此,如果不对这两个解决方案进行基准测试,我的猜测是虚拟 Stub
和 Concrete
会更快。但是,这将有点取决于 use-cases.
Stub
实现也将占用较少的数据。另一方面,如果这是在 std::vector<Base*> data;
中使用指针的唯一原因,那么您可能要考虑使用 isStubbed
(或 data.empty()
)并使用 std::vector<Base>
相反 - 整体上节省了一定程度的间接寻址 - 这可能会更好,具体取决于具体情况。
最终,如果它对性能很重要,您将希望同时实施 运行 具有不同 load-patterns 的基准测试并测量时间。还要查看每个 运行 的分析数据,以了解代码将时间花在哪里。