首先,我对for标题表示歉意,因为它可能无法很好地描述问题。我无法提出更好的建议。
我将使用一个简单例子说明我要解决的实际问题。
作为核心,我有一个基准,该基准被“之前”和“之后”调用所包围,这些调用记录了基准的相关信息。我记录的东西的明显例子是当前时间戳,但是还有很多有趣的东西,例如周期计数,内存使用等等。我称记录这些值的动作为 stamp ,所以我们有这样的东西:
Stamp before = stamper.stamp();
// benchmark code goes here
Stamp after = stamper.stamp();
// maybe we calculate (after - before) here, etc
我们可能想记录很多事情,并且在运行时指定我们需要的信息。例如,我们可能想使用std::chrono::high_resolution_clock
计算挂钟时间。我们可能要使用clock(3)
等来计算CPU时间。我们可能想使用平台特定的性能计数器来计算执行的指令数和分支的错误预测。
大多数这些仅需要一小段代码,并且除了参数值外,它们中的许多共享相同的代码(例如,“指令”和“分支”计数器使用相同的代码,但它们传递不同的标识符供性能计数器读取)。
更重要的是,最终用户可能选择查看的许多值都是 mutable 值的函数-例如,我们可能会报告“每纳秒指令数”值或“分支预测错误”每个指令”的值,每个值需要两个值,然后计算它们的比率。
我们称这种类型的值为我们要输出一个指标(因此“每条指令的分支”是一个指标),而我们直接记录一个 measurement (因此“周期”或“纳秒挂钟时间”为度量)。有些度量标准与单个度量标准一样简单,但是通常它们可能更复杂(如比率示例中所示)。在此框架中, stamp 只是测量值的集合。
我正在苦苦挣扎的是如何创建一种机制,给定所需度量的列表,可以创建一个stamper
对象,该对象的stamp()
方法记录所有必要的度量,然后可以转换为指标。
一个选项是这样的:
/* something that can take a measurement */
struct Taker {
/* return the value of the measurement at the
current instant */
virtual double take() = 0;
};
// a Stamp is just an array of doubles, one
// for each registered Taker
using Stamp = std::vector<double>;
class Stamper {
std::vector<Measurement> takers;
public:
// register a Taker to be called during stamp()
// returns: the index of the result in the Stamp
size_t register_taker(Taker* t) {
takers.push_back(t);
return takers.size() - 1;
}
// return a Stamp for the current moment by calling each taker
Stamp stamp() {
Stamp result;
for (auto taker : takers) {
result.push_back(taker->take());
}
}
}
然后,您可以针对所需的所有测量使用Taker
实现(包括仅在像这样的参数中有所变化的状态共享实现):
struct ClockTaker : public Taker {
double take() override { return clock(); }
}
struct PerfCounterTaker : public Taker {
int counter_id;
double take() override { return read_counter(counter_id); }
}
最后,您有了一个Metric
接口和实现 1 ,它们知道它们需要哪些度量以及如何注册正确的Taker
对象并使用结果。一个简单的例子是时钟指标:
struct Metric {
virtual void register_takers(Stamper& stamper) = 0;
double get_metric(const Stamp& delta) = 0;
}
struct ClockMetric : public Metric {
size_t taker_id;
void register_takers(Stamper& stamper) {
taker_id = stamper.register_taker(new ClockTaker{});
}
double get_metric(const Stamp& delta) {
return delta[taker_id];
}
}
更复杂的指标可能会注册多个Takers
,例如,对于两个性能计数器的比率:
class PerfCounterRatio : public Metric {
int top_id, bottom_id;
size_t top_taker, bottom_taker;
public:
PerfCounterRatio(int top_id, int bottom_id) : top_id{top_id}, bottom_id{bottom_id} {}
void register_takers(Stamper& stamper) {
top_taker = stamper.register_taker(new PerfCounterTaker{top_id });
bottom_taker = stamper.register_taker(new PerfCounterTaker{bottom_id});
}
double get_metric(const Stamp& delta) {
return delta[taker_id];
}
}
在没有充实一些未显示的其他细节的情况下,例如如何获取增量,内存管理等,这基本上是可行的,但是它存在以下问题:
stamp()
中发生的事情也越多,开销和噪声也会增加到测量。take()
的返回类型受限于Taker
的接口double
或其他“单一”选项。通常,不同的Taker
对象可能具有自然代表结果的不同类型,并且他们希望使用它们。仅在最后,例如,在get_metric
中,我们需要转换为通用的数字类型进行显示(或者甚至不需要,因为多态打印代码可以处理不同的类型)。第一个问题是主要问题,也是我要解决的问题。第二种可能已经通过某种类型的擦除或任何其他方式解决,但是第一种解决方案也应包含第二种。
尤其是Metric
和Measurement
实例具有多对多关系,但是我希望进行最少的测量。
任何在这里运作良好的模式?应尽可能保留类型安全性。 stamp()
方法应尽可能高效,但其他方法的效率无关紧要。
1 在这里,我将度量定义融合在一起(即,度量标准不变的细节,例如度量函数和top_id
和bottom_id
中的PerfCounterMetric
),其中存储状态的对象与Stamper
进行了特定的交互(例如,task_id
状态记录了我们期望在什么位置找到结果)。它们在逻辑上是分开的,并且具有不同的多重性(定义类只需要在整个流程范围内存在一次),因此我们也可以将它们分开。
答案 0 :(得分:1)
如果我正确阅读了您的描述,那么您想要的是同步惰性事件系统。
class event
{
public:
using callback_t = std::function<void(double)>;
event() = default;
event(std::function<double()> driver)
: driver{std::move(driver)} {}
void subscribe(callback_t c)
{
callbacks.push_back(std::move(c));
}
void execute()
{
if(callbacks.size() > 0)
{
auto d = driver();
for(auto& c : callbacks)
c(d);
}
}
private:
std::vector<callback_t> callbacks;
std::function<double()> driver;
};
您可能在events
中有一个Stamper
的列表,并且订阅是一个简单的查找
class Stamper
{
void stamp()
{
for(auto& [_, e] : events)
e.execute();
}
// ...
std::unordered_map<std::string, event> events;
};
struct PerfCounter
{
PerfCounter(Stamper& s)
{
s.events["perf"].subscribe([&](double d){ perf = d; });
s.events["counter"].subscribe([&](double d){ counter = d; });
}
double perf, counter;
};