首先,我对for标题表示歉意,因为它可能无法很好地描述问题。我无法提出更好的选择。

我将使用一个简单示例说明我要解决的实际问题。

核心是一个基准,该基准被“之前”和“之后”调用包围,它们记录了基准的相关信息。我记录的东西的明显例子是当前时间戳,但是还有很多有趣的东西,例如周期计数,内存使用等等。我将记录这些值的动作称为标记,所以我们有这样的东西:

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时间。我们可能想使用平台特定的性能计数器来计算执行的指令数和分支的错误预测。

其中大多数只需要一小段代码,并且除了参数值外,它们中的许多共享相同的代码(例如,“指令”和“分支”计数器使用相同的代码,只是它们为性能传递不同的标识符计数器阅读)。

更重要的是,最终用户可能选择查看的许多值是由多个值组成的-例如,我们可能报告“每纳秒指令数”值或“每条指令分支预测错误”值,每个值都需要两个值,然后计算它们的比例。

我们称这种类型的值为输出度量(因此“每条指令的分支数”为度量),而直接记录测量值的基础值(因此“周期”或“纳秒挂钟时间”为测量值)。有些度量标准与单个度量标准一样简单,但是通常它们可能更复杂(如比率示例中所示)。在此框架中,图章只是测量值的集合。

我正在努力的是如何创建一种机制,给定所需度量的列表,可以创建一个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];
  }
}


在不充实一些未显示的其他细节的情况下(例如如何使用增量,内存管理等),这基本上可以正常工作,但是存在以下问题:


相同的Taker对象可能会多次注册。例如,如果您计算“每个周期的指令”和“每个周期的分支”,则“周期”性能计数器将被注册两次。在实践中,这是一个严重的问题,因为您可以读取的性能计数器的数量可能会受到限制,即使没有限制,stamp()中发生的事件也越多,测量中的开销和噪声也就越大。 。
take()的返回类型与Taker的接口或其他“单个”选项受限制。通常,不同的double对象可能具有自然代表结果的不同类型,并且它们希望使用它们。仅在最后,例如,在Taker中,我们需要转换为通用的数字类型以进行显示(或者甚至不需要,因为多态打印代码可以处理不同的类型)。


第一个问题是我要解决的主要问题。第二种可能已经可以通过某种类型的擦除或其他任何方式解决,但是第一种解决方案也应包含第二种。

特别是get_metricMetric实例具有多对多关系,但是我希望进行最少的测量。

有没有在这里行之有效的模式?应尽可能保留类型安全性。 Measurement方法应尽可能高效,但其他方法的效率无关紧要。



1在这里,我将度量标准定义(即度量标准的不变细节,例如度量函数以及stamp()示例中的top_idbottom_id)与存储状态a的对象混合在一起与PerfCounterMetric的特定交互(例如,记录我们期望在哪个位置找到结果的Stamper状态)。它们在逻辑上是分开的,并且具有不同的多重性(定义类只需要在整个流程范围内存在一次),因此我们也可以将它们分开。

最佳答案

如果我正确阅读了您的描述,那么您想要的是同步惰性事件系统。

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;
};

09-06 16:22