Disabled external gits

This commit is contained in:
2022-04-07 18:46:57 +02:00
parent 88cb3426ad
commit 15e7120d6d
5316 changed files with 4563444 additions and 6 deletions

View File

@@ -0,0 +1,55 @@
BIN := ./$(notdir $(lastword $(abspath .)))
EXT_H := h
EXT_HPP := h hh hpp hxx h++
EXT_C := c
EXT_CXX := C cc cpp cxx c++
INCLUDE_DIRS := ../include .
SOURCE_DIRS := .
WILD_EXT = $(strip $(foreach EXT,$($(1)),$(wildcard $(2)/*.$(EXT))))
HDRS_C := $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),$(call WILD_EXT,EXT_H,$(INCLUDE_DIR)))
HDRS_CXX := $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),$(call WILD_EXT,EXT_HPP,$(INCLUDE_DIR)))
SRCS_C := $(foreach SOURCE_DIR,$(SOURCE_DIRS),$(call WILD_EXT,EXT_C,$(SOURCE_DIR)))
SRCS_CXX := $(foreach SOURCE_DIR,$(SOURCE_DIRS),$(call WILD_EXT,EXT_CXX,$(SOURCE_DIR)))
OBJS := $(SRCS_C:%=%.o) $(SRCS_CXX:%=%.o)
CC := $(CC)
CCFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c11 $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),-I$(INCLUDE_DIR))
CXX := $(CXX)
CXXFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c++17 $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),-I$(INCLUDE_DIR))
LD := $(if $(SRCS_CXX),$(CXX),$(CC))
LDFLAGS :=
LDLIBS := -ldl -lpthread
LIB_DIRS := $(filter-out ../include/ ../grading/ ../playground/ ../template/ ../sync-examples/,$(filter-out $(wildcard ../*),$(wildcard ../*/)))
LIB_SOS := $(patsubst %/,%.so,$(filter-out ../reference/,$(LIB_DIRS)))
.PHONY: build build-libs clean clean-libs run
build: $(BIN)
build-libs:
@$(foreach DIR,$(LIB_DIRS),make -C $(DIR) build; )
clean:
$(RM) $(OBJS) $(BIN)
clean-libs:
@$(foreach DIR,$(LIB_DIRS),make -C $(DIR) clean; )
run: $(BIN)
$(BIN) 453 ../reference.so $(LIB_SOS)
define BUILD_C
%.$(1).o: %.$(1) $$(HDRS_C) Makefile
$$(CC) $$(CCFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_C),$(eval $(call BUILD_C,$(EXT))))
define BUILD_CXX
%.$(1).o: %.$(1) $$(HDRS_CXX) Makefile
$$(CXX) $$(CXXFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_CXX),$(eval $(call BUILD_CXX,$(EXT))))
$(BIN): $(OBJS) Makefile
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

View File

@@ -0,0 +1,333 @@
/**
* @file common.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* 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 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* 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.
*
* @section DESCRIPTION
*
* Common set of helper functions/classes.
**/
#pragma once
// External headers
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <mutex>
#include <thread>
#include <utility>
extern "C" {
#include <time.h>
}
// -------------------------------------------------------------------------- //
/** Define a proposition as likely true.
* @param prop Proposition
**/
#undef likely
#ifdef __GNUC__
#define likely(prop) \
__builtin_expect((prop) ? 1 : 0, 1)
#else
#define likely(prop) \
(prop)
#endif
/** Define a proposition as likely false.
* @param prop Proposition
**/
#undef unlikely
#ifdef __GNUC__
#define unlikely(prop) \
__builtin_expect((prop) ? 1 : 0, 0)
#else
#define unlikely(prop) \
(prop)
#endif
// -------------------------------------------------------------------------- //
// Whether to enable more safety checks
constexpr static auto assert_mode = false;
// Maximum waiting time for initialization/clean-ups (in ms)
constexpr static auto max_side_time = ::std::chrono::milliseconds{2000};
// -------------------------------------------------------------------------- //
namespace Exception {
/** Defines a simple exception.
* @param name Exception name
* @param parent Parent exception (use 'Any' as the root)
* @param deftxt Default explanatory string
**/
#define EXCEPTION(name, parent, deftxt) \
class name: public parent { \
public: \
/** Default explanatory string constructor. \
**/ \
name(): parent{deftxt} {} \
/** Non-default parent forwarding constructor. \
* @param ... Forwarded arguments \
**/ \
template<class... Args> name(Args&&... args): parent{::std::forward<Args>(args)...} {} \
}
/** Root user exception class.
**/
class Any: public ::std::exception {
protected:
char const* text; // Explanation string
public:
/** Explanatory string constructor.
* @param text Explanatory null-terminated string
**/
Any(char const* text): text{text} {}
public:
/** Return the explanatory string.
* @return Explanatory string
**/
virtual char const* what() const noexcept {
return text;
}
};
/** Exception tree.
**/
EXCEPTION(Unreachable, Any, "unreachable code reached");
EXCEPTION(Bounded, Any, "bounded execution exception");
EXCEPTION(BoundedOverrun, Any, "bounded execution overrun");
}
// -------------------------------------------------------------------------- //
/** Non-copyable helper base class.
**/
class NonCopyable {
public:
/** Deleted copy constructor/assignment.
**/
NonCopyable(NonCopyable const&) = delete;
NonCopyable& operator=(NonCopyable const&) = delete;
protected:
/** Protected default constructor, to make sure class is not directly instantiated.
**/
NonCopyable() = default;
};
/** Time accounting class.
**/
class Chrono final {
public:
/** Tick class (always 1 tick = 1 ns).
**/
using Tick = uint_fast64_t;
constexpr static auto invalid_tick = Tick{0xbadc0de}; // Invalid tick value
private:
Tick total; // Total tick counter
Tick local; // Segment tick counter
public:
/** Tick constructor.
* @param tick Initial number of ticks (optional)
**/
Chrono(Tick tick = 0) noexcept: total{tick} {}
private:
/** Call a "clock" function, convert the result to the Tick type.
* @param func "Clock" function to call
* @return Resulting time
**/
static Tick convert(int (*func)(::clockid_t, struct ::timespec*)) noexcept {
struct ::timespec buf;
if (unlikely(func(CLOCK_MONOTONIC, &buf) < 0))
return invalid_tick;
auto res = static_cast<Tick>(buf.tv_nsec) + static_cast<Tick>(buf.tv_sec) * static_cast<Tick>(1000000000ul);
if (unlikely(res == invalid_tick)) // Bad luck...
return invalid_tick + 1;
return res;
}
public:
/** Get the resolution of the clock used.
* @return Resolution (in ns), 'invalid_tick' for unknown
**/
static auto get_resolution() noexcept {
return convert(::clock_getres);
}
public:
/** Start measuring a time segment.
**/
void start() noexcept {
local = convert(::clock_gettime);
}
/** Measure a time segment.
**/
auto delta() noexcept {
return convert(::clock_gettime) - local;
}
/** Stop measuring a time segment, and add it to the total.
**/
void stop() noexcept {
total += delta();
}
/** Reset the total tick counter.
**/
void reset() noexcept {
total = 0;
}
/** Get the total tick counter.
* @return Total tick counter
**/
auto get_tick() const noexcept {
return total;
}
};
/** Atomic waitable latch class.
**/
class Latch final {
private:
::std::mutex lock; // Local lock
::std::condition_variable cv; // For waiting/waking up
bool raised; // State of the latch
public:
/** Deleted copy/move constructor/assignment.
**/
Latch(Latch const&) = delete;
Latch& operator=(Latch const&) = delete;
/** Initial state constructor.
* @param raised Initial state of the latch
**/
Latch(bool raised = false): lock{}, raised{raised} {}
public:
/** Raise the latch, no-op if already raised, release semantic.
**/
void raise() {
// Release fence
::std::atomic_thread_fence(::std::memory_order_release);
// Raise
::std::unique_lock<decltype(lock)> guard{lock};
raised = true;
cv.notify_all();
}
/** Wait for the latch to be raised, then reset it, acquire semantic if no timeout.
* @param maxtick Maximal duration to wait for (in ticks)
* @return Whether the latch was raised before the maximal duration elapsed
**/
bool wait(Chrono::Tick maxtick) {
{ // Wait for raised
::std::unique_lock<decltype(lock)> guard{lock};
// Wait
if (maxtick == Chrono::invalid_tick) {
while (!raised)
cv.wait(guard);
} else {
if (!cv.wait_for(guard, ::std::chrono::nanoseconds{maxtick}, [&]() { return raised; })) // Overtime
return false;
}
// Reset
raised = false;
}
// Acquire fence
::std::atomic_thread_fence(::std::memory_order_acquire);
return true;
}
};
// -------------------------------------------------------------------------- //
/** Pause execution for a "short" period of time.
**/
static void short_pause() {
#if (defined(__i386__) || defined(__x86_64__)) && defined(USE_MM_PAUSE)
_mm_pause();
#else
::std::this_thread::yield();
#endif
}
/** Run some function for some bounded time, throws 'Exception::BoundedOverrun' on overtime.
* @param dur Maximum execution duration
* @param func Function to run (void -> void)
* @param emsg Null-terminated error message
**/
template<class Rep, class Period, class Func> static void bounded_run(::std::chrono::duration<Rep, Period> const& dur, Func&& func, char const* emsg) {
::std::mutex lock;
::std::unique_lock<decltype(lock)> guard{lock};
::std::condition_variable cv;
::std::thread runner{[&]() {
func();
{ // Notify master
::std::unique_lock<decltype(lock)> guard{lock};
cv.notify_all();
}
}};
if (cv.wait_for(guard, dur) != std::cv_status::no_timeout) {
runner.detach();
throw Exception::BoundedOverrun{emsg};
}
runner.join();
}
/** Spin barrier class.
**/
class Barrier final {
public:
/** Counter class.
**/
using Counter = uint_fast32_t;
/** Mode enum class.
**/
enum class Mode {
enter,
leave
};
private:
Counter cardinal; // Total number of threads that synchronize
::std::atomic<Counter> mutable step; // Step counters
::std::atomic<Mode> mutable mode; // Current mode
public:
/** Deleted copy constructor/assignment.
**/
Barrier(Barrier const&) = delete;
Barrier& operator=(Barrier const&) = delete;
/** Number of threads constructor.
* @param cardinal Non-null total number of threads synchronizing on this barrier
**/
Barrier(Counter cardinal): cardinal{cardinal}, step{0}, mode{Mode::enter} {}
public:
/** [thread-safe] Synchronize all the threads.
**/
void sync() const {
// Enter
if (step.fetch_add(1, ::std::memory_order_relaxed) + 1 == cardinal) { // Set leave mode
mode.store(Mode::leave, ::std::memory_order_release);
} else { // Wait for leave mode
while (unlikely(mode.load(::std::memory_order_acquire) != Mode::leave))
short_pause();
}
// Leave
if (step.fetch_sub(1, ::std::memory_order_relaxed) - 1 == 0) { // Set enter mode
mode.store(Mode::enter, ::std::memory_order_release);
} else { // Wait for enter mode
while (unlikely(mode.load(::std::memory_order_acquire) != Mode::enter))
short_pause();
}
}
};

View File

@@ -0,0 +1,353 @@
/**
* @file grading.cpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* 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 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* 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.
*
* @section DESCRIPTION
*
* Grading of the implementations.
**/
// External headers
#include <algorithm>
#include <atomic>
#include <cstring>
#include <iostream>
#include <random>
#include <variant>
// Internal headers
#include "common.hpp"
#include "transactional.hpp"
#include "workload.hpp"
// -------------------------------------------------------------------------- //
/** Tailored thread synchronization class.
**/
class Sync final {
private:
/** Synchronization status.
**/
enum class Status {
Wait, // Workers waiting each others, run as soon as all ready
Run, // Workers running (still full success)
Abort, // Workers running (>0 failure)
Done, // Workers done (all success)
Fail, // Workers done (>0 failure)
Quit // Workers must terminate
};
private:
unsigned int const nbworkers; // Number of workers to support
::std::atomic<unsigned int> nbready; // Number of thread having reached that state
::std::atomic<Status> status; // Current synchronization status
::std::atomic<char const*> errmsg; // Any one of the error message(s)
Chrono runtime; // Runtime between 'master_notify' and when the last worker finished
Latch donelatch; // For synchronization last worker -> master
public:
/** Deleted copy constructor/assignment.
**/
Sync(Sync const&) = delete;
Sync& operator=(Sync const&) = delete;
/** Worker count constructor.
* @param nbworkers Number of workers to support
**/
Sync(unsigned int nbworkers): nbworkers{nbworkers}, nbready{0}, status{Status::Done}, errmsg{nullptr} {}
public:
/** Master trigger "synchronized" execution in all threads (instead of joining).
**/
void master_notify() noexcept {
status.store(Status::Wait, ::std::memory_order_relaxed);
runtime.start();
}
/** Master trigger termination in all threads (instead of notifying).
**/
void master_join() noexcept {
status.store(Status::Quit, ::std::memory_order_relaxed);
}
/** Master wait for all workers to finish.
* @param maxtick Maximum number of ticks to wait before exiting the process on an error (optional, 'invalid_tick' for none)
* @return Total execution time on success, or error constant null-terminated string on failure
**/
::std::variant<Chrono, char const*> master_wait(Chrono::Tick maxtick = Chrono::invalid_tick) {
// Wait for all worker threads, synchronize-with the last one
if (!donelatch.wait(maxtick))
throw Exception::BoundedOverrun{"Transactional library takes too long to process the transactions"};
// Return runtime on success, of error message on failure
switch (status.load(::std::memory_order_relaxed)) {
case Status::Done:
return runtime;
case Status::Fail:
return errmsg;
default:
throw Exception::Unreachable{"Master woke after raised latch, no timeout, but unexpected status"};
}
}
/** Worker spin-wait until next run.
* @return Whether the worker can proceed, or quit otherwise
**/
bool worker_wait() noexcept {
while (true) {
auto res = status.load(::std::memory_order_relaxed);
if (res == Status::Wait)
break;
if (res == Status::Quit)
return false;
short_pause();
}
auto res = nbready.fetch_add(1, ::std::memory_order_relaxed);
if (res + 1 == nbworkers) { // Latest worker, switch to run status
nbready.store(0, ::std::memory_order_relaxed);
status.store(Status::Run, ::std::memory_order_release); // Synchronize-with previous worker waiting for run/abort state
} else do { // Not latest worker, wait for run status
short_pause();
auto res = status.load(::std::memory_order_acquire); // Synchronize-with latest worker switching to run/abort state
if (res == Status::Run || res == Status::Abort)
break;
} while (true);
return true;
}
/** Worker notify termination of its run.
* @param error Error constant null-terminated string ('nullptr' for none)
**/
void worker_notify(char const* error) noexcept {
if (error) {
errmsg.store(error, ::std::memory_order_relaxed);
status.store(Status::Abort, ::std::memory_order_relaxed);
}
auto&& res = nbready.fetch_add(1, ::std::memory_order_acq_rel); // Synchronize-with previous worker(s) potentially setting aborted status
if (res + 1 == nbworkers) { // Latest worker, switch to done/fail status
nbready.store(0, ::std::memory_order_relaxed);
status.store(status.load(::std::memory_order_relaxed) == Status::Abort ? Status::Fail : Status::Done, ::std::memory_order_relaxed);
runtime.stop();
donelatch.raise(); // Synchronize-with 'master_wait'
}
}
};
/** Measure the arithmetic mean of the execution time of the given workload with the given transaction library.
* @param workload Workload instance to use
* @param nbthreads Number of concurrent threads to use
* @param nbrepeats Number of repetitions (keep the median)
* @param seed Seed to use for performance measurements
* @param maxtick_init Timeout for (re)initialization ('Chrono::invalid_tick' for none)
* @param maxtick_perf Timeout for performance measurements ('Chrono::invalid_tick' for none)
* @param maxtick_chck Timeout for correctness check ('Chrono::invalid_tick' for none)
* @return Error constant null-terminated string ('nullptr' for none), execution times (in ns) (undefined if inconsistency detected)
**/
static auto measure(Workload& workload, unsigned int const nbthreads, unsigned int const nbrepeats, Seed seed, Chrono::Tick maxtick_init, Chrono::Tick maxtick_perf, Chrono::Tick maxtick_chck) {
::std::vector<::std::thread> threads(nbthreads);
::std::mutex cerrlock; // To avoid interleaving writes to 'cerr' in case more than one thread throw
Sync sync{nbthreads}; // "As-synchronized-as-possible" starts so that threads interfere "as-much-as-possible"
// We start nbthreads threads to measure performance.
for (unsigned int i = 0; i < nbthreads; ++i) { // Start threads
try {
threads[i] = ::std::thread{[&](unsigned int i) {
// This is the workload that all threads run simulataneously.
// It is devided into a series of small tests. Each test is specified in workload.hpp.
// Threads are synchronized between each test so that they run with a lot of concurrency.
try {
// 1. Initialization
if (!sync.worker_wait()) return; // Sync. of threads
sync.worker_notify(workload.init()); // Runs the test and tells the master about errors
// 2. Performance measurements
for (unsigned int count = 0; count < nbrepeats; ++count) {
if (!sync.worker_wait()) return;
sync.worker_notify(workload.run(i, seed + nbthreads * count + i));
}
// 3. Correctness check
if (!sync.worker_wait()) return;
sync.worker_notify(workload.check(i, std::random_device{}())); // Random seed is wanted here
// Synchronized quit
if (!sync.worker_wait()) return;
throw Exception::Unreachable{"unexpected worker iteration after checks"};
} catch (::std::exception const& err) {
sync.worker_notify("Internal worker exception(s)"); // Exception post-'Sync::worker_wait' (i.e. in 'Workload::run' or 'Workload::check'), since 'Sync::worker_*' do not throw
{ // Print the error
::std::unique_lock<decltype(cerrlock)> guard{cerrlock};
::std::cerr << "⎪⎧ *** EXCEPTION ***" << ::std::endl << "⎪⎩ " << err.what() << ::std::endl;
}
return;
}
}, i};
} catch (...) {
for (unsigned int j = 0; j < i; ++j) // Detach threads to avoid termination due to attached thread going out of scope
threads[j].detach();
throw;
}
}
// This is the master that synchronizes the worker threads.
// It basically triggers each step seen above.
// After all tests succeed, it returns the time it took to run each test.
// It returns early in case of a failure.
try {
char const* error = nullptr;
Chrono::Tick time_init = Chrono::invalid_tick;
Chrono::Tick times[nbrepeats];
Chrono::Tick time_chck = Chrono::invalid_tick;
auto const posmedian = nbrepeats / 2;
{ // Initialization (with cheap correctness test)
sync.master_notify(); // We tell workers to start working.
auto res = sync.master_wait(maxtick_init); // If running the student's version, it will timeout if way slower than the reference.
if (unlikely(::std::holds_alternative<char const*>(res))) { // If an error happened (timeout or violation), we return early!
error = ::std::get<char const*>(res);
goto join;
}
time_init = ::std::get<Chrono>(res).get_tick();
}
{ // Performance measurements (with cheap correctness tests)
for (unsigned int i = 0; i < nbrepeats; ++i) {
sync.master_notify();
auto res = sync.master_wait(maxtick_perf);
if (unlikely(::std::holds_alternative<char const*>(res))) {
error = ::std::get<char const*>(res);
goto join;
}
times[i] = ::std::get<Chrono>(res).get_tick();
}
::std::nth_element(times, times + posmedian, times + nbrepeats); // Partition times around the median
}
{ // Correctness check
sync.master_notify();
auto res = sync.master_wait(maxtick_chck);
if (unlikely(::std::holds_alternative<char const*>(res))) {
error = ::std::get<char const*>(res);
goto join;
}
time_chck = ::std::get<Chrono>(res).get_tick();
}
join: { // Joining
sync.master_join(); // Join with threads
for (unsigned int i = 0; i < nbthreads; ++i)
threads[i].join();
}
return ::std::make_tuple(error, time_init, times[posmedian], time_chck);
} catch (...) {
for (unsigned int i = 0; i < nbthreads; ++i) // Detach threads to avoid termination due to attached thread going out of scope
threads[i].detach();
throw;
}
}
// -------------------------------------------------------------------------- //
/** Program entry point.
* @param argc Arguments count
* @param argv Arguments values
* @return Program return code
**/
int main(int argc, char** argv) {
try {
// Parse command line option(s)
if (argc < 3) {
::std::cout << "Usage: " << (argc > 0 ? argv[0] : "grading") << " <seed> <reference library path> <tested library path>..." << ::std::endl;
return 1;
}
// Get/set/compute run parameters
auto const nbworkers = []() {
auto res = ::std::thread::hardware_concurrency();
if (unlikely(res == 0))
res = 16;
return static_cast<size_t>(res);
}();
auto const nbtxperwrk = 200000ul / nbworkers;
auto const nbaccounts = 32 * nbworkers;
auto const expnbaccounts = 256 * nbworkers;
auto const init_balance = 100ul;
auto const prob_long = 0.5f;
auto const prob_alloc = 0.01f;
auto const nbrepeats = 7;
auto const seed = static_cast<Seed>(::std::stoul(argv[1]));
auto const clk_res = Chrono::get_resolution();
auto const slow_factor = 16ul;
// Print run parameters
::std::cout << "⎧ #worker threads: " << nbworkers << ::std::endl;
::std::cout << "⎪ #TX per worker: " << nbtxperwrk << ::std::endl;
::std::cout << "⎪ #repetitions: " << nbrepeats << ::std::endl;
::std::cout << "⎪ Initial #accounts: " << nbaccounts << ::std::endl;
::std::cout << "⎪ Expected #accounts: " << expnbaccounts << ::std::endl;
::std::cout << "⎪ Initial balance: " << init_balance << ::std::endl;
::std::cout << "⎪ Long TX probability: " << prob_long << ::std::endl;
::std::cout << "⎪ Allocation TX prob.: " << prob_alloc << ::std::endl;
::std::cout << "⎪ Slow trigger factor: " << slow_factor << ::std::endl;
::std::cout << "⎪ Clock resolution: ";
if (unlikely(clk_res == Chrono::invalid_tick)) {
::std::cout << "<unknown>" << ::std::endl;
} else {
::std::cout << clk_res << " ns" << ::std::endl;
}
::std::cout << "⎩ Seed value: " << seed << ::std::endl;
// Library evaluations
double reference = 0.; // Set to avoid irrelevant '-Wmaybe-uninitialized'
auto const pertxdiv = static_cast<double>(nbworkers) * static_cast<double>(nbtxperwrk);
auto maxtick_init = Chrono::invalid_tick;
auto maxtick_perf = Chrono::invalid_tick;
auto maxtick_chck = Chrono::invalid_tick;
for (auto i = 2; i < argc; ++i) {
::std::cout << "⎧ Evaluating '" << argv[i] << "'" << (maxtick_init == Chrono::invalid_tick ? " (reference)" : "") << "..." << ::std::endl;
// Load TM library
TransactionalLibrary tl{argv[i]};
// Initialize workload (shared memory lifetime bound to workload: created and destroyed at the same time)
WorkloadBank bank{tl, nbworkers, nbtxperwrk, nbaccounts, expnbaccounts, init_balance, prob_long, prob_alloc};
try {
// Actual performance measurements and correctness check
auto res = measure(bank, nbworkers, nbrepeats, seed, maxtick_init, maxtick_perf, maxtick_chck);
// Check false negative-free correctness
auto error = ::std::get<0>(res);
if (unlikely(error)) {
::std::cout << "" << error << ::std::endl;
return 1;
}
// Print results
auto tick_init = ::std::get<1>(res);
auto tick_perf = ::std::get<2>(res);
auto tick_chck = ::std::get<3>(res);
auto perfdbl = static_cast<double>(tick_perf);
::std::cout << "⎪ Total user execution time: " << (perfdbl / 1000000.) << " ms";
if (maxtick_init == Chrono::invalid_tick) { // Set reference performance
maxtick_init = slow_factor * tick_init;
if (unlikely(maxtick_init == Chrono::invalid_tick)) // Bad luck...
++maxtick_init;
maxtick_perf = slow_factor * tick_perf;
if (unlikely(maxtick_perf == Chrono::invalid_tick)) // Bad luck...
++maxtick_perf;
maxtick_chck = slow_factor * tick_chck;
if (unlikely(maxtick_chck == Chrono::invalid_tick)) // Bad luck...
++maxtick_chck;
reference = perfdbl;
} else { // Compare with reference performance
::std::cout << " -> " << (reference / perfdbl) << " speedup";
}
::std::cout << ::std::endl;
::std::cout << "⎩ Average TX execution time: " << (perfdbl / pertxdiv) << " ns" << ::std::endl;
} catch (::std::exception const& err) { // Special case: cannot unload library with running threads, so print error and quick-exit
::std::cerr << "⎪ *** EXCEPTION ***" << ::std::endl;
::std::cerr << "" << err.what() << ::std::endl;
::std::quick_exit(2);
}
}
return 0;
} catch (::std::exception const& err) {
::std::cerr << "⎧ *** EXCEPTION ***" << ::std::endl;
::std::cerr << "" << err.what() << ::std::endl;
return 1;
}
}

View File

@@ -0,0 +1,620 @@
/**
* @file transactional.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* 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 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* 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.
*
* @section DESCRIPTION
*
* Transactional memory library management and use.
**/
#pragma once
// External headers
extern "C" {
#include <dlfcn.h>
#include <limits.h>
}
// Internal headers
namespace STM {
#include <tm.hpp>
}
#include "common.hpp"
// -------------------------------------------------------------------------- //
namespace Exception {
/** Exception tree.
**/
EXCEPTION(Path, Any, "path exception");
EXCEPTION(PathResolve, Path, "unable to resolve the given path");
EXCEPTION(Module, Any, "transaction library exception");
EXCEPTION(ModuleLoading, Module, "unable to load a transaction library");
EXCEPTION(ModuleSymbol, Module, "symbol not found in loaded libraries");
EXCEPTION(Transaction, Any, "transaction manager exception");
EXCEPTION(TransactionAlign, Transaction, "incorrect alignment detected before transactional operation");
EXCEPTION(TransactionReadOnly, Transaction, "tried to write/alloc/free using a read-only transaction");
EXCEPTION(TransactionCreate, Transaction, "shared memory region creation failed");
EXCEPTION(TransactionBegin, Transaction, "transaction begin failed");
EXCEPTION(TransactionAlloc, Transaction, "memory allocation failed (insufficient memory)");
EXCEPTION(TransactionRetry, Transaction, "transaction aborted and can be retried");
EXCEPTION(TransactionNotLastSegment, Transaction, "trying to deallocate the first segment");
EXCEPTION(Shared, Any, "operation in shared memory exception");
EXCEPTION(SharedAlign, Shared, "address in shared memory is not properly aligned for the specified type");
EXCEPTION(SharedOverflow, Shared, "index is past array length");
EXCEPTION(SharedDoubleAlloc, Shared, "(probable) double allocation detected before transactional operation");
EXCEPTION(SharedDoubleFree, Shared, "double free detected before transactional operation");
}
// -------------------------------------------------------------------------- //
/** Transactional library management class.
**/
class TransactionalLibrary final: private NonCopyable {
friend class TransactionalMemory;
private:
/** Function types.
**/
using FnCreate = decltype(&STM::tm_create);
using FnDestroy = decltype(&STM::tm_destroy);
using FnStart = decltype(&STM::tm_start);
using FnSize = decltype(&STM::tm_size);
using FnAlign = decltype(&STM::tm_align);
using FnBegin = decltype(&STM::tm_begin);
using FnEnd = decltype(&STM::tm_end);
using FnRead = decltype(&STM::tm_read);
using FnWrite = decltype(&STM::tm_write);
using FnAlloc = decltype(&STM::tm_alloc);
using FnFree = decltype(&STM::tm_free);
private:
void* module; // Module opaque handler
FnCreate tm_create; // Module's initialization function
FnDestroy tm_destroy; // Module's cleanup function
FnStart tm_start; // Module's start address query function
FnSize tm_size; // Module's size query function
FnAlign tm_align; // Module's alignment query function
FnBegin tm_begin; // Module's transaction begin function
FnEnd tm_end; // Module's transaction end function
FnRead tm_read; // Module's shared memory read function
FnWrite tm_write; // Module's shared memory write function
FnAlloc tm_alloc; // Module's shared memory allocation function
FnFree tm_free; // Module's shared memory freeing function
private:
/** Solve a symbol from its name, and bind it to the given function.
* @param name Name of the symbol to resolve
* @param func Target function to bind (optional, to use template parameter deduction)
**/
template<class Signature> auto solve(char const* name) const {
auto res = ::dlsym(module, name);
if (unlikely(!res))
throw Exception::ModuleSymbol{};
return *reinterpret_cast<Signature*>(&res);
}
template<class Signature> void solve(char const* name, Signature& func) const {
func = solve<Signature>(name);
}
public:
/** Loader constructor.
* @param path Path to the library to load
**/
TransactionalLibrary(char const* path) {
{ // Resolve path and load module
char resolved[PATH_MAX];
if (unlikely(!realpath(path, resolved)))
throw Exception::PathResolve{};
module = ::dlopen(resolved, RTLD_NOW | RTLD_LOCAL);
if (unlikely(!module))
throw Exception::ModuleLoading{};
}
{ // Bind module's 'tm_*' symbols
solve("tm_create", tm_create);
solve("tm_destroy", tm_destroy);
solve("tm_start", tm_start);
solve("tm_size", tm_size);
solve("tm_align", tm_align);
solve("tm_begin", tm_begin);
solve("tm_end", tm_end);
solve("tm_read", tm_read);
solve("tm_write", tm_write);
solve("tm_alloc", tm_alloc);
solve("tm_free", tm_free);
}
}
/** Unloader destructor.
**/
~TransactionalLibrary() noexcept {
::dlclose(module); // Close loaded module
}
};
/** One shared memory region management class.
**/
class TransactionalMemory final: private NonCopyable {
private:
/** Check whether the given alignment is a power of 2
**/
constexpr static bool is_power_of_two(size_t align) noexcept {
return align != 0 && (align & (align - 1)) == 0;
}
public:
/** Opaque shared memory region handle class.
**/
using Shared = STM::shared_t;
/** Transaction class alias.
**/
using TX = STM::tx_t;
private:
TransactionalLibrary const& tl; // Bound transactional library
Shared shared; // Handle of the shared memory region used
void* start_addr; // Shared memory region first segment's start address
size_t start_size; // Shared memory region first segment's size (in bytes)
size_t alignment; // Shared memory region alignment (in bytes)
public:
/** Bind constructor.
* @param library Transactional library to use
* @param align Shared memory region required alignment
* @param size Size of the shared memory region to allocate
**/
TransactionalMemory(TransactionalLibrary const& library, size_t align, size_t size): tl{library}, start_size{size}, alignment{align} {
if (unlikely(assert_mode && (!is_power_of_two(align) || size % align != 0)))
throw Exception::TransactionAlign{};
bounded_run(max_side_time, [&]() {
shared = tl.tm_create(size, align);
if (unlikely(shared == STM::invalid_shared))
throw Exception::TransactionCreate{};
start_addr = tl.tm_start(shared);
}, "The transactional library takes too long creating the shared memory");
}
/** Unbind destructor.
**/
~TransactionalMemory() noexcept {
bounded_run(max_side_time, [&]() {
tl.tm_destroy(shared);
}, "The transactional library takes too long destroying the shared memory");
}
public:
/** [thread-safe] Return the start address of the first shared segment.
* @return Address of the first allocated shared region
**/
auto get_start() const noexcept {
return start_addr;
}
/** [thread-safe] Return the size of the first shared segment.
* @return Size in the first allocated shared region (in bytes)
**/
auto get_size() const noexcept {
return start_size;
}
/** [thread-safe] Get the shared memory region global alignment.
* @return Global alignment (in bytes)
**/
auto get_align() const noexcept {
return alignment;
}
public:
/** [thread-safe] Begin a new transaction on the shared memory region.
* @param ro Whether the transaction is read-only
* @return Opaque transaction ID, 'STM::invalid_tx' on failure
**/
auto begin(bool ro) const noexcept {
return tl.tm_begin(shared, ro);
}
/** [thread-safe] End the given transaction.
* @param tx Opaque transaction ID
* @return Whether the whole transaction is a success
**/
auto end(TX tx) const noexcept {
return tl.tm_end(shared, tx);
}
/** [thread-safe] Read operation in the given transaction, source in the shared region and target in a private region.
* @param tx Transaction to use
* @param source Source start address
* @param size Source/target range
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto read(TX tx, void const* source, size_t size, void* target) const noexcept {
return tl.tm_read(shared, tx, source, size, target);
}
/** [thread-safe] Write operation in the given transaction, source in a private region and target in the shared region.
* @param tx Transaction to use
* @param source Source start address
* @param size Source/target range
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto write(TX tx, void const* source, size_t size, void* target) const noexcept {
return tl.tm_write(shared, tx, source, size, target);
}
/** [thread-safe] Memory allocation operation in the given transaction, throw if no memory available.
* @param tx Transaction to use
* @param size Size to allocate
* @param target Target start address
* @return Allocation status
**/
auto alloc(TX tx, size_t size, void** target) const noexcept {
return tl.tm_alloc(shared, tx, size, target);
}
/** [thread-safe] Memory freeing operation in the given transaction.
* @param tx Transaction to use
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto free(TX tx, void* target) const noexcept {
return tl.tm_free(shared, tx, target);
}
};
/** One transaction over a shared memory region management class.
**/
class Transaction final: private NonCopyable {
public:
/** Transaction mode class.
**/
enum class Mode: bool {
read_write = false,
read_only = true
};
private:
TransactionalMemory const& tm; // Bound transactional memory
STM::tx_t tx; // Opaque transaction handle
bool aborted; // Transaction was aborted
bool is_ro; // Whether the transaction is read-only (solely for assertion)
public:
/** Deleted copy constructor/assignment.
**/
Transaction(Transaction const&) = delete;
Transaction& operator=(Transaction const&) = delete;
/** Begin constructor.
* @param tm Transactional memory to bind
* @param ro Whether the transaction is read-only
**/
Transaction(TransactionalMemory const& tm, Mode ro): tm{tm}, tx{tm.begin(static_cast<bool>(ro))}, aborted{false}, is_ro{static_cast<bool>(ro)} {
if (unlikely(tx == STM::invalid_tx))
throw Exception::TransactionBegin{};
}
/** End destructor.
**/
~Transaction() noexcept(false) {
if (likely(!aborted)) {
if (unlikely(!tm.end(tx)))
throw Exception::TransactionRetry{};
}
}
public:
/** [thread-safe] Return the bound transactional memory instance.
* @return Bound transactional memory instance
**/
auto const& get_tm() const noexcept {
return tm;
}
public:
/** [thread-safe] Read operation in the bound transaction, source in the shared region and target in a private region.
* @param source Source start address
* @param size Source/target range
* @param target Target start address
**/
void read(void const* source, size_t size, void* target) {
if (unlikely(!tm.read(tx, source, size, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Write operation in the bound transaction, source in a private region and target in the shared region.
* @param source Source start address
* @param size Source/target range
* @param target Target start address
**/
void write(void const* source, size_t size, void* target) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
if (unlikely(!tm.write(tx, source, size, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Memory allocation operation in the bound transaction, throw if no memory available.
* @param size Size to allocate
* @return Target start address
**/
void* alloc(size_t size) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
void* target;
switch (tm.alloc(tx, size, &target)) {
case STM::Alloc::success:
return target;
case STM::Alloc::nomem:
throw Exception::TransactionAlloc{};
default: // STM::Alloc::abort
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Memory freeing operation in the bound transaction.
* @param target Target start address
**/
void free(void* target) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
if (unlikely(!tm.free(tx, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
};
// -------------------------------------------------------------------------- //
/** Shared read/write helper class.
* @param Type Specified type (array)
**/
template<class Type> class Shared {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @return Private copy of the content at the shared address
**/
Type read() const {
Type res;
tx.read(address, sizeof(Type), &res);
return res;
}
operator Type() const {
return read();
}
/** Write operation.
* @param source Private content to write at the shared address
**/
void write(Type const& source) const {
tx.write(&source, sizeof(Type), address);
}
void operator=(Type const& source) const {
return write(source);
}
public:
/** Address of the first byte after the entry.
* @return First byte after the entry
**/
void* after() const noexcept {
return address + 1;
}
};
template<class Type> class Shared<Type*> {
protected:
Transaction& tx; // Bound transaction
Type** address; // Address in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type**>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type*) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @return Private copy of the content at the shared address
**/
Type* read() const {
Type* res;
tx.read(address, sizeof(Type*), &res);
return res;
}
operator Type*() const {
return read();
}
/** Write operation.
* @param source Private content to write at the shared address
**/
void write(Type* source) const {
tx.write(&source, sizeof(Type*), address);
}
void operator=(Type* source) const {
return write(source);
}
/** Allocate and write operation.
* @param size Size to allocate (defaults to size of the underlying class)
* @return Private copy of the just-written content at the shared address
**/
Type* alloc(size_t size = 0) const {
if (unlikely(assert_mode && read() != nullptr))
throw Exception::SharedDoubleAlloc{};
auto addr = tx.alloc(size > 0 ? size: sizeof(Type));
write(reinterpret_cast<Type*>(addr));
return reinterpret_cast<Type*>(addr);
}
/** Free and write operation.
**/
void free() const {
if (unlikely(assert_mode && read() == nullptr))
throw Exception::SharedDoubleFree{};
tx.free(read());
write(nullptr);
}
public:
/** Address of the first byte after the entry.
* @return First byte after the entry
**/
void* after() const noexcept {
return address + 1;
}
};
template<class Type> class Shared<Type[]> {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address of the first element in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @param index Index to read
* @return Private copy of the content at the shared address
**/
Type read(size_t index) const {
Type res;
tx.read(address + index, sizeof(Type), &res);
return res;
}
/** Write operation.
* @param index Index to write
* @param source Private content to write at the shared address
**/
void write(size_t index, Type const& source) const {
tx.write(tx, &source, sizeof(Type), address + index);
}
public:
/** Reference a cell.
* @param index Cell to reference
* @return Shared on that cell
**/
Shared<Type> operator[](size_t index) const {
return Shared<Type>{tx, address + index};
}
/** Address of the first byte after the entry.
* @param length Length of the array
* @return First byte after the entry
**/
void* after(size_t length) const noexcept {
return address + length;
}
};
template<class Type, size_t n> class Shared<Type[n]> {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address of the first element in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @param index Index to read
* @return Private copy of the content at the shared address
**/
Type read(size_t index) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
Type res;
tx.read(address + index, sizeof(Type), &res);
return res;
}
/** Write operation.
* @param index Index to write
* @param source Private content to write at the shared address
**/
void write(size_t index, Type const& source) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
tx.write(tx, &source, sizeof(Type), address + index);
}
public:
/** Reference a cell.
* @param index Cell to reference
* @return Shared on that cell
**/
Shared<Type> operator[](size_t index) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
return Shared<Type>{tx, address + index};
}
/** Address of the first byte after the array.
* @return First byte after the array
**/
void* after() const noexcept {
return address + n;
}
};
// -------------------------------------------------------------------------- //
/** Repeat a given transaction until it commits.
* @param tm Transactional memory
* @param mode Transactional mode
* @param func Transaction closure (Transaction& -> ...)
* @return Returned value (or void) when the transaction committed
**/
template<class Func> static auto transactional(TransactionalMemory const& tm, Transaction::Mode mode, Func&& func) {
do {
try {
Transaction tx{tm, mode};
return func(tx);
} catch (Exception::TransactionRetry const&) {
continue;
}
} while (true);
}

View File

@@ -0,0 +1,389 @@
/**
* @file workload.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* 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 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* 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.
*
* @section DESCRIPTION
*
* Workload base class and derived workload(s) implementations.
**/
#pragma once
// External headers
#include <cstdint>
#include <random>
// Internal headers
#include "common.hpp"
// -------------------------------------------------------------------------- //
/** Worker unique ID type.
**/
using Uid = uint_fast32_t;
/** Seed type.
**/
using Seed = uint_fast32_t;
/** Workload base class.
**/
class Workload {
protected:
TransactionalLibrary const& tl; // Associated transactional library
TransactionalMemory tm; // Built transactional memory to use
public:
/** Deleted copy constructor/assignment.
**/
Workload(Workload const&) = delete;
Workload& operator=(Workload const&) = delete;
/** Transactional memory constructor.
* @param library Transactional library to use
* @param align Shared memory region required alignment
* @param size Size of the shared memory region to allocate
**/
Workload(TransactionalLibrary const& library, size_t align, size_t size): tl{library}, tm{tl, align, size} {}
/** Virtual destructor.
**/
virtual ~Workload() {};
public:
/** Shared memory (re)initialization.
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* init() const = 0;
/** [thread-safe] Worker's full run.
* @param Unique ID (between 0 to n-1)
* @param Seed to use
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* run(Uid, Seed) const = 0;
/** [thread-safe] Worker's false negative-free check.
* @param Unique ID (between 0 to n-1)
* @param Seed to use
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* check(Uid, Seed) const = 0;
};
// -------------------------------------------------------------------------- //
/** Bank workload class.
**/
class WorkloadBank final: public Workload {
public:
/** Account balance class alias.
**/
using Balance = intptr_t;
static_assert(sizeof(Balance) >= sizeof(void*), "Balance class is too small");
private:
/** Shared segment of accounts class.
**/
class AccountSegment final {
private:
/** Dummy structure for size and alignment retrieval.
**/
struct Dummy {
size_t dummy0;
void* dummy1;
Balance dummy2;
Balance dummy3[];
};
public:
/** Get the segment size for a given number of accounts.
* @param nbaccounts Number of accounts per segment
* @return Segment size (in bytes)
**/
constexpr static auto size(size_t nbaccounts) noexcept {
return sizeof(Dummy) + nbaccounts * sizeof(Balance);
}
/** Get the segment alignment for a given number of accounts.
* @return Segment size (in bytes)
**/
constexpr static auto align() noexcept {
return alignof(Dummy);
}
public:
Shared<size_t> count; // Number of allocated accounts in this segment
Shared<AccountSegment*> next; // Next allocated segment
Shared<Balance> parity; // Segment balance correction for when deleting an account
Shared<Balance[]> accounts; // Amount of money on the accounts (undefined if not allocated)
public:
/** Deleted copy constructor/assignment.
**/
AccountSegment(AccountSegment const&) = delete;
AccountSegment& operator=(AccountSegment const&) = delete;
/** Binding constructor.
* @param tx Associated pending transaction
* @param address Block base address
**/
AccountSegment(Transaction& tx, void* address): count{tx, address}, next{tx, count.after()}, parity{tx, next.after()}, accounts{tx, parity.after()} {}
};
private:
size_t nbworkers; // Number of concurrent workers
size_t nbtxperwrk; // Number of transactions per worker
size_t nbaccounts; // Initial number of accounts and number of accounts per segment
size_t expnbaccounts; // Expected total number of accounts
Balance init_balance; // Initial account balance
float prob_long; // Probability of running a long, read-only control transaction
float prob_alloc; // Probability of running an allocation/deallocation transaction, knowing a long transaction won't run
Barrier barrier; // Barrier for thread synchronization during 'check'
public:
/** Bank workload constructor.
* @param library Transactional library to use
* @param nbworkers Total number of concurrent threads (for both 'run' and 'check')
* @param nbtxperwrk Number of transactions per worker
* @param nbaccounts Initial number of accounts and number of accounts per segment
* @param expnbaccounts Initial number of accounts and number of accounts per segment
* @param init_balance Initial account balance
* @param prob_long Probability of running a long, read-only control transaction
* @param prob_alloc Probability of running an allocation/deallocation transaction, knowing a long transaction won't run
**/
WorkloadBank(TransactionalLibrary const& library, size_t nbworkers, size_t nbtxperwrk, size_t nbaccounts, size_t expnbaccounts, Balance init_balance, float prob_long, float prob_alloc): Workload{library, AccountSegment::align(), AccountSegment::size(nbaccounts)}, nbworkers{nbworkers}, nbtxperwrk{nbtxperwrk}, nbaccounts{nbaccounts}, expnbaccounts{expnbaccounts}, init_balance{init_balance}, prob_long{prob_long}, prob_alloc{prob_alloc}, barrier{nbworkers} {}
private:
/** Long read-only transaction, summing the balance of each account.
* @param count Loosely-updated number of accounts
* @return Whether no inconsistency has been found
**/
bool long_tx(size_t& nbaccounts) const {
return transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
auto count = 0ul; // Total number of accounts seen.
auto sum = Balance{0}; // Total balance on all seen accounts + parity ammount.
auto start = tm.get_start(); // The list of accounts starts at the first word of the shared memory region.
while (start) {
AccountSegment segment{tx, start}; // We interpret the memory as a segment/array of accounts.
decltype(count) segment_count = segment.count;
count += segment_count; // And accumulate the total number of accounts.
sum += segment.parity; // We also sum the money that results from the destruction of accounts.
for (decltype(count) i = 0; i < segment_count; ++i) {
Balance local = segment.accounts[i];
if (unlikely(local < 0)) // If one account has a negative balance, there's a consistency issue.
return false;
sum += local;
}
start = segment.next; // Accounts are stored in linked segments, we move to the next one.
}
nbaccounts = count;
return sum == static_cast<Balance>(init_balance * count); // Consistency check: no money should ever be destroyed or created out of thin air.
});
}
/** Account (de)allocation transaction, adding accounts with initial balance or removing them.
* @param trigger Trigger level that will decide whether to allocate or deallocate
**/
void alloc_tx(size_t trigger) const {
return transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
auto count = 0ul; // Total number of accounts seen.
void* prev = nullptr;
auto start = tm.get_start();
while (true) {
AccountSegment segment{tx, start};
decltype(count) segment_count = segment.count;
count += segment_count;
decltype(start) segment_next = segment.next;
if (!segment_next) { // Currently at the last segment
if (count > trigger && likely(count > 2)) { // If we have seen "too many" accounts, we will destroy one.
--segment_count; // Let's remove the last account from the last segment.
auto new_parity = segment.parity.read() + segment.accounts[segment_count] - init_balance; // We remove 1x the initial balance but don't break parity.
if (segment_count > 0) { // Just remove one account from the (last) segment without deallocating memory.
segment.count = segment_count;
segment.parity = new_parity;
} else { // If there's no one in the last segment anymore, we deallocate it.
if (unlikely(assert_mode && prev == nullptr))
throw Exception::TransactionNotLastSegment{};
AccountSegment prev_segment{tx, prev};
prev_segment.next.free();
prev_segment.parity = prev_segment.parity.read() + new_parity;
}
} else { // If we don't destroy any account, then let's create a new one.
if (segment_count < nbaccounts) { // If there's room in the last segment, then let's create the account in it without allocating memory.
segment.accounts[segment_count] = init_balance;
segment.count = segment_count + 1;
} else { // Otherwise, we really need to allocate memory for the new account.
AccountSegment next_segment{tx, segment.next.alloc(AccountSegment::size(nbaccounts))};
next_segment.count = 1;
next_segment.accounts[0] = init_balance;
}
}
return;
}
prev = start;
start = segment_next;
}
});
}
/** Short read-write transaction, transferring one unit from an account to an account (potentially the same).
* @param send_id Index of the sender account
* @param recv_id Index of the receiver account (potentially same as source)
* @return Whether the parameters were satisfying and the transaction committed on useful work
**/
bool short_tx(size_t send_id, size_t recv_id) const {
return transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
void* send_ptr = nullptr;
void* recv_ptr = nullptr;
// Get the account pointers in shared memory
auto start = tm.get_start();
while (true) {
AccountSegment segment{tx, start};
size_t segment_count = segment.count;
if (!send_ptr) {
if (send_id < segment_count) {
send_ptr = segment.accounts[send_id].get();
if (recv_ptr)
break;
} else {
send_id -= segment_count;
}
}
if (!recv_ptr) {
if (recv_id < segment_count) {
recv_ptr = segment.accounts[recv_id].get();
if (send_ptr)
break;
} else {
recv_id -= segment_count;
}
}
start = segment.next;
if (!start) // Current segment is the last segment
return false; // At least one account does not exist => do nothing
}
// Transfer the money if enough fund
Shared<Balance> sender{tx, send_ptr}; // Shared is a template that overloads copy to use tm_read/tm_write.
Shared<Balance> recver{tx, recv_ptr};
auto send_val = sender.read();
if (send_val > 0) {
sender = send_val - 1;
recver = recver.read() + 1;
}
return true;
});
}
public:
/**
* Initialize the first segment of accounts and check the initial ballance (2 transactions).
**/
virtual char const* init() const {
transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
AccountSegment segment{tx, tm.get_start()};
segment.count = nbaccounts;
for (size_t i = 0; i < nbaccounts; ++i)
segment.accounts[i] = init_balance;
});
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
AccountSegment segment{tx, tm.get_start()};
return segment.accounts[0] == init_balance;
});
if (unlikely(!correct))
return "Violated consistency (check that committed writes in shared memory get visible to the following transactions' reads)";
return nullptr;
}
/**
* Run nbtxperwrk random transactions until completion.
* @param seed Randomness source
**/
virtual char const* run(Uid uid [[gnu::unused]], Seed seed) const {
::std::minstd_rand engine{seed};
::std::bernoulli_distribution long_dist{prob_long};
::std::bernoulli_distribution alloc_dist{prob_alloc};
::std::gamma_distribution<float> alloc_trigger(expnbaccounts, 1);
size_t count = nbaccounts;
for (size_t cntr = 0; cntr < nbtxperwrk; ++cntr) {
if (long_dist(engine)) { // We roll a dice and, if "lucky", run a long transaction.
if (unlikely(!long_tx(count))) // If it fails, then we return an error message.
return "Violated isolation or atomicity";
} else if (alloc_dist(engine)) { // Let's roll a dice again to trigger an allocation transaction.
alloc_tx(alloc_trigger(engine));
} else { // No luck with previous rolls, let's just run a short transaction.
::std::uniform_int_distribution<size_t> account{0, count - 1};
while (unlikely(!short_tx(account(engine), account(engine))));
}
}
{ // Last long transaction
size_t dummy;
if (!long_tx(dummy))
return "Violated isolation or atomicity";
}
return nullptr;
}
/**
* Test in which we check that multiple concurrent transactions can decrease a counter in a sequential manner.
* @param uid Id of the thread to run the check
**/
virtual char const* check(Uid uid, Seed seed [[gnu::unused]]) const {
constexpr size_t nbtxperwrk = 100;
barrier.sync();
if (uid == 0) { // Only the first thread initializes the shared memory.
// We first write the initial value,
auto init_counter = nbtxperwrk * nbworkers;
transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
counter = init_counter;
});
// And check in another transaction that it was written correctly.
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter == init_counter;
});
if (unlikely(!correct)) {
barrier.sync();
barrier.sync();
return "Violated consistency during initialization";
}
}
// In each thread,
barrier.sync();
for (size_t i = 0; i < nbtxperwrk; ++i) {
// We first fetch the last value of the counter,
auto last = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter.read();
});
// And then we decrease the value of the counter after checking that it didn't increase since the last read.
auto correct = transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
auto value = counter.read();
if (unlikely(value > last))
return false;
counter = value - 1;
return true;
});
if (unlikely(!correct)) {
barrier.sync();
return "Violated consistency, isolation or atomicity";
}
}
// Finally, a last transaction runs in the first thread to check that the counter reached 0 (i.e., each transaction decreased it by 1.).
barrier.sync();
if (uid == 0) {
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter == 0;
});
if (unlikely(!correct))
return "Violated consistency";
}
return nullptr;
}
};