Disabled external gits
This commit is contained in:
55
cs453-ca/CS453-2021-project/grading/Makefile
Normal file
55
cs453-ca/CS453-2021-project/grading/Makefile
Normal 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)
|
333
cs453-ca/CS453-2021-project/grading/common.hpp
Normal file
333
cs453-ca/CS453-2021-project/grading/common.hpp
Normal 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();
|
||||
}
|
||||
}
|
||||
};
|
353
cs453-ca/CS453-2021-project/grading/grading.cpp
Normal file
353
cs453-ca/CS453-2021-project/grading/grading.cpp
Normal 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;
|
||||
}
|
||||
}
|
620
cs453-ca/CS453-2021-project/grading/transactional.hpp
Normal file
620
cs453-ca/CS453-2021-project/grading/transactional.hpp
Normal 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);
|
||||
}
|
389
cs453-ca/CS453-2021-project/grading/workload.hpp
Normal file
389
cs453-ca/CS453-2021-project/grading/workload.hpp
Normal 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;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user