Во-первых, я прошу прощения за заголовок, потому что он, вероятно, плохо описывает проблему. Я не мог придумать лучшего.
Я буду использовать упрощенный пример реальной проблемы, которую пытаюсь решить.
По сути, у меня есть тест, окруженный вызовами «до» и «после», которые записывают соответствующую информацию для теста. Очевидным примером того, что я записываю, является текущая временная метка, но есть еще много интересных вещей, таких как счетчик циклов, использование памяти, что угодно. Я называю действие записи этих значений штампом , поэтому у нас есть что-то вроде этого:
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)
и так далее. Мы можем рассчитать количество выполненных инструкций и ошибочных прогнозов ветвей с помощью счетчиков производительности для конкретной платформы.
Большинству из них требуется только небольшой фрагмент кода, и многие из них используют один и тот же код, за исключением значения параметра (например, счетчики «инструкции» и «ветви» используют один и тот же код, за исключением того, что передают другой идентификатор для счетчика производительности для чтения).
Что еще более важно, многие из значений, которые конечный пользователь может выбрать для просмотра, составлены как функция множественных значений - например, мы можем сообщить о значении «количество команд в наносекунду» или «количество ветвей, ошибочно предсказанных для Инструкция "Значение, для каждого из которых необходимо два значения, а затем рассчитать их соотношение.
Давайте назовем этот тип значения, которое мы хотим вывести, метрику (таким образом, «количество ветвей на команду» является метрикой), а базовые значения, которые мы записываем непосредственно, измерение ( таким образом, «циклы» или «время настенных часов наносекунд» являются измерениями). Некоторые метрики так же просты, как одно измерение, но в целом они могут быть более сложными (как в примерах соотношений). В этих рамках штамп - это просто набор измерений.
Я борюсь с тем, как создать механизм, в котором по заданному списку желаемых метрик можно создать объект 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_metric
нам нужно преобразовать в общий числовой тип для отображения (или, может быть, даже тогда, поскольку полиморфный код печати может обрабатывать разные типы).
Первая проблема - это главная и та, которую я хотел бы решить.Второе уже может быть решено с помощью какого-либо типа стирания или чего-то еще, но решение первого должно также соответствовать второму.
В частности, экземпляры Metric
и Measurement
имеют отношение многие ко многим, но я хочу, чтобы было выполнено минимальное количество измерений.
Любой шаблон, который работает здесь?Тип безопасности должен быть максимально сохранен.Метод stamp()
должен быть максимально эффективным, но эффективность других методов не имеет значения.
1 Здесь я собрал метрику определение (т. е. неизменяемые детали того, что он делает, например, функция измерения и top_id
и bottom_id
в примере PerfCounterMetric
), с объектом, который хранит состояние, определенное взаимодействие с Stamper
(например, task_id
заявляет, что записывает, в какой позиции мы ожидаем найти результат).Они логически разделены и имеют различную кратность (класс определения должен существовать только один раз в рамках всего процесса), поэтому мы также можем разделить их.