2018-03-24 00:23:33 +01:00
|
|
|
/*
|
|
|
|
|
MICROBENCHMARK.hpp - multithreaded timing measurement
|
|
|
|
|
|
|
|
|
|
Copyright (C) Lumiera.org
|
|
|
|
|
2018, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
|
|
|
modify it under the terms of the GNU General Public License as
|
|
|
|
|
published by the Free Software Foundation; either version 2 of
|
|
|
|
|
the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @file microbenchmark.hpp
|
2023-07-22 01:54:25 +02:00
|
|
|
** Functions to perform (multithreaded) timing measurement on a given functor.
|
2018-03-24 00:23:33 +01:00
|
|
|
** This helper simplifies micro benchmarks of isolated implementation details.
|
2023-07-22 01:54:25 +02:00
|
|
|
** The test subject, given as function object or lambda, is invoked numerous times
|
|
|
|
|
** within a tight loop. In the [multithreaded variant](\ref threadBenchmark()),
|
|
|
|
|
** the lambda is copied into N threads and performed in each thread in parallel;
|
|
|
|
|
** after waiting on termination of the test threads, results are summed up and then
|
|
|
|
|
** averaged into milliseconds per single invocation. The actual timing measurement
|
|
|
|
|
** relies on `chrono::duration`, which means to count micro ticks of the OS.
|
2018-03-24 00:23:33 +01:00
|
|
|
** @warning care has to bee taken when optimisation is involved!
|
|
|
|
|
** Optimisation usually has quite some impact on the results, but since
|
|
|
|
|
** this function is inline, the lambda can typically be inlined and the
|
2023-07-22 01:54:25 +02:00
|
|
|
** loop possibly be optimised away altogether. A simple workaround is
|
|
|
|
|
** to define a _volatile_ variable in the call context, close the lambda
|
2018-03-24 00:23:33 +01:00
|
|
|
** by reference, and perform a comparison with that volatile variable
|
|
|
|
|
** in each invocation. The compiler is required actually to access the
|
|
|
|
|
** value of the volatile each time.
|
|
|
|
|
** @remarks some interesting observations (in my setup, 8 core AMD FX-8350)
|
|
|
|
|
** - if we replace the global volatile by a local variable within the
|
|
|
|
|
** test subject, the initialisation of that local typically costs +5ns
|
|
|
|
|
** per invocation.
|
|
|
|
|
** - incrementing the volatile costs +10ns
|
|
|
|
|
** - multithreaded (unlocked) incrementing of the _global_ volatile
|
|
|
|
|
** creates massive overhead and increases the running time by factor 100.
|
|
|
|
|
** This nicely confirms that the x86_64 platform has strong cache coherence.
|
2023-07-22 01:54:25 +02:00
|
|
|
**
|
2018-03-24 00:23:33 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef LIB_TEST_MICROBENCHMARK_H
|
|
|
|
|
#define LIB_TEST_MICROBENCHMARK_H
|
|
|
|
|
|
|
|
|
|
|
2023-07-22 01:54:25 +02:00
|
|
|
#include "lib/meta/function.hpp"
|
2023-10-03 22:56:09 +02:00
|
|
|
#include "lib/scoped-collection.hpp"
|
|
|
|
|
#include "lib/sync-barrier.hpp"
|
|
|
|
|
#include "lib/thread.hpp"
|
2018-03-24 00:23:33 +01:00
|
|
|
|
2023-10-30 16:16:54 +01:00
|
|
|
#include "lib/test/microbenchmark-adaptor.hpp"
|
|
|
|
|
|
2018-03-24 00:23:33 +01:00
|
|
|
#include <chrono>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace lib {
|
|
|
|
|
namespace test{
|
|
|
|
|
|
|
|
|
|
namespace {
|
2023-07-22 01:54:25 +02:00
|
|
|
constexpr size_t DEFAULT_RUNS = 10'000'000;
|
2023-10-07 03:25:39 +02:00
|
|
|
using CLOCK_SCALE = std::micro; // Results are in µ-sec
|
2018-03-24 00:23:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-07-22 01:54:25 +02:00
|
|
|
/**
|
|
|
|
|
* Helper to invoke a functor or λ to observe its running time.
|
|
|
|
|
* @param invokeTestLoop the test (complete including loop) invoked once
|
|
|
|
|
* @param repeatCnt number of repetitions to divide the timing measurement
|
2023-09-24 18:05:17 +02:00
|
|
|
* @return averaged time for one repetition, in microseconds
|
2023-07-22 01:54:25 +02:00
|
|
|
*/
|
|
|
|
|
template<class FUN>
|
|
|
|
|
inline double
|
2023-12-18 23:34:10 +01:00
|
|
|
benchmarkTime (FUN const& invokeTestCode, const size_t repeatCnt =1)
|
2023-07-22 01:54:25 +02:00
|
|
|
{
|
2023-10-30 16:16:54 +01:00
|
|
|
using std::chrono::steady_clock;
|
2023-10-07 03:25:39 +02:00
|
|
|
using Dur = std::chrono::duration<double, CLOCK_SCALE>;
|
2023-07-22 01:54:25 +02:00
|
|
|
|
2023-10-30 16:16:54 +01:00
|
|
|
auto start = steady_clock::now();
|
2023-12-18 23:34:10 +01:00
|
|
|
invokeTestCode();
|
2023-10-30 16:16:54 +01:00
|
|
|
Dur duration = steady_clock::now () - start;
|
2023-10-25 23:43:19 +02:00
|
|
|
return duration.count() / repeatCnt;
|
2023-07-22 01:54:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Benchmark building block to invoke a functor or λ in a tight loop,
|
|
|
|
|
* passing the current loop index and capturing a result checksum value.
|
|
|
|
|
* @return sum of all individual invocation results as checksum
|
|
|
|
|
*/
|
|
|
|
|
template<class FUN>
|
|
|
|
|
inline size_t
|
|
|
|
|
benchmarkLoop (FUN const& testSubject, const size_t repeatCnt = DEFAULT_RUNS)
|
|
|
|
|
{
|
|
|
|
|
// the test subject gets the current loop-index and returns a checksum value
|
2023-10-30 16:16:54 +01:00
|
|
|
auto subject4benchmark = microbenchmark::adapted4benchmark (testSubject);
|
2023-07-22 01:54:25 +02:00
|
|
|
|
|
|
|
|
size_t checksum{0};
|
|
|
|
|
for (size_t i=0; i<repeatCnt; ++i)
|
2023-10-30 16:16:54 +01:00
|
|
|
checksum += subject4benchmark(i);
|
2023-07-22 01:54:25 +02:00
|
|
|
return checksum;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** perform a simple looped microbenchmark.
|
|
|
|
|
* @param testSubject the operation to test as functor or λ
|
2023-09-24 18:05:17 +02:00
|
|
|
* @return a pair `(microseconds, checksum)`
|
2023-07-22 01:54:25 +02:00
|
|
|
* @warning this setup is only usable under strong optimisation;
|
|
|
|
|
* moreover, the scaffolding without actual operation should also
|
|
|
|
|
* be tested for comparison, to get a feeling for the setup overhead.
|
|
|
|
|
* For very small test subjects (single operations) it is recommended
|
|
|
|
|
* to use a direct loop without any lambdas and building blocks.
|
|
|
|
|
*/
|
|
|
|
|
template<class FUN>
|
|
|
|
|
inline auto
|
|
|
|
|
microBenchmark (FUN const& testSubject, const size_t repeatCnt = DEFAULT_RUNS)
|
|
|
|
|
{
|
2023-12-10 22:09:46 +01:00
|
|
|
volatile size_t checksum{0};
|
2023-07-22 01:54:25 +02:00
|
|
|
auto invokeTestLoop = [&]{ checksum = benchmarkLoop (testSubject, repeatCnt); };
|
2023-09-24 18:05:17 +02:00
|
|
|
double micros = benchmarkTime (invokeTestLoop, repeatCnt);
|
2023-12-10 22:09:46 +01:00
|
|
|
return std::make_pair (micros, checksum);
|
2023-07-22 01:54:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-03-24 00:23:33 +01:00
|
|
|
/** perform a multithreaded microbenchmark.
|
|
|
|
|
* This function fires up a number of threads
|
|
|
|
|
* and invokes the given test subject repeatedly.
|
|
|
|
|
* @tparam number of threads to run in parallel
|
2023-10-30 16:16:54 +01:00
|
|
|
* @param subject function to be timed in parallel
|
2023-09-24 18:05:17 +02:00
|
|
|
* @param repeatCnt loop-count _within each thread_
|
|
|
|
|
* @return a pair `(microseconds, checksum)` combining the averaged
|
|
|
|
|
* invocation time and a compounded checksum from all threads.
|
2018-03-24 00:23:33 +01:00
|
|
|
* @remarks - the subject function will be _copied_ into each thread
|
|
|
|
|
* - so `nThreads` copies of this function will run in parallel
|
|
|
|
|
* - consider locking if this function accesses a shared closure.
|
2024-02-14 20:58:57 +01:00
|
|
|
* - if you pass a lambda, it is eligible for inlining followed by
|
|
|
|
|
* loop optimisation -- be sure to include an observable effect,
|
|
|
|
|
* like returning a value tied to the actual computation, to
|
|
|
|
|
* prevent the compiler from optimising it away altogether.
|
2018-03-24 00:23:33 +01:00
|
|
|
*/
|
|
|
|
|
template<size_t nThreads, class FUN>
|
2023-09-24 18:05:17 +02:00
|
|
|
inline auto
|
|
|
|
|
threadBenchmark(FUN const& subject, const size_t repeatCnt = DEFAULT_RUNS)
|
2018-03-24 00:23:33 +01:00
|
|
|
{
|
2023-10-30 16:16:54 +01:00
|
|
|
using std::chrono::steady_clock;
|
2023-10-07 03:25:39 +02:00
|
|
|
using Dur = std::chrono::duration<double, CLOCK_SCALE>;
|
2018-03-24 00:23:33 +01:00
|
|
|
|
2023-09-24 18:05:17 +02:00
|
|
|
// the test subject gets the current loop-index and returns a checksum value
|
2023-10-30 16:16:54 +01:00
|
|
|
auto subject4benchmark = microbenchmark::adapted4benchmark (subject);
|
|
|
|
|
using Subject = decltype(subject4benchmark);
|
2023-09-24 18:05:17 +02:00
|
|
|
|
2018-03-24 00:23:33 +01:00
|
|
|
struct Thread
|
2023-10-03 22:56:09 +02:00
|
|
|
: lib::ThreadJoinable<>
|
2018-03-24 00:23:33 +01:00
|
|
|
{
|
2023-10-30 16:16:54 +01:00
|
|
|
Thread(Subject const& testSubject, size_t loopCnt, SyncBarrier& testStart)
|
2023-10-03 22:56:09 +02:00
|
|
|
: ThreadJoinable{"Micro-Benchmark"
|
|
|
|
|
,[=, &testStart]() // local copy of the test-subject-Functor
|
2018-03-24 07:48:59 +01:00
|
|
|
{
|
2023-09-24 18:05:17 +02:00
|
|
|
testStart.sync(); // block until all threads are ready
|
2023-10-30 16:16:54 +01:00
|
|
|
auto start = steady_clock::now();
|
2018-03-24 07:48:59 +01:00
|
|
|
for (size_t i=0; i < loopCnt; ++i)
|
2023-09-24 18:05:17 +02:00
|
|
|
checksum += testSubject(i);
|
2023-10-30 16:16:54 +01:00
|
|
|
duration = steady_clock::now () - start;
|
2023-10-03 22:56:09 +02:00
|
|
|
}}
|
2018-03-24 00:23:33 +01:00
|
|
|
{ }
|
2023-09-24 18:05:17 +02:00
|
|
|
// Note: barrier at begin and join at end both ensure data synchronisation
|
|
|
|
|
Dur duration{}; // measured time within thread
|
|
|
|
|
size_t checksum{0}; // collected checksum
|
2018-03-24 00:23:33 +01:00
|
|
|
};
|
|
|
|
|
|
2023-09-24 18:05:17 +02:00
|
|
|
SyncBarrier testStart{nThreads + 1}; // coordinated start of timing measurement
|
2023-10-03 22:56:09 +02:00
|
|
|
lib::ScopedCollection<Thread> threads(nThreads);
|
2018-03-24 00:23:33 +01:00
|
|
|
for (size_t n=0; n<nThreads; ++n) // create test threads
|
2023-10-30 16:16:54 +01:00
|
|
|
threads.emplace (subject4benchmark, repeatCnt, testStart);
|
2018-03-24 00:23:33 +01:00
|
|
|
|
2023-09-24 18:05:17 +02:00
|
|
|
testStart.sync(); // barrier until all threads are ready
|
2018-03-24 00:23:33 +01:00
|
|
|
|
2023-09-24 18:05:17 +02:00
|
|
|
size_t checksum{0};
|
2018-03-24 00:23:33 +01:00
|
|
|
Dur sumDuration{0.0};
|
|
|
|
|
for (auto& thread : threads)
|
|
|
|
|
{
|
2023-09-24 18:05:17 +02:00
|
|
|
thread.join(); // block on measurement end (fence)
|
2018-03-24 00:23:33 +01:00
|
|
|
sumDuration += thread.duration;
|
2023-09-24 18:05:17 +02:00
|
|
|
checksum += thread.checksum;
|
2018-03-24 00:23:33 +01:00
|
|
|
}
|
|
|
|
|
|
2023-10-07 03:25:39 +02:00
|
|
|
double micros = sumDuration.count() / (nThreads * repeatCnt);
|
2023-09-24 18:05:17 +02:00
|
|
|
return std::make_tuple (micros, checksum);
|
2018-03-24 00:23:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}} // namespace lib::test
|
|
|
|
|
#endif /*LIB_TEST_MICROBENCHMARK_H*/
|