1 #pragma once
2 
3 /// \file Session.h
4 /// \brief Benchmark
5 /// \author Pavel Sevecek (sevecek at sirrah.troja.mff.cuni.cz)
6 /// \date 2016-2021
7 
8 #include "bench/Common.h"
9 #include "bench/Stats.h"
10 #include "io/Logger.h"
11 #include "io/Path.h"
12 #include "objects/containers/Array.h"
13 #include "objects/wrappers/Expected.h"
14 #include "objects/wrappers/Outcome.h"
15 #include "objects/wrappers/SharedPtr.h"
16 #include "system/Timer.h"
17 #include <fstream>
18 #include <map>
19 
20 NAMESPACE_BENCHMARK_BEGIN
21 
22 /// Benchmark mode
23 enum class Mode {
24     SIMPLE,               ///< Just run benchmarks and report statistics
25     MAKE_BASELINE,        ///< Store iteration numbers to baseline file
26     RUN_AGAINST_BASELINE, ///< Compare iteration numbers with recorded baseline
27 };
28 
29 struct Target {
30     Mode mode;
31     uint64_t duration;
32     Size iterateCnt;
33 };
34 
35 struct Result {
36     uint64_t duration;
37     Size iterateCnt;
38     Float mean;
39     Float variance;
40     Float min;
41     Float max;
42 };
43 
44 /// Accessible from benchmarks
45 class Context {
46 private:
47     Target target;
48 
49     bool state = true;
50 
51     Size iterateCnt = 0;
52 
53     Timer timer;
54 
55     Timer iterationTimer;
56 
57     Stats stats;
58 
59     /// Name of the running benchmark
60     String name;
61 
62 public:
Context(const Target target,const String & name)63     Context(const Target target, const String& name)
64         : target(target)
65         , timer(target.duration)
66         , name(name) {}
67 
68     /// Whether to keep running or exit
running()69     INLINE bool running() {
70         state = this->shouldContinue();
71         if (iterateCnt <= 2) {
72             // restart to discard benchmark setup time and first few iterations (startup)
73             timer.restart();
74         } else {
75             stats.add(1.e-3_f * iterationTimer.elapsed(TimerUnit::MICROSECOND));
76         }
77         iterationTimer.restart();
78         iterateCnt++;
79         return state;
80     }
81 
elapsed()82     INLINE uint64_t elapsed() const {
83         return timer.elapsed(TimerUnit::MILLISECOND);
84     }
85 
iterationCnt()86     INLINE Size iterationCnt() const {
87         return iterateCnt;
88     }
89 
getStats()90     INLINE Stats getStats() const {
91         return stats;
92     }
93 
94     /// Writes given message into the logger
95     template <typename... TArgs>
log(TArgs &&...args)96     INLINE void log(TArgs&&... args) {
97         StdOutLogger logger;
98         logger.write(std::forward<TArgs>(args)...);
99     }
100 
101 private:
shouldContinue()102     INLINE bool shouldContinue() const {
103         switch (target.mode) {
104         case Mode::SIMPLE:
105         case Mode::MAKE_BASELINE:
106             // either not enough time passed, or not enough iterations
107             return iterateCnt < target.iterateCnt || !timer.isExpired();
108         case Mode::RUN_AGAINST_BASELINE:
109             return iterateCnt < target.iterateCnt;
110         default:
111             NOT_IMPLEMENTED;
112         }
113     }
114 };
115 
116 /// Single benchmark unit
117 class Unit {
118 private:
119     String name;
120 
121     using Function = void (*)(Context&);
122     Function function;
123 
124 public:
Unit(const String & name,const Function func)125     Unit(const String& name, const Function func)
126         : name(name)
127         , function(func) {
128         SPH_ASSERT(function != nullptr);
129     }
130 
getName()131     const String& getName() const {
132         return name;
133     }
134 
run(const Target target)135     Expected<Result> run(const Target target) {
136         Context context(target, name);
137         function(context);
138         uint64_t elapsed = context.elapsed();
139         Stats stats = context.getStats();
140         return Result{
141             elapsed, context.iterationCnt() - 1, stats.mean(), stats.variance(), stats.min(), stats.max()
142         };
143     }
144 };
145 
146 class Group {
147 private:
148     String name;
149     Array<SharedPtr<Unit>> benchmarks;
150 
151 public:
152     Group() = default;
153 
Group(const String & name)154     Group(const String& name)
155         : name(name) {}
156 
getName()157     const String& getName() const {
158         return name;
159     }
160 
addBenchmark(const SharedPtr<Unit> & benchmark)161     void addBenchmark(const SharedPtr<Unit>& benchmark) {
162         benchmarks.push(benchmark);
163     }
164 
begin()165     auto begin() const {
166         return benchmarks.begin();
167     }
168 
end()169     auto end() const {
170         return benchmarks.end();
171     }
172 
size()173     Size size() const {
174         return benchmarks.size();
175     }
176 };
177 
178 template <class T>
doNotOptimize(T && value)179 INLINE T&& doNotOptimize(T&& value) {
180 #if defined(__clang__)
181     asm volatile("" : : "g"(value) : "memory");
182 #else
183     asm volatile("" : : "i,r,m"(value) : "memory");
184 #endif
185     return std::forward<T>(value);
186 }
187 
188 // Force the compiler to flush pending writes to global memory. Acts as an effective read/write barrier
clobberMemory()189 INLINE void clobberMemory() {
190     asm volatile("" : : : "memory");
191 }
192 
193 class Baseline {
194 private:
195     std::map<String, Result> benchs;
196 
197 public:
198     Baseline() = default;
199 
parse(const Path & path)200     bool parse(const Path& path) {
201         benchs.clear();
202         std::wifstream ifs(path.native());
203         std::wstring wstr;
204         while (std::getline(ifs, wstr)) {
205             String line = String::fromWstring(wstr);
206             std::size_t n = line.findLast(L'/');
207             if (n == String::npos) {
208                 return false;
209             }
210             const String name = line.substr(0, n).trim();
211             Array<String> values = split(line.substr(n + 1), ',');
212             if (values.size() != 6) {
213                 return false;
214             }
215             Result result;
216             result.duration = fromString<int>(values[0]).value();
217             result.iterateCnt = fromString<int>(values[1]).value();
218             result.mean = fromString<float>(values[2]).value();
219             result.variance = fromString<float>(values[3]).value();
220             result.min = fromString<float>(values[4]).value();
221             result.max = fromString<float>(values[5]).value();
222 
223             benchs[name] = result;
224         }
225         return true;
226     }
227 
isRecorded(const String & name)228     bool isRecorded(const String& name) {
229         return benchs.find(name) != benchs.end();
230     }
231 
232     INLINE Result operator[](const String& name) {
233         return benchs[name];
234     }
235 };
236 
237 class Session {
238 private:
239     /// Global instance of the session
240     static AutoPtr<Session> instance;
241 
242     /// List of all benchmarks in the session
243     Array<SharedPtr<Unit>> benchmarks;
244 
245     /// Benchmark groups
246     Array<Group> groups;
247 
248     /// Logger used to output benchmark results
249     AutoPtr<ILogger> logger;
250 
251     /// Status of the session, contains an error if the session is in invalid state.
252     Outcome status = SUCCESS;
253 
254     enum class Flag {
255         RUN_AGAINST_BASELINE = 1 << 0, ///< Compare results with baseline
256 
257         MAKE_BASELINE = 1 << 1, ///< Record and cache baseline
258 
259         SILENT = 1 << 2, ///< Only print failed benchmarks
260     };
261 
262     struct {
263         /// Run only selected group of benchmarks
264         String group;
265 
266         Flags<Flag> flags;
267 
268         struct {
269             Path path;
270             Size commit = 0;
271         } baseline;
272 
273         Array<String> benchmarksToRun;
274 
275         Target target{ Mode::SIMPLE, 500 /*ms*/, 10 };
276 
277         Float confidence = 6._f; // sigma
278 
279         /// Maximum allowed duration of single benchmark unit; benchmarks running longer that that will
280         /// generate a warning.
281         uint64_t maxAllowedDuration = 5000 /*ms*/;
282     } params;
283 
284 public:
285     Session();
286 
287     static Session& getInstance();
288 
289     /// Adds a new benchmark into the session.
290     void registerBenchmark(const SharedPtr<Unit>& benchmark, const String& groupName);
291 
292     /// Runs all benchmarks.
293     void run(int argc, char* argv[]);
294 
295     ~Session();
296 
297 private:
298     Group& getGroupByName(const String& groupName);
299 
300     Outcome parseArgs(int arcs, char* argv[]);
301 
302     void printHelp();
303 
304     void writeBaseline(const String& name, const Result& measured);
305 
306     Path getBaselinePath();
307 
308     void compareResults(const Result& measured, const Result& baseline);
309 
310     template <typename... TArgs>
311     void log(TArgs&&... args);
312 
313     template <typename... TArgs>
314     void logError(TArgs&&... args);
315 };
316 
317 /// \todo param, warning for too fast/too slow units
318 /// \todo add comparing benchmarks, running two functions and comparing, instead of comparing against
319 /// baseline
320 
321 class Register {
322 public:
323     Register(const SharedPtr<Unit>& benchmark, const String& groupName);
324 };
325 
326 #define BENCHMARK_UNIQUE_NAME_IMPL(prefix, line) prefix##line
327 
328 #define BENCHMARK_UNIQUE_NAME(prefix, line) BENCHMARK_UNIQUE_NAME_IMPL(prefix, line)
329 
330 #define BENCHMARK_FUNCTION_NAME BENCHMARK_UNIQUE_NAME(BENCHMARK, __LINE__)
331 
332 #define BENCHMARK_REGISTER_NAME BENCHMARK_UNIQUE_NAME(REGISTER, __LINE__)
333 
334 #define BENCHMARK(name, group, ...)                                                                          \
335     static void BENCHMARK_FUNCTION_NAME(__VA_ARGS__);                                                        \
336     namespace {                                                                                              \
337         ::Sph::Benchmark::Register BENCHMARK_REGISTER_NAME(                                                  \
338             makeShared<::Sph::Benchmark::Unit>(name, &BENCHMARK_FUNCTION_NAME),                              \
339             group);                                                                                          \
340     }                                                                                                        \
341     static void BENCHMARK_FUNCTION_NAME(__VA_ARGS__)
342 
343 NAMESPACE_BENCHMARK_END
344