2024-11-16 04:52:58 +01:00
|
|
|
|
/*
|
|
|
|
|
|
RandomConcurrent(Test) - investigate concurrent random number generation
|
|
|
|
|
|
|
Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
* there is no entity "Lumiera.org" which holds any copyrights
* Lumiera source code is provided under the GPL Version 2+
== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''
The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!
The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
|
|
|
|
Copyright (C)
|
|
|
|
|
|
2024, Hermann Vosseler <Ichthyostega@web.de>
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
* there is no entity "Lumiera.org" which holds any copyrights
* Lumiera source code is provided under the GPL Version 2+
== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''
The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!
The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
|
|
|
|
**Lumiera** 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. See the file COPYING for further details.
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
* there is no entity "Lumiera.org" which holds any copyrights
* Lumiera source code is provided under the GPL Version 2+
== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''
The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!
The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
|
|
|
|
* *****************************************************************/
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
|
|
|
|
|
/** @file random-concurrent-test.cpp
|
|
|
|
|
|
** unit test \ref RandomConcurrent_test
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "lib/test/run.hpp"
|
|
|
|
|
|
#include "lib/sync-barrier.hpp"
|
|
|
|
|
|
#include "lib/random.hpp"
|
|
|
|
|
|
#include "lib/thread.hpp"
|
|
|
|
|
|
#include "lib/sync.hpp"
|
|
|
|
|
|
#include "lib/util.hpp"
|
|
|
|
|
|
#include "lib/scoped-collection.hpp"
|
|
|
|
|
|
#include "lib/test/microbenchmark.hpp"
|
2024-11-16 19:34:37 +01:00
|
|
|
|
#include "lib/format-string.hpp"
|
|
|
|
|
|
#include "lib/format-cout.hpp"
|
2024-11-16 04:52:58 +01:00
|
|
|
|
#include "lib/test/diagnostic-output.hpp"
|
|
|
|
|
|
|
|
|
|
|
|
#include <deque>
|
2024-11-16 13:30:22 +01:00
|
|
|
|
#include <tuple>
|
|
|
|
|
|
using std::tuple;
|
|
|
|
|
|
using std::deque;
|
2024-11-16 19:34:37 +01:00
|
|
|
|
using util::_Fmt;
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
|
|
|
|
|
namespace lib {
|
|
|
|
|
|
namespace test {
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
2024-11-16 19:34:37 +01:00
|
|
|
|
const uint NUM_THREADS = 8; ///< for concurrent probes
|
|
|
|
|
|
const uint NUM_SAMPLES = 80; ///< overall number measurement runs
|
2025-05-11 01:18:19 +02:00
|
|
|
|
const uint NUM_INVOKES = 1'000'000; ///< invocations of the target per measurement
|
2024-11-16 04:52:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-16 19:34:37 +01:00
|
|
|
|
|
2024-11-16 04:52:58 +01:00
|
|
|
|
/******************************************************************//**
|
|
|
|
|
|
* @test demonstrate simple access to random number generation,
|
|
|
|
|
|
* as well as the setup of controlled random number sequences.
|
|
|
|
|
|
* @see random.hpp
|
|
|
|
|
|
*/
|
|
|
|
|
|
class RandomConcurrent_test : public Test
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
virtual void
|
2024-11-17 19:45:41 +01:00
|
|
|
|
run (Arg arg)
|
2024-11-16 04:52:58 +01:00
|
|
|
|
{
|
|
|
|
|
|
seedRand();
|
2024-11-17 19:45:41 +01:00
|
|
|
|
benchmark_random_gen();
|
|
|
|
|
|
if ("quick" != firstTok (arg))
|
|
|
|
|
|
investigate_concurrentAccess();
|
2024-11-16 04:52:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-11-17 19:45:41 +01:00
|
|
|
|
/** @test microbenchmark of various random number generators
|
|
|
|
|
|
* @remark typical values
|
|
|
|
|
|
* - `rand()` (trinomial generator) : 15ns / 10ns (O3)
|
|
|
|
|
|
* - Mersenne twister 64bit : 55ns / 25ns (O3)
|
|
|
|
|
|
* - reading /dev/urandom : 480ns / 470 (O3)
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
benchmark_random_gen()
|
|
|
|
|
|
{
|
|
|
|
|
|
auto do_nothing = []{ /* take it easy */ };
|
|
|
|
|
|
auto mersenne64 = []{ return rani(); };
|
|
|
|
|
|
auto legacy_gen = []{ return rand(); };
|
|
|
|
|
|
std::random_device entropySource{"/dev/urandom"};
|
|
|
|
|
|
auto rly_random = [&]{ return entropySource(); };
|
|
|
|
|
|
|
|
|
|
|
|
_Fmt resultDisplay{"µ-bench(%s)%|45T.| %5.3f µs"};
|
|
|
|
|
|
|
|
|
|
|
|
double d1 = microBenchmark (do_nothing, NUM_INVOKES).first;
|
|
|
|
|
|
cout << resultDisplay % "(empty call)" % d1 <<endl;
|
|
|
|
|
|
|
|
|
|
|
|
double d2 = microBenchmark (mersenne64, NUM_INVOKES).first;
|
|
|
|
|
|
cout << resultDisplay % "Mersenne-64" % d2 <<endl;
|
|
|
|
|
|
|
|
|
|
|
|
double d3 = microBenchmark (legacy_gen, NUM_INVOKES).first;
|
|
|
|
|
|
cout << resultDisplay % "std::rand()" % d3 <<endl;
|
|
|
|
|
|
|
|
|
|
|
|
double d4 = microBenchmark (rly_random, NUM_INVOKES).first;
|
|
|
|
|
|
cout << resultDisplay % "/dev/urandom" % d4 <<endl;
|
|
|
|
|
|
|
|
|
|
|
|
CHECK (d3 < d2 and d2 < d4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Research setup to investigate concurrent access to a random generator.
|
|
|
|
|
|
* From each test thread, the shared generator instance is invoked a huge number times
|
|
|
|
|
|
* (defined by NUM_INVOKES), thereby computing the mean value and checking for defect
|
|
|
|
|
|
* numbers outside the generator's definition range. This probe cycle is repeated
|
|
|
|
|
|
* several times (defined by NUM_SAMPLES) and the results are collected and evaluated
|
|
|
|
|
|
* afterwards to detect signs of a skewed distribution.
|
|
|
|
|
|
* @tparam GEN a C++ compliant generator type
|
|
|
|
|
|
* @tparam threads number of threads to run in parallel
|
|
|
|
|
|
* @remark Pseudo random number generation as such is not threadsafe, and pressing for
|
|
|
|
|
|
* concurrent access (as done here) will produce a corrupted internal generator
|
|
|
|
|
|
* state sooner or later. Under some circumstances however, theses glitches
|
|
|
|
|
|
* can be ignored, if quality of generated numbers actually does not matter.
|
|
|
|
|
|
*/
|
2024-11-16 19:34:37 +01:00
|
|
|
|
template<typename GEN, uint threads>
|
|
|
|
|
|
struct Experiment
|
|
|
|
|
|
: Sync<>
|
2024-11-16 04:52:58 +01:00
|
|
|
|
{
|
2024-11-16 19:34:37 +01:00
|
|
|
|
deque<tuple<double,uint>> results;
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
|
recordRun (double err, uint fails)
|
2024-11-16 04:52:58 +01:00
|
|
|
|
{
|
2024-11-16 19:34:37 +01:00
|
|
|
|
Lock sync(this);
|
|
|
|
|
|
results.emplace_back (err, fails);
|
|
|
|
|
|
}
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
2024-11-16 13:30:22 +01:00
|
|
|
|
|
2024-11-16 19:34:37 +01:00
|
|
|
|
GEN generator;
|
|
|
|
|
|
|
|
|
|
|
|
Experiment(GEN&& fun)
|
|
|
|
|
|
: generator{move (fun)}
|
|
|
|
|
|
{ }
|
2024-11-16 13:30:22 +01:00
|
|
|
|
|
|
|
|
|
|
const uint N = NUM_INVOKES;
|
2024-11-16 19:34:37 +01:00
|
|
|
|
const uint REPEATS = NUM_SAMPLES / threads;
|
2025-07-05 20:08:18 +02:00
|
|
|
|
using ResVal = GEN::result_type;
|
2024-11-16 19:34:37 +01:00
|
|
|
|
ResVal expect = (GEN::max() - GEN::min()) / 2;
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
2024-11-16 19:34:37 +01:00
|
|
|
|
/* === Measurement Results === */
|
|
|
|
|
|
double percentGlitches{0.0};
|
|
|
|
|
|
double percentTilted {0.0};
|
|
|
|
|
|
bool isFailure {false};
|
|
|
|
|
|
|
2024-11-17 19:45:41 +01:00
|
|
|
|
/** run the experiment series */
|
2024-11-16 19:34:37 +01:00
|
|
|
|
void
|
|
|
|
|
|
perform()
|
|
|
|
|
|
{
|
|
|
|
|
|
auto drawRandom = [&]()
|
2024-11-16 13:30:22 +01:00
|
|
|
|
{
|
2024-11-16 19:34:37 +01:00
|
|
|
|
uint fail{0};
|
|
|
|
|
|
double avg{0.0};
|
|
|
|
|
|
for (uint i=0; i<N; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
auto r = generator();
|
|
|
|
|
|
if (r < GEN::min() or r > GEN::max())
|
|
|
|
|
|
++fail;
|
2024-11-17 19:45:41 +01:00
|
|
|
|
avg += 1.0/N * r;
|
2024-11-16 19:34:37 +01:00
|
|
|
|
}
|
|
|
|
|
|
auto error = avg/expect - 1;
|
|
|
|
|
|
recordRun (error, fail);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
threadBenchmark<threads> (drawRandom, REPEATS);
|
|
|
|
|
|
|
|
|
|
|
|
uint cases{0}, lows{0}, glitches{0};
|
|
|
|
|
|
_Fmt resultLine{"%6.3f ‰ : %d %s"};
|
|
|
|
|
|
for (auto [err,fails] : results)
|
|
|
|
|
|
{
|
2024-11-17 19:45:41 +01:00
|
|
|
|
bool isGlitch = fails or fabs(err) > 3 * 1/sqrt(N); // mean of a sound distribution will remain within bounds
|
2024-11-16 19:34:37 +01:00
|
|
|
|
cout << resultLine % (err*1000)
|
|
|
|
|
|
% fails
|
|
|
|
|
|
% (fails? "FAIL": isGlitch? " !! ":"") << endl;
|
|
|
|
|
|
++cases;
|
|
|
|
|
|
if (err < 0) ++lows;
|
|
|
|
|
|
if (isGlitch) ++glitches;
|
|
|
|
|
|
}
|
|
|
|
|
|
// assess overall results......
|
|
|
|
|
|
percentGlitches = 100.0 * glitches/cases;
|
2024-11-17 19:45:41 +01:00
|
|
|
|
percentTilted = 100.0 * fabs(double(lows)/cases - 0.5)*2; // degree to which mean is biased for one side
|
|
|
|
|
|
isFailure = glitches or percentTilted > 30; // (empirical trigger criterion)
|
2024-11-16 19:34:37 +01:00
|
|
|
|
cout << _Fmt{"++-------------++ %s\n"
|
|
|
|
|
|
" Glitches: %5.1f %%\n"
|
|
|
|
|
|
" Tilted: %5.1f %%\n"
|
|
|
|
|
|
"++-------------++\n"}
|
|
|
|
|
|
% (isFailure? "FAIL": "(ok)")
|
|
|
|
|
|
% percentGlitches
|
|
|
|
|
|
% percentTilted
|
|
|
|
|
|
<< endl;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-11-17 19:45:41 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @test examine behaviour of PRNG under concurrency stress
|
|
|
|
|
|
* - running a 32bit generator single threaded should not trigger alarms
|
|
|
|
|
|
* - while under concurrent pressure several defect numbers should be produced
|
|
|
|
|
|
* - even the 64bit generator will show uneven distribution due to corrupted state
|
|
|
|
|
|
* - the 32bit generator capped to its valid range exhibits skew only occasionally
|
|
|
|
|
|
* @see lib::CappedGen
|
|
|
|
|
|
*/
|
2024-11-16 19:34:37 +01:00
|
|
|
|
void
|
|
|
|
|
|
investigate_concurrentAccess()
|
|
|
|
|
|
{
|
|
|
|
|
|
using Mersenne64 = std::mt19937_64;
|
2024-11-17 19:45:41 +01:00
|
|
|
|
using Mersenne32 = std::mt19937;
|
|
|
|
|
|
using CappedMs32 = CappedGen<Mersenne32>;
|
2024-11-16 19:34:37 +01:00
|
|
|
|
|
2024-11-17 19:45:41 +01:00
|
|
|
|
Experiment<Mersenne32,1> single_mers32{Mersenne32(defaultGen.uni())};
|
|
|
|
|
|
Experiment<Mersenne32,NUM_THREADS> concurr_mers32{Mersenne32(defaultGen.uni())};
|
|
|
|
|
|
Experiment<Mersenne64,NUM_THREADS> concurr_mers64{Mersenne64(defaultGen.uni())};
|
|
|
|
|
|
Experiment<CappedMs32,NUM_THREADS> concCap_mers32{CappedMs32(defaultGen.uni())};
|
2024-11-16 19:34:37 +01:00
|
|
|
|
|
2024-11-17 19:45:41 +01:00
|
|
|
|
single_mers32.perform();
|
|
|
|
|
|
concurr_mers32.perform();
|
|
|
|
|
|
concurr_mers64.perform();
|
|
|
|
|
|
concCap_mers32.perform();
|
2024-11-16 04:52:58 +01:00
|
|
|
|
|
2024-11-17 19:45:41 +01:00
|
|
|
|
CHECK (not single_mers32.isFailure, "ALARM : single-threaded Mersenne-Twister 32bit produces skewed distribution");
|
|
|
|
|
|
CHECK ( concurr_mers32.isFailure, "SURPRISE : Mersenne-Twister 32bit encountered NO glitches under concurrent pressure");
|
|
|
|
|
|
CHECK ( concurr_mers64.isFailure, "SURPRISE : Mersenne-Twister 64bit encountered NO glitches under concurrent pressure");
|
2024-11-16 04:52:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
LAUNCHER (RandomConcurrent_test, "unit common");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}} // namespace lib::test
|