码迷,mamicode.com
首页 > 其他好文 > 详细

简易可扩展 Bencher

时间:2020-08-25 18:39:52      阅读:49      评论:0      收藏:0      [点我收藏+]

标签:lan   move   process   sse   弱引用   finish   rom   fine   小程序   

这里的 Bencher 指的是 C++ 代码性能测试工具。

下面是一点儿碎碎念

真的,C++ 要是有统一的包管理工具就好了。现在不但库的使用比较杂,还没有什么好的教材,真有一种旧时候手艺人的风格,大概得有个师父带着才能高效。如果我在写 Rust 的话,就直接上 Criterion 了,还管什么自己造轮子呢(当然我承认是我自己懒得去研究 Google Benchmark 怎么搞了)?

话说回来,现在写出的这个 Bencher 其实和最初的版本大相径庭。我一开始设计的有五个模块,但不断推敲后,最终变成了现在这样。设计真的不是那么简单呀。

大概想法

上一篇文章里的 Timer 功能还是太简陋了,如果想比对多组数据,甚至可视化结果,那么 Timer 就不够看了。所以我需要更多的轮子。虽然说不要 Reinvent the wheel,但是用作学习的话,我想不失为好的方法。

口上说着要更多功能,但我并不打算扩展 Timer。毕竟它只是个 Timer,给它加上其它奇怪的功能不太好。甚至,说实话,我觉得那个求 Average 的功能都有些多余。

具体设计

两个接口

  • Benchmark,主要的接口。一个 Benchmark 唯一拥有一个 Viewer,Benchmark 的数据会发送到 Viewer 并显示出来。
  • Viewer,负责展示数据,甚至是数据可视化(如果有的话!鸽鸽)。

Viewer 的实现里或许可以持有对 Benchmark 的弱引用,从而实现 Lazy Evaluation 等。但无论如何在我的设计里是 Benchmark 有 Viewer 的所有权。理想上来看,应该是 Benchmark 单向依赖于 Viewer。我或许需要再想想更好的结构。但目前这样应该是够用了。

// seideun\include\benchmark\Benchmark.hpp
//
// Created by Deuchie on 2020/8/20.

#ifndef SEIDEUN_BENCHMARK_HPP
#define SEIDEUN_BENCHMARK_HPP

#include "Viewer.hpp"

#include <chrono>
#include <memory>

namespace seideun::benchmark {

template <typename Signature>
class Benchmark;

template <typename R, typename... Args>
class Benchmark<R(Args...)> {
public:
  virtual void set_viewer(std::unique_ptr<Viewer>&& viewer) = 0; // I‘m not sure if I need this function
  virtual auto get_viewer() -> Viewer* = 0; // For viewer-specific operations
  virtual void add_case(uint64_t amount_of_data, Args&&... args) = 0;
  virtual void report() = 0;
  virtual void set_title(std::string_view new_title) = 0;
};

}

#endif //SEIDEUN_BENCHMARK_HPP

// seideun\include\benchmark\Viewer.hpp
//
// Created by Deuchie on 2020/8/20.
//

#ifndef SEIDEUN_VIEWER_HPP
#define SEIDEUN_VIEWER_HPP

#include "BenchResult.hpp"

#include <string_view>

namespace seideun::benchmark {

class Viewer {
public:
  /** General function to make the Viewer report current benchmark state
   *
   * Subclasses may well implement more specific operations. For example, designating format styles
   */
  virtual void report() = 0;

  /** Set the title of this viewer
   *
   * @param title
   */
  virtual void set_title(std::string_view new_title) = 0;

  /** Add a bench case to the viewer.
   *
   * It depends on the concrete viewers to provide specific representations of the amount of data (x) and the
   * duration (y). A user of this interface has no idea how the data will be shown.
   *
   * @param amount_of_data an abstract value indicating how much the amount of data was.
   * @param duration how long the bench took.
   */
  virtual void add_case(BenchResult bench_result) = 0;
};

}

#endif //SEIDEUN_VIEWER_HPP

二者之间通过一个结构 BenchResult 传递消息。本来我是直接写在函数签名里的,但在检查的时候顺便抽出了一个类——或许这会是过度设计吧!

// seideun\include\benchmark\BenchResult.hpp
//
// Created by Deuchie on 2020/8/20.

#ifndef SEIDEUN_BENCHRESULT_HPP
#define SEIDEUN_BENCHRESULT_HPP

#include <chrono>

namespace seideun::benchmark {

class BenchResult {
public:
  BenchResult(uint64_t amount_of_data, std::chrono::nanoseconds duration)
      : amount_of_data_(amount_of_data), duration_(duration) {}

  [[nodiscard]] auto amount_of_data() const -> uint64_t { return amount_of_data_; }
  [[nodiscard]] auto duration() const -> std::chrono::nanoseconds { return duration_; }

private:
  uint64_t amount_of_data_;
  std::chrono::nanoseconds duration_;
};

}

#endif //SEIDEUN_BENCHRESULT_HPP

基本实现

DefaultBencher 使用输出到 coutTextViewer。一开始没想加 reportset_title 两个函数,但是如果让用户调用 get_viewer 的话,会暴露奇怪的接口。我可以把接口中的 add_case 声明成 private 然后再把 Benchmark 声明成友元,但现在这样应该也行,我就懒得改了。

// seideun\include\benchmark\TextViewer.hpp
//
// Created by Deuchie on 2020/8/20.

#ifndef SEIDEUN_TEXTVIEWER_HPP
#define SEIDEUN_TEXTVIEWER_HPP

#include "Viewer.hpp"

#include <ostream>
#include <memory>
#include <vector>

namespace seideun::benchmark {

/** A viewer that outputs report to the ostream it was created with.
 *
 * Has shared ownership of the ostream. This may make it a little complicated with stdout (it is static, so we have
 * to provide a dummy deleter to prevent "deleting" stdout once the shared_ptr‘s are all destructed) but otherwise
 * frees us from the burden of closing the stream at the right time manually.
 *
 * Not thread safe.
 */
class TextViewer : public Viewer {
public:
  TextViewer() = delete;
  explicit TextViewer(std::shared_ptr<std::ostream> output_stream);

  void report() override;
  void set_title(std::string_view new_title) override;
  void add_case(BenchResult bench_result) override;

private:
  std::shared_ptr<std::ostream> ostream_;
  std::vector<BenchResult> bench_results_;
  std::string title_;
};

}

#endif //SEIDEUN_TEXTVIEWER_HPP

TextViewer 不是模板类,我顺便把实现分离到 cpp 文件里了:

// seideun/src/benchmark/TextViewer.cpp
//
// Created by Deuchie on 2020/8/20.

#include "benchmark/TextViewer.hpp"

#include "fmt/format.h"

namespace seideun::benchmark {

TextViewer::TextViewer(std::shared_ptr<std::ostream> output_stream) : ostream_(std::move(output_stream)) {}

void TextViewer::report() {
  (*ostream_) << fmt::format("# {}:\n", title_);

  size_t id = 1;
  for (auto const& i : bench_results_) {
    (*ostream_) << fmt::format(
        "Case {}: Data Amount: {}\tDuration: {}ns\n", id, i.amount_of_data(), i.duration().count());
    ++id;
  }
}

void TextViewer::set_title(std::string_view new_title) {
  title_ = new_title;
}

void TextViewer::add_case(BenchResult bench_result) {
  bench_results_.emplace_back(bench_result); // this is trivially copiable
}

}

然后是 DefaultBencher

// seideun\include\benchmark\DefaultBencher.hpp
//
// Created by Deuchie on 2020/8/20.

#ifndef SEIDEUN_DEFAULTBENCHER_HPP
#define SEIDEUN_DEFAULTBENCHER_HPP

#include "Benchmark.hpp"
#include "TextViewer.hpp"

#include <iostream>
#include <functional>

namespace seideun::benchmark {

template <typename Signature>
class DefaultBencher;

template <typename R, typename... Args>
class DefaultBencher<R(Args...)> : public Benchmark<R(Args...)> {
public:
  // Sooooo ugly
  explicit DefaultBencher(std::function<R(Args...)> func, std::string_view title = "") :
    func_(std::move(func)),
    viewer_(new TextViewer(std::shared_ptr<std::ostream>(&std::cout, [](auto i){})))
  {
    viewer_->set_title(title);
  }

  void set_viewer(std::unique_ptr<Viewer>&& viewer) override { viewer_ = std::move(viewer); }

  auto get_viewer() -> Viewer* override { return viewer_.get(); }

  void report() override { viewer_->report(); }

  void set_title(std::string_view new_title) override { viewer_->set_title(new_title); }

  void add_case(uint64_t amount_of_data, Args&&... args) override {
    using namespace std::chrono;

    auto const start = high_resolution_clock::now();
    func_(std::forward<Args>(args)...);
    auto const time_elapsed = duration_cast<nanoseconds>(high_resolution_clock::now() - start);

    viewer_->add_case(BenchResult(amount_of_data, time_elapsed)); // Inform the viewer of the new bench result
  }

private:
  std::function<R(Args...)> func_; // function to bench
  std::unique_ptr<Viewer> viewer_;
};

}

#endif //SEIDEUN_DEFAULTBENCHER_HPP

测试一下

说实话这种没有确定输出的组件,我不知道怎么写测试……就简单写个小程序试试。

#include "benchmark/DefaultBencher.hpp"

#include <cmath>
#include <fstream>
#include <random>

int func(int a) {
  using namespace std;
  auto seed = a * 987610523 + 15636982;
  default_random_engine e(seed);
  uniform_int_distribution<uint64_t> u;
  ofstream os("temp_output.txt", ios::app);
  for (int i = 0; i != a; ++i) {
    os << u(e) << ‘\t‘ << u(e) << ‘\n‘;
  }
  return seed;
}


int main() {
  using namespace seideun::benchmark;

  // 这里我没想出该如何利用自动推导,省略显式地写出模板参数的需要。
  DefaultBencher<int(int)> test_bench(func, "Test Bench");
  for (int i = 1; i != 10; ++i) {
    test_bench.add_case(i * 1000, i * 1000);
  }
  test_bench.report();

}

下面是结果:

# Test Bench:
Case 1: Data Amount: 1000       Duration: 2583300ns
Case 2: Data Amount: 2000       Duration: 2742300ns
Case 3: Data Amount: 3000       Duration: 3626900ns
Case 4: Data Amount: 4000       Duration: 5078400ns
Case 5: Data Amount: 5000       Duration: 6092700ns
Case 6: Data Amount: 6000       Duration: 7197600ns
Case 7: Data Amount: 7000       Duration: 8216900ns
Case 8: Data Amount: 8000       Duration: 9797600ns
Case 9: Data Amount: 9000       Duration: 11040400ns

Process finished with exit code 0

嗯,即使只有默认实现,这东西用来简单判断一下算法复杂度什么的应该也够用了。

简易可扩展 Bencher

标签:lan   move   process   sse   弱引用   finish   rom   fine   小程序   

原文地址:https://www.cnblogs.com/seideun/p/13536573.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!