diff --git a/build_and_bench_all.py b/build_and_bench_all.py index 786d804..57be13e 100755 --- a/build_and_bench_all.py +++ b/build_and_bench_all.py @@ -15,7 +15,7 @@ import shutil runtimes = { - "cpp": ["citor", "libfork", "TooManyCooks", "tbb", "taskflow", "cppcoro", "coros", "cobalt", + "cpp": ["citor", "libfork", "TooManyCooks", "schobi", "tbb", "taskflow", "cppcoro", "coros", "cobalt", # these 4 are quite slow - you can remove them to speed up total runtime "folly", "concurrencpp", "HPX", "libcoro"] } @@ -26,6 +26,7 @@ "citor": "https://github.com/Lallapallooza/citor", "libfork": "https://github.com/ConorWilliams/libfork", "TooManyCooks": "https://github.com/tzcnt/TooManyCooks", + "schobi": "https://codeberg.org/Schobi/Schobi", "tbb": "https://www.intel.com/content/www/us/en/developer/tools/oneapi/onetbb.html", "taskflow": "https://github.com/taskflow/taskflow", "cppcoro": "https://github.com/andreasbuhr/cppcoro", @@ -65,6 +66,22 @@ # The runtime name will be suffixed with "_" in output (e.g., "cobalt_st_asio") # Format: { "runtime": { "benchmark": ["config1", "config2", ...], ... }, ... } benchmark_configs = { + # schobi ships three implementations of each fork-join benchmark. The empty + # config is the recursive fork/join every runtime here runs, under the plain + # name, and is the only one comparable to the other rows. "sequential" is the + # same problem on one thread with no scheduler started - the baseline the rest + # are measured against. "group-shared" regroups the work into one fiber group + # that folds its own partials through group barriers. + # + # Note that the ranking below takes the best row per benchmark as the + # denominator, so schobi_sequential and schobi_group-shared both compete for + # it despite running a different program from everyone else. + "schobi": { + "fib": ["", "sequential", "group-shared"], + "skynet": ["", "sequential", "group-shared"], + "nqueens": ["", "sequential", "group-shared"], + "matmul": ["", "sequential", "group-shared"], + }, "cobalt": { "channel": ["st_asio"] }, diff --git a/cpp/2common/bench_config.hpp b/cpp/2common/bench_config.hpp new file mode 100644 index 0000000..af6cfaf --- /dev/null +++ b/cpp/2common/bench_config.hpp @@ -0,0 +1,44 @@ +#pragma once +// Selects which implementation of a benchmark to run. The harness appends the +// config name after the thread count; an absent or unrecognised name is the +// default, per-node program that every runtime here implements. +// +// group-shared cuts the problem into one chunk per task up front and reduces the +// partials, instead of spawning a task per node. It is the same arithmetic, so +// it stays a valid answer to the same benchmark, but it measures bulk dispatch +// rather than spawn cost. + +#include + +namespace bench { + +enum class Config { + fork_join, + group_shared, +}; + +inline constexpr const char* fork_join_name = "fork-join"; +inline constexpr const char* group_shared_name = "group-shared"; + +// tasks per worker in the group-shared implementations: enough chunks that a +// late-finishing one can be absorbed by the rest, few enough to stay bulk work +inline constexpr size_t tasks_per_worker = 8; + +inline Config parse_config(const char* name) { + if (name != nullptr && std::strcmp(name, group_shared_name) == 0) { + return Config::group_shared; + } + return Config::fork_join; +} + +inline const char* config_name(Config config) { + switch (config) { + case Config::group_shared: + return group_shared_name; + case Config::fork_join: + break; + } + return fork_join_name; +} + +} // namespace bench diff --git a/cpp/libfork/fib.cpp b/cpp/libfork/fib.cpp index f903117..4cb79b3 100644 --- a/cpp/libfork/fib.cpp +++ b/cpp/libfork/fib.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include "bench_config.hpp" #include "memusage.hpp" #include @@ -34,6 +36,79 @@ inline constexpr auto fib = [](auto fib, size_t n) -> lf::task { co_return x + y; }; + +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// fib(n) is expanded breadth-first into a frontier of subproblems, one chunk of +// which each task sums without the scheduler. Replacing v with v-1 and v-2 keeps +// sum(fib(frontier)) equal to fib(n), so the same tree is walked. + +inline constexpr size_t fib_leaf = 2; // below this a node is its own value +// room for the frontier to overshoot its target on the last expansion pass +inline constexpr size_t fib_frontier_slots_per_task = 4; + +static size_t fib_serial(size_t n) { + return n < fib_leaf ? n : fib_serial(n - 1) + fib_serial(n - 2); +} + +static size_t +build_frontier(std::vector& frontier, size_t n, size_t target) { + size_t capacity = frontier.size(); + size_t count = 1; + frontier[0] = static_cast(n); + for (;;) { + if (count >= target || count * 2 > capacity) { + break; + } + size_t split = 0; + for (size_t i = count; i-- > 0;) { + unsigned v = frontier[i]; + if (v < fib_leaf) { + continue; + } + frontier[i] = v - 1; + frontier[count + split] = v - 2; + ++split; + } + if (split == 0) { + break; + } + count += split; + } + return count; +} + +inline constexpr auto fib_chunk = + [](auto, unsigned const* frontier, size_t begin, size_t end) -> lf::task { + size_t sum = 0; + for (size_t i = begin; i < end; ++i) { + sum += fib_serial(frontier[i]); + } + co_return sum; +}; + +inline constexpr auto fib_grouped = + [](auto, size_t n, size_t tasks) -> lf::task { + std::vector frontier(tasks * fib_frontier_slots_per_task); + size_t count = build_frontier(frontier, n, tasks); + + std::vector results(tasks); + unsigned const* data = frontier.data(); + for (size_t t = 0; t < tasks; ++t) { + co_await lf::fork[&results[t], fib_chunk]( + data, t * count / tasks, (t + 1) * count / tasks + ); + } + co_await lf::join; + + size_t sum = 0; + for (size_t t = 0; t < tasks; ++t) { + sum += results[t]; + } + co_return sum; +}; + int main(int argc, char* argv[]) { if (argc > 2) { thread_count = static_cast(atoi(argv[2])); @@ -43,16 +118,25 @@ int main(int argc, char* argv[]) { exit(0); } size_t n = static_cast(atoi(argv[1])); + bench::Config config = bench::parse_config(argc > 3 ? argv[3] : nullptr); + size_t tasks = thread_count * bench::tasks_per_worker; std::printf("threads: %" PRIu64 "\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); lf::lazy_pool pool(thread_count); - auto result = lf::sync_wait(pool, fib, 30); // warmup + auto run = [&](size_t value) { + return config == bench::Config::group_shared + ? lf::sync_wait(pool, fib_grouped, value, tasks) + : lf::sync_wait(pool, fib, value); + }; + + run(30); // warmup auto startTime = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < iter_count; ++i) { - auto result = lf::sync_wait(pool, fib, n); + auto result = run(n); std::printf("output: %zu\n", result); } diff --git a/cpp/libfork/matmul.cpp b/cpp/libfork/matmul.cpp index a8f7054..c3797a1 100644 --- a/cpp/libfork/matmul.cpp +++ b/cpp/libfork/matmul.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt) +#include "bench_config.hpp" #include "matmul.hpp" #include "memusage.hpp" #include @@ -47,7 +48,49 @@ inline constexpr auto matmul = } }; -std::vector run_matmul(lf::lazy_pool& executor, int N) { +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// One task per block of C, each accumulating over the k blocks. Every block of C +// has a single writer, so the two-phase split the recursion needs disappears and +// the whole multiply is one dispatch. Same N^3 multiply-accumulates. + +// sized for the cache: a task holds a block of C live while streaming a block +// row of A against a block column of B +inline constexpr int matmul_blocked_block = 128; + +inline constexpr auto matmul_block_row = + [](auto, int* a, int* b, int* c, int N, int nb, int begin, + int end) -> lf::task { + for (int idx = begin; idx < end; ++idx) { + int bi = idx / nb; + int bj = idx % nb; + int* cblock = c + bi * matmul_blocked_block * N + bj * matmul_blocked_block; + for (int k = 0; k < nb; ++k) { + matmul_small( + a + bi * matmul_blocked_block * N + k * matmul_blocked_block, + b + k * matmul_blocked_block * N + bj * matmul_blocked_block, cblock, + matmul_blocked_block, N + ); + } + } + co_return; +}; + +inline constexpr auto matmul_grouped = + [](auto, int* a, int* b, int* c, int N, size_t tasks) -> lf::task { + int nb = N / matmul_blocked_block; + int blocks = nb * nb; + for (size_t t = 0; t < tasks; ++t) { + int begin = static_cast(t * blocks / tasks); + int end = static_cast((t + 1) * blocks / tasks); + co_await lf::fork[matmul_block_row](a, b, c, N, nb, begin, end); + } + co_await lf::join; +}; + +std::vector +run_matmul(lf::lazy_pool& executor, int N, bench::Config config, size_t tasks) { std::vector A(N * N, 1); std::vector B(N * N, 1); std::vector C(N * N, 0); @@ -56,7 +99,11 @@ std::vector run_matmul(lf::lazy_pool& executor, int N) { int* b = B.data(); int* c = C.data(); - lf::sync_wait(executor, matmul, a, b, c, N, N); + if (config == bench::Config::group_shared) { + lf::sync_wait(executor, matmul_grouped, a, b, c, N, tasks); + } else { + lf::sync_wait(executor, matmul, a, b, c, N, N); + } return C; } @@ -77,9 +124,9 @@ void validate_result(std::vector& C, int N) { } } -void run_one(lf::lazy_pool& executor, int N) { +void run_one(lf::lazy_pool& executor, int N, bench::Config config, size_t tasks) { auto startTime = std::chrono::high_resolution_clock::now(); - std::vector result = run_matmul(executor, N); + std::vector result = run_matmul(executor, N, config, tasks); auto endTime = std::chrono::high_resolution_clock::now(); validate_result(result, N); auto totalTimeUs = @@ -98,12 +145,16 @@ int main(int argc, char* argv[]) { exit(0); } int n = atoi(argv[1]); + bench::Config config = bench::parse_config(argc > 3 ? argv[3] : nullptr); + size_t tasks = thread_count * bench::tasks_per_worker; + std::printf("threads: %zu\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); lf::lazy_pool executor(thread_count); - run_matmul(executor, n); // warmup + run_matmul(executor, n, config, tasks); // warmup std::printf("runs:\n"); - run_one(executor, n); + run_one(executor, n, config, tasks); } diff --git a/cpp/libfork/nqueens.cpp b/cpp/libfork/nqueens.cpp index 97ded85..2c69706 100644 --- a/cpp/libfork/nqueens.cpp +++ b/cpp/libfork/nqueens.cpp @@ -14,9 +14,12 @@ #include #include #include +#include "bench_config.hpp" #include "memusage.hpp" #include +#include #include +#include static size_t thread_count = std::thread::hardware_concurrency() / 2; static const size_t iter_count = 1; @@ -87,24 +90,142 @@ constexpr auto nqueens = co_return ret; }; +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// The search tree is expanded breadth-first into a frontier of partial boards, +// one chunk of which each task searches to the full board without the +// scheduler. The same tree is walked; only the dispatch changes. + +inline constexpr int board_bytes = nqueens_work; +inline constexpr unsigned max_children = nqueens_work; + +static bool legal_at(char const* buf, int xMax, char q) { + for (int x = 0; x < xMax; ++x) { + char p = buf[x]; + if (q == p || q == p - (xMax - x) || q == p + (xMax - x)) { + return false; + } + } + return true; +} + +static int nqueens_serial(int xMax, char const* buf) { + if (xMax == nqueens_work) { + return 1; + } + int ret = 0; + for (int y = 0; y < nqueens_work; ++y) { + char q = static_cast(y); + if (!legal_at(buf, xMax, q)) { + continue; + } + char child[board_bytes]; + std::memcpy(child, buf, board_bytes); + child[xMax] = q; + ret += nqueens_serial(xMax + 1, child); + } + return ret; +} + +// fill boards with one row per legal continuation, return how many +static unsigned expand(char* boards, char const* buf, int xMax) { + unsigned count = 0; + for (int y = 0; y < nqueens_work; ++y) { + char q = static_cast(y); + if (!legal_at(buf, xMax, q)) { + continue; + } + char* child = boards + static_cast(count) * board_bytes; + std::memcpy(child, buf, board_bytes); + child[xMax] = q; + ++count; + } + return count; +} + +inline constexpr auto nqueens_chunk = + [](auto, char const* frontier, unsigned begin, unsigned end, + int depth) -> lf::task { + int sum = 0; + for (unsigned i = begin; i < end; ++i) { + sum += nqueens_serial(depth, frontier + static_cast(i) * board_bytes); + } + co_return sum; +}; + +inline constexpr auto nqueens_grouped = + [](auto, unsigned target) -> lf::task { + unsigned capacity = target * max_children; // boards, not bytes + std::vector current(static_cast(capacity) * board_bytes, 0); + std::vector next(static_cast(capacity) * board_bytes); + unsigned count = 1; + int depth = 0; + + while (count < target && depth < nqueens_work) { + unsigned produced = 0; + for (unsigned i = 0; i < count; ++i) { + if (produced + max_children > capacity) { + break; + } + produced += expand( + next.data() + static_cast(produced) * board_bytes, + current.data() + static_cast(i) * board_bytes, depth + ); + } + if (produced == 0) { + break; + } + current.swap(next); + count = produced; + ++depth; + } + + std::vector results(target); + char const* frontier = current.data(); + for (unsigned t = 0; t < target; ++t) { + co_await lf::fork[&results[t], nqueens_chunk]( + frontier, static_cast(static_cast(t) * count / target), + static_cast(static_cast(t + 1) * count / target), depth + ); + } + co_await lf::join; + + int sum = 0; + for (unsigned t = 0; t < target; ++t) { + sum += results[t]; + } + co_return sum; +}; + +// -------------------------------------------------------------------------- + int main(int argc, char* argv[]) { if (argc > 1) { thread_count = static_cast(atoi(argv[1])); } + bench::Config config = bench::parse_config(argc > 2 ? argv[2] : nullptr); + unsigned tasks = + static_cast(thread_count * bench::tasks_per_worker); + std::printf("threads: %" PRIu64 "\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); lf::lazy_pool pool(thread_count); - { + + auto run = [&]() { + if (config == bench::Config::group_shared) { + return lf::sync_wait(pool, nqueens_grouped, tasks); + } std::array buf{}; - auto result = lf::sync_wait(pool, nqueens, 0, buf); // warmup - check_answer(result); - } + return lf::sync_wait(pool, nqueens, 0, buf); + }; + + check_answer(run()); // warmup auto startTime = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < iter_count; ++i) { - - std::array buf{}; - auto result = lf::sync_wait(pool, nqueens, 0, buf); + auto result = run(); check_answer(result); std::printf("output: %d\n", result); } diff --git a/cpp/libfork/skynet.cpp b/cpp/libfork/skynet.cpp index 5291ddd..ee548a2 100644 --- a/cpp/libfork/skynet.cpp +++ b/cpp/libfork/skynet.cpp @@ -28,6 +28,7 @@ // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. +#include "bench_config.hpp" #include "memusage.hpp" #include @@ -35,6 +36,7 @@ #include #include #include +#include static size_t thread_count = std::thread::hardware_concurrency() / 2; static const size_t iter_count = 1; @@ -72,20 +74,99 @@ inline constexpr auto skynet_one = co_return count; }; +inline constexpr size_t skynet_depth = 8; +inline constexpr size_t skynet_fanout = 10; +inline constexpr size_t skynet_expected = 4999999950000000ull; + +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// The tree is cut at the first depth holding at least one node per task, and +// each node of that level is walked to its leaves without the scheduler. Every +// leaf is still visited, so the work is the tree's; only the dispatch changes. + +template size_t skynet_serial(size_t BaseNum, size_t Depth) { + if (Depth == DepthMax) { + return BaseNum; + } + size_t depthOffset = 1; + for (size_t i = 0; i < DepthMax - Depth - 1; ++i) { + depthOffset *= skynet_fanout; + } + size_t count = 0; + for (size_t i = 0; i < skynet_fanout; ++i) { + count += skynet_serial(BaseNum + depthOffset * i, Depth + 1); + } + return count; +} + template -inline constexpr auto skynet = [](auto skynet) -> lf::task { - size_t count = co_await lf::just[skynet_one](0, 0); - if (count != 4999999950000000) { - std::printf("ERROR: wrong result - %" PRIu64 "\n", count); +inline constexpr auto skynet_chunk = + [](auto, size_t begin, size_t end, size_t spacing, size_t depth) -> lf::task { + size_t sum = 0; + for (size_t i = begin; i < end; ++i) { + sum += skynet_serial(i * spacing, depth); + } + co_return sum; +}; + +template +inline constexpr auto skynet_grouped = [](auto, size_t tasks) -> lf::task { + size_t depth = 0; + size_t nodes = 1; + while (nodes < tasks && depth < DepthMax) { + nodes *= skynet_fanout; + ++depth; + } + size_t nodeSpacing = 1; + for (size_t i = 0; i < DepthMax - depth; ++i) { + nodeSpacing *= skynet_fanout; + } + + std::vector results(tasks); + for (size_t t = 0; t < tasks; ++t) { + co_await lf::fork[&results[t], skynet_chunk]( + t * nodes / tasks, (t + 1) * nodes / tasks, nodeSpacing, depth + ); + } + co_await lf::join; + + size_t count = 0; + for (size_t t = 0; t < tasks; ++t) { + count += results[t]; } + co_return count; }; -template -inline constexpr auto loop_skynet = [](auto loop_skynet) -> lf::task { +// -------------------------------------------------------------------------- + +template +void run_skynet(lf::lazy_pool& pool, bench::Config config, size_t tasks) { + size_t count = config == bench::Config::group_shared + ? lf::sync_wait(pool, skynet_grouped, tasks) + : lf::sync_wait(pool, skynet_one, size_t{0}, size_t{0}); + if (count != skynet_expected) { + std::printf("ERROR: wrong result - %" PRIu64 "\n", count); + } +} + +int main(int argc, char* argv[]) { + if (argc > 1) { + thread_count = static_cast(atoi(argv[1])); + } + bench::Config config = bench::parse_config(argc > 2 ? argv[2] : nullptr); + size_t tasks = thread_count * bench::tasks_per_worker; + + std::printf("threads: %" PRIu64 "\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); + lf::lazy_pool pool(thread_count); + + run_skynet(pool, config, tasks); // warmup + std::printf("runs:\n"); auto startTime = std::chrono::high_resolution_clock::now(); for (size_t j = 0; j < iter_count; ++j) { - co_await lf::just[skynet](); + run_skynet(pool, config, tasks); } auto endTime = std::chrono::high_resolution_clock::now(); auto totalTimeUs = @@ -93,14 +174,4 @@ inline constexpr auto loop_skynet = [](auto loop_skynet) -> lf::task { std::printf(" - iteration_count: %" PRIu64 "\n", iter_count); std::printf(" duration: %" PRIu64 " us\n", totalTimeUs.count()); std::printf(" max_rss: %ld KiB\n", peak_memory_usage()); -}; - -int main(int argc, char* argv[]) { - if (argc > 1) { - thread_count = static_cast(atoi(argv[1])); - } - std::printf("threads: %" PRIu64 "\n", thread_count); - lf::lazy_pool pool(thread_count); - lf::sync_wait(pool, skynet<8>); // warmup - lf::sync_wait(pool, loop_skynet<8>); } diff --git a/cpp/schobi/CMakeLists.txt b/cpp/schobi/CMakeLists.txt new file mode 100644 index 0000000..eded587 --- /dev/null +++ b/cpp/schobi/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.16) +project(runtime_benchmarks_schobi) + +set(CMAKE_MODULE_PATH + ${runtime_benchmarks_schobi_SOURCE_DIR}/../1CMake + ${CMAKE_MODULE_PATH}) + +set(CMAKE_EXPORT_COMPILE_COMMANDS "1") +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_definitions( + + # Performance tuning options + "-march=native" +) + +include(../1CMake/CPM.cmake) + +# schobi ships no CMake build of its own, so the library sources are compiled here. +# Each release attaches a "library source" drop - include/ + source/ + tls/ - which is +# what an integrator compiles; a git clone would pull in the test and bench submodules. +set(SCHOBI_VERSION 1.0-RC3-beta) + +if(DEFINED ENV{RUNTIME_BENCHMARKS_LIBRARY_REF}) + set(SCHOBI_VERSION "$ENV{RUNTIME_BENCHMARKS_LIBRARY_REF}") +endif() + +CPMAddPackage( + NAME schobi + URL https://codeberg.org/Schobi/Schobi/releases/download/v${SCHOBI_VERSION}/schobi-${SCHOBI_VERSION}-library-source.zip + DOWNLOAD_ONLY) +# source/compat carries the malloc.h shim the library sources need on Darwin +include_directories(${schobi_SOURCE_DIR}/include ${schobi_SOURCE_DIR}/source/compat) + +# the source list of the release's own build script +set(SCHOBI_SOURCES + ${schobi_SOURCE_DIR}/source/scheduler.c + ${schobi_SOURCE_DIR}/source/event.c + ${schobi_SOURCE_DIR}/source/common/atomic_stack.c + ${schobi_SOURCE_DIR}/source/common/linear_alloc.c + ${schobi_SOURCE_DIR}/source/common/precise_time.c +) + +# arm64 counter/pause are pure arch code; on x64 the pause path is the fault probed +# file in the OS folder, so it follows the architecture rather than the OS +if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") + list(APPEND SCHOBI_SOURCES + ${schobi_SOURCE_DIR}/source/arch/coro_arm64.c + ${schobi_SOURCE_DIR}/source/arch/cpu_arm64.c) +else() + list(APPEND SCHOBI_SOURCES + ${schobi_SOURCE_DIR}/source/arch/coro_x64.c + ${schobi_SOURCE_DIR}/source/os/linux/cpu_x64.c) +endif() + +if(APPLE) + list(APPEND SCHOBI_SOURCES ${schobi_SOURCE_DIR}/source/os/mac/threading.c) +else() + list(APPEND SCHOBI_SOURCES ${schobi_SOURCE_DIR}/source/os/linux/threading.c) +endif() + +# schobi hands out groups from its own linear allocator, but that allocator refills +# from malloc, so the fork-join programs still reach it often enough to care. Linked +# as the rest of the suite does. +find_package(libtcmalloc) + +if(LIBTCMALLOC_FOUND) + set(MALLOC_LIB "${LIBTCMALLOC_LIBRARY}") + message(STATUS "Using malloc: ${MALLOC_LIB}") +else() + find_package(libmimalloc) + + if(LIBMIMALLOC_FOUND) + set(MALLOC_LIB "${LIBMIMALLOC_LIBRARY}") + message(STATUS "Using malloc: ${MALLOC_LIB}") + else() + find_package(libjemalloc) + + if(LIBJEMALLOC_FOUND) + set(MALLOC_LIB "${LIBJEMALLOC_LIBRARY}") + message(STATUS "Using malloc: ${MALLOC_LIB}") + else() + message(STATUS "Using malloc: default") + endif() + endif() +endif() + +link_libraries(${MALLOC_LIB} pthread) + +add_executable(fib fib.c ${SCHOBI_SOURCES}) + +add_executable(skynet skynet.c ${SCHOBI_SOURCES}) + +add_executable(nqueens nqueens.c ${SCHOBI_SOURCES}) + +# nqueens is particularly sensitive to misaligned loops, +# which could cause minor library changes to cause big performance variations +target_compile_options(nqueens PRIVATE "-falign-loops=64") + +add_executable(matmul matmul.c ${SCHOBI_SOURCES}) diff --git a/cpp/schobi/CMakePresets.json b/cpp/schobi/CMakePresets.json new file mode 100644 index 0000000..9b66dd6 --- /dev/null +++ b/cpp/schobi/CMakePresets.json @@ -0,0 +1,359 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "clang-linux-debug", + "displayName": "Clang-Linux Debug", + "generator": "Ninja", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "clang-linux-release", + "displayName": "Clang-Linux Release", + "generator": "Ninja", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "clang-linux-relwithdebinfo", + "displayName": "Clang-Linux Release with Debug Info", + "generator": "Ninja", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "gcc-linux-debug", + "displayName": "GCC-Linux Debug", + "generator": "Ninja", + "description": "Using compilers: C = gcc, CXX = g++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "gcc-linux-release", + "displayName": "GCC-Linux Release", + "generator": "Ninja", + "description": "Using compilers: C = gcc, CXX = g++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "gcc-linux-relwithdebinfo", + "displayName": "GCC-Linux Release with Debug Info", + "generator": "Ninja", + "description": "Using compilers: C = gcc, CXX = g++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "clang-win-debug", + "displayName": "Clang-Win Debug", + "generator": "Ninja", + "description": "Using compiler: clang-cl.exe", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang-cl.exe", + "CMAKE_CXX_COMPILER": "clang-cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "windows-clang-x64" + } + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "clang-win-release", + "displayName": "Clang-Win Release", + "generator": "Ninja", + "description": "Using compiler: clang-cl.exe", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang-cl.exe", + "CMAKE_CXX_COMPILER": "clang-cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "windows-clang-x64" + } + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "clang-win-relwithdebinfo", + "displayName": "Clang-Win Release with Debug Info", + "generator": "Ninja", + "description": "Using compiler: clang-cl.exe", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_C_COMPILER": "clang-cl.exe", + "CMAKE_CXX_COMPILER": "clang-cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "intelliSenseMode": "windows-clang-x64" + } + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-win-debug", + "displayName": "MSVC-Win Debug", + "description": "Using compiler: cl.exe", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-win-release", + "displayName": "MSVC-Win Release", + "description": "Using compiler: cl.exe", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_FLAGS": "/DWIN32 /D_WINDOWS /W3 /GR /EHsc /arch:AVX2", + "CMAKE_C_FLAGS": "/DWIN32 /D_WINDOWS /W3 /arch:AVX2" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "msvc-win-relwithdebinfo", + "displayName": "MSVC-Win Release with Debug Info", + "description": "Using compiler: cl.exe", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_FLAGS": "/DWIN32 /D_WINDOWS /W3 /GR /EHsc /arch:AVX2", + "CMAKE_C_FLAGS": "/DWIN32 /D_WINDOWS /W3 /arch:AVX2", + "CMAKE_CXX_FLAGS_RELWITHDEBINFO": "/MD /Zi /O2 /Ob2 /DNDEBUG", + "CMAKE_C_FLAGS_RELWITHDEBINFO": "/MD /Zi /O2 /Ob2 /DNDEBUG", + "CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO": "/debug /INCREMENTAL:NO", + "CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO": "/debug /INCREMENTAL:NO", + "CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO": "/debug /INCREMENTAL:NO" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "clang-macos-debug", + "displayName": "Clang-MacOS Debug", + "generator": "Unix Makefiles", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fexperimental-library", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "environment": { + "CMAKE_BUILD_PARALLEL_LEVEL": "8" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "clang-macos-release", + "displayName": "Clang-MacOS Release", + "generator": "Unix Makefiles", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fexperimental-library", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "environment": { + "CMAKE_BUILD_PARALLEL_LEVEL": "8" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "clang-macos-relwithdebinfo", + "displayName": "Clang-MacOS Release with Debug Info", + "generator": "Unix Makefiles", + "description": "Using compilers: C = clang, CXX = clang++", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_CXX_FLAGS": "-fexperimental-library", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "environment": { + "CMAKE_BUILD_PARALLEL_LEVEL": "8" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + } + ] +} \ No newline at end of file diff --git a/cpp/schobi/bench_common.h b/cpp/schobi/bench_common.h new file mode 100644 index 0000000..26d97da --- /dev/null +++ b/cpp/schobi/bench_common.h @@ -0,0 +1,159 @@ +#pragma once +/* Scheduler bringup, timing, temporary memory, and the reduction slot the + * group-shared implementations fold into. + */ +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#else +#include +#include +#endif + +/* Which of the three implementations a benchmark runs. Selected by the config + * argument the harness appends after the thread count. + */ +enum BenchConfig +{ + BENCH_SEQUENTIAL, /* no scheduler at all - the baseline the others are measured against */ + BENCH_FORK_JOIN, /* recursive fork/join, the program every runtime in this suite runs */ + BENCH_GROUP_SHARED, /* one group, tasks divide the work and fold partials through barriers */ +}; + +#define BENCH_CONFIG_SEQUENTIAL_NAME "sequential" +#define BENCH_CONFIG_GROUP_SHARED_NAME "group-shared" +#define BENCH_CONFIG_FORK_JOIN_NAME "fork-join" + +static inline enum BenchConfig bench_parse_config(const char* name) +{ + if (name == NULL) + return BENCH_FORK_JOIN; + if (strcmp(name, BENCH_CONFIG_SEQUENTIAL_NAME) == 0) + return BENCH_SEQUENTIAL; + if (strcmp(name, BENCH_CONFIG_GROUP_SHARED_NAME) == 0) + return BENCH_GROUP_SHARED; + return BENCH_FORK_JOIN; +} + +static inline const char* bench_config_name(enum BenchConfig config) +{ + switch (config) + { + case BENCH_SEQUENTIAL: + return BENCH_CONFIG_SEQUENTIAL_NAME; + case BENCH_GROUP_SHARED: + return BENCH_CONFIG_GROUP_SHARED_NAME; + case BENCH_FORK_JOIN: + break; + } + return BENCH_CONFIG_FORK_JOIN_NAME; +} + +/* tasks per worker in the group-shared implementations, and the stack each fiber + * task parks on - a barrier needs a fiber, and a fiber needs a retained stack + */ +#define BENCH_TASKS_PER_WORKER 8 +#define BENCH_FIBER_STACK_KB 16 + +/* a group whose tasks each carry their own share wants every index individually + * stealable, so the fiber launches tile 1x1 + */ +#define BENCH_FIBER_TILE_X 1 +#define BENCH_FIBER_TILE_Y 1 + +/* A worker whose await cannot be satisfied by rushing hands its core to a + * replacement body. The fork/join programs block inside a task at every level of + * the recursion, so past this exponent a blocked worker waits instead. + */ +#define BENCH_POW_LAUNCH_DELAY 8 + +/* Short lived scratch - frontiers, board lists, reduction slots - comes from + * schobi's linear allocator rather than malloc: a bump pointer into a cached + * page, which is what this memory wants since it lives for exactly one launch. + */ +#define BENCH_TEMP_PAGE LAPS_64KB +#define BENCH_TEMP_REF_COUNT 1 +#define BENCH_CACHE_LINE 64 + +static inline void* bench_temp_alloc(size_t size, size_t alignment) +{ + return schobi_linear_alloc(size, alignment, BENCH_TEMP_REF_COUNT, BENCH_TEMP_PAGE); +} + +static inline void* bench_temp_alloc_zeroed(size_t size, size_t alignment) +{ + void* memory = bench_temp_alloc(size, alignment); + memset(memory, 0, size); + return memory; +} + +static inline void bench_temp_free(void* memory) +{ + schobi_linear_free(memory, BENCH_TEMP_REF_COUNT, BENCH_TEMP_PAGE); +} + +/* one reduction slot per task, each on its own cache line */ +struct BenchPartial +{ + _Alignas(BENCH_CACHE_LINE) size_t value; + char pad[BENCH_CACHE_LINE - sizeof(size_t)]; +}; + +static inline struct BenchPartial* bench_partials(unsigned count) +{ + return (struct BenchPartial*)bench_temp_alloc_zeroed( + count * sizeof(struct BenchPartial), BENCH_CACHE_LINE); +} + +static inline void bench_free_partials(struct BenchPartial* slots) +{ + bench_temp_free(slots); +} + +static inline unsigned bench_default_threads(void) +{ +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + unsigned n = (unsigned)info.dwNumberOfProcessors; +#else + unsigned n = (unsigned)sysconf(_SC_NPROCESSORS_ONLN); +#endif + return n > 1 ? n / 2 : 1; +} + +static inline struct SchobiScheduler* bench_start_workers(unsigned threads) +{ + struct SchobiSchedulerDesc desc; + schobi_default_scheduler_desc(&desc); + desc.core_start_index = 0; + desc.core_end_index = threads; + desc.pow_launch_delay = BENCH_POW_LAUNCH_DELAY; + return schobi_start_workers(&desc); +} + +/* a monotonic clock outside the library under test */ +static inline double bench_seconds(void) +{ +#ifdef _WIN32 + LARGE_INTEGER freq, now; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&now); + return (double)now.QuadPart / (double)freq.QuadPart; +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +#endif +} + +static inline uint64_t bench_elapsed_us(double start) +{ + return (uint64_t)((bench_seconds() - start) * 1e6 + 0.5); +} diff --git a/cpp/schobi/build_all.sh b/cpp/schobi/build_all.sh new file mode 100644 index 0000000..f5a6019 --- /dev/null +++ b/cpp/schobi/build_all.sh @@ -0,0 +1,3 @@ +PRESET=${1:-"clang-linux-release"} +cmake --preset $PRESET . +cmake --build ./build --parallel 16 --target all diff --git a/cpp/schobi/fib.c b/cpp/schobi/fib.c new file mode 100644 index 0000000..442e95b --- /dev/null +++ b/cpp/schobi/fib.c @@ -0,0 +1,251 @@ +/* An implementation of the recursive fork fibonacci parallelism test. + + Adapted from https://github.com/tzcnt/tmc-examples/blob/main/examples/fib.cpp + Original author: tzcnt + Unlicense License + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. */ + +/* Three implementations, selected by the config argument: + + "sequential" the naive recursion on one thread, no scheduler started. + + (default) fork-join - a group of 2 per node, recursing to n < 2. + + "group-shared" fib(n) expanded breadth-first into a frontier of + subproblems, which one fiber group divides between its + tasks and reduces through group barriers. */ + +#include "bench_common.h" +#include "memusage.h" +#include + +#include +#include + +#define FIB_WARMUP_N 30 +#define FIB_LEAF 2 /* below this a node is its own value */ + +/* the two children of a node: index 0 takes n-1, index 1 takes n-2 */ +#define FIB_FORK_WAYS 2 + +/* room for the frontier to overshoot its target on the last expansion pass */ +#define FIB_FRONTIER_SLOTS_PER_TASK 4 + +static const unsigned iter_count = 1; + +/* ------------------------------------------------------------------------- */ +/* sequential */ +/* ------------------------------------------------------------------------- */ +static size_t fib_sequential(size_t n) +{ + return n < FIB_LEAF ? n : fib_sequential(n - 1) + fib_sequential(n - 2); +} + +/* ------------------------------------------------------------------------- */ +/* fork-join */ +/* ------------------------------------------------------------------------- */ +static size_t fib(struct SchobiScheduler* scheduler, size_t n); + +/* One group of FIB_FORK_WAYS per node, one return slot each. The tile stays 1x1 + * so both indices are individually stealable; tiling would coarsen the tail steal + * that provides the load balance. + */ +SCHOBI_DECLARE_CLOSURE(size_t, FibAsyncTask, (size_t, n)) +SCHOBI_CLOSURE(size_t, FibAsyncTask, (size_t, n)) +{ + SCHOBI_CLOSURE_ENV(); + return fib(schobi_scheduler, n - 1 - schobi_index); +} + +static size_t fib(struct SchobiScheduler* scheduler, size_t n) +{ + if (n < FIB_LEAF) + return n; + + FibAsyncTask* handle = schobi_async(scheduler, (FIB_FORK_WAYS, FibAsyncTask, n)); + schobi_await(handle); + size_t result = 0; + for (unsigned i = 0; i < FIB_FORK_WAYS; ++i) + result += schobi_return_value(handle, i); + schobi_release(handle); + return result; +} + +/* the root runs inside a one-task group so the recursion sits on a worker rather + * than on the calling thread + */ +SCHOBI_DECLARE_CLOSURE(size_t, FibAsyncRootTask, (size_t, n)) +SCHOBI_CLOSURE(size_t, FibAsyncRootTask, (size_t, n)) +{ + SCHOBI_CLOSURE_ENV(); + return fib(schobi_scheduler, n); +} + +static size_t fib_async(struct SchobiScheduler* scheduler, size_t n) +{ + FibAsyncRootTask* root = schobi_async(scheduler, (1, FibAsyncRootTask, n)); + schobi_await(root); + size_t result = schobi_return_value(root, 0); + schobi_release(root); + return result; +} + +/* ------------------------------------------------------------------------- */ +/* group-shared */ +/* ------------------------------------------------------------------------- */ +/* Each task sums its share of the frontier, publishes the partial, then the group + * folds pairwise: neighbours combine across log2(tasks) rounds with a barrier + * between them. A barrier parks a task on its retained stack, and any free worker + * resumes it, so the exchange never leaves the group. + */ +SCHOBI_DECLARE_CLOSURE_VOID(FibGroupTask, (const unsigned*, frontier)(unsigned, count)(unsigned, tasks)(struct BenchPartial*, slots)) +SCHOBI_CLOSURE_VOID(FibGroupTask, (const unsigned*, frontier)(unsigned, count)(unsigned, tasks)(struct BenchPartial*, slots)) +{ + SCHOBI_CLOSURE_ENV(); + size_t sum = 0; + for (unsigned i = schobi_index; i < count; i += tasks) + sum += fib_sequential(frontier[i]); + slots[schobi_index].value = sum; + + /* publish before any neighbour reads */ + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + + /* every task crosses every barrier; in each round the tasks sitting at a + * multiple of twice the stride absorb the neighbour one stride above them + */ + for (unsigned stride = 1; stride < tasks; stride *= 2) + { + if ((schobi_index % (2 * stride)) == 0 && schobi_index + stride < tasks) + slots[schobi_index].value += slots[schobi_index + stride].value; + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + } +} + +/* Expand fib(n) breadth-first until there are enough subproblems to divide. + * Replacing v with v-1 and v-2 keeps sum(fib(frontier)) equal to fib(n), so a + * short frontier costs parallelism and never correctness. + */ +static unsigned build_frontier(unsigned* frontier, unsigned capacity, size_t n, unsigned target) +{ + unsigned count = 1; + frontier[0] = (unsigned)n; + for (;;) + { + if (count >= target || count * 2 > capacity) + break; + unsigned split = 0; + for (unsigned i = count; i-- > 0;) + { + unsigned v = frontier[i]; + if (v < FIB_LEAF) + continue; + frontier[i] = v - 1; + frontier[count + split] = v - 2; + ++split; + } + if (split == 0) + break; + count += split; + } + return count; +} + +static size_t fib_group(struct SchobiScheduler* scheduler, size_t n, unsigned tasks) +{ + unsigned capacity = tasks * FIB_FRONTIER_SLOTS_PER_TASK; + unsigned* frontier = + (unsigned*)bench_temp_alloc(sizeof(unsigned) * capacity, _Alignof(unsigned)); + struct BenchPartial* slots = bench_partials(tasks); + unsigned count = build_frontier(frontier, capacity, n, tasks); + + /* barriers are fiber-only: every task needs a retained stack to park on */ + schobi_parallel_for_fibers(BENCH_FIBER_STACK_KB, BENCH_FIBER_TILE_X, BENCH_FIBER_TILE_Y)( + scheduler, (tasks, FibGroupTask, frontier, count, tasks, slots)); + + size_t result = slots[0].value; + bench_temp_free(frontier); + bench_free_partials(slots); + return result; +} + +/* ------------------------------------------------------------------------- */ + +static size_t fib_run(struct SchobiScheduler* scheduler, enum BenchConfig config, + size_t n, unsigned tasks) +{ + switch (config) + { + case BENCH_SEQUENTIAL: + return fib_sequential(n); + case BENCH_GROUP_SHARED: + return fib_group(scheduler, n, tasks); + case BENCH_FORK_JOIN: + break; + } + return fib_async(scheduler, n); +} + +int main(int argc, char* argv[]) +{ + unsigned thread_count = bench_default_threads(); + if (argc > 2) + thread_count = (unsigned)atoi(argv[2]); + if (argc < 2) + { + printf("Usage: fib [threads] [%s|%s]\n", + BENCH_CONFIG_SEQUENTIAL_NAME, BENCH_CONFIG_GROUP_SHARED_NAME); + return 0; + } + size_t n = (size_t)atoi(argv[1]); + enum BenchConfig config = bench_parse_config(argc > 3 ? argv[3] : NULL); + + printf("threads: %u\n", config == BENCH_SEQUENTIAL ? 1u : thread_count); + printf("config: %s\n", bench_config_name(config)); + + struct SchobiScheduler* scheduler = NULL; + if (config != BENCH_SEQUENTIAL) + scheduler = bench_start_workers(thread_count); + unsigned tasks = thread_count * BENCH_TASKS_PER_WORKER; + + fib_run(scheduler, config, FIB_WARMUP_N, tasks); /* warmup */ + + double start = bench_seconds(); + + for (unsigned i = 0; i < iter_count; ++i) + { + size_t result = fib_run(scheduler, config, n, tasks); + printf("output: %zu\n", result); + } + + uint64_t elapsed_us = bench_elapsed_us(start); + + if (scheduler != NULL) + schobi_stop_workers(scheduler); + + printf("runs:\n"); + printf(" - iteration_count: %u\n", iter_count); + printf(" duration: %llu us\n", (unsigned long long)elapsed_us); + printf(" max_rss: %ld KiB\n", peak_memory_usage()); + return 0; +} diff --git a/cpp/schobi/matmul.c b/cpp/schobi/matmul.c new file mode 100644 index 0000000..b5790c0 --- /dev/null +++ b/cpp/schobi/matmul.c @@ -0,0 +1,264 @@ +/* An implementation of recursive matrix multiplication + + Adapted from + https://github.com/mtmucha/coros/blob/main/benchmarks/coros_mat.h + + Original author: mtmucha + Distributed under the Boost Software License, Version 1.0. (See accompanying + file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt) */ + +/* Three implementations, selected by the config argument: + + "sequential" the blocked multiply on one thread, no scheduler started. + + (default) fork-join - recursive 4-way quadrant split in two phases. + The phases exist because both halves of a product write the + same output quadrant, so they cannot run concurrently. + + "group-shared" one tiled group over the blocks of C, each accumulating over + the k blocks. Every block of C has a single writer, so no + phase split is needed and the whole multiply is one launch. */ + +#include "bench_common.h" +#include "memusage.h" +#include + +#include +#include +#include + +#define MATMUL_BLOCK 32 /* base case of the recursion */ + +/* the quadrant split: bit 1 of the index picks the row half, bit 0 the column + * half, and the two phases are the two products that land on the same quadrant + */ +#define MATMUL_QUADRANTS 4 +#define MATMUL_PHASES 2 + +/* The blocked implementations cut the output on their own grid rather than at the + * recursion base case, so this is sized for the cache: a task holds a block of C + * live while streaming a block row of A against a block column of B, and a 32x32 + * int block is too small to pay for that traffic. The sequential and group-shared + * paths share it - same kernel, same working set, so the comparison between them + * isolates the scheduling. + */ +#ifndef MATMUL_BLOCKED_BLOCK +#define MATMUL_BLOCKED_BLOCK 128 +#endif + +/* a tile is the x*y contiguous tasks a worker grabs at once; it also bounds how + * many workers a launch can occupy, since a tile is never split + */ +#ifndef MATMUL_TILE_X +#define MATMUL_TILE_X 4 +#endif +#define MATMUL_TILE_Y 1 + +static void matmul_small(int* a, int* b, int* c, int n, int N) +{ + for (int i = 0; i < n; i++) + for (int k = 0; k < n; k++) + for (int j = 0; j < n; j++) + c[i * N + j] += a[i * N + k] * b[k * N + j]; +} + +/* ------------------------------------------------------------------------- */ +/* sequential */ +/* ------------------------------------------------------------------------- */ +static void matmul_sequential(int* a, int* b, int* c, int N) +{ + int nb = N / MATMUL_BLOCKED_BLOCK; + for (int bi = 0; bi < nb; ++bi) + for (int bj = 0; bj < nb; ++bj) + { + int* cblock = c + bi * MATMUL_BLOCKED_BLOCK * N + bj * MATMUL_BLOCKED_BLOCK; + for (int k = 0; k < nb; ++k) + matmul_small(a + bi * MATMUL_BLOCKED_BLOCK * N + k * MATMUL_BLOCKED_BLOCK, + b + k * MATMUL_BLOCKED_BLOCK * N + bj * MATMUL_BLOCKED_BLOCK, + cblock, MATMUL_BLOCKED_BLOCK, N); + } +} + +/* ------------------------------------------------------------------------- */ +/* fork-join */ +/* ------------------------------------------------------------------------- */ +static void matmul(struct SchobiScheduler* scheduler, int* a, int* b, int* c, int n, int N); + +SCHOBI_DECLARE_CLOSURE_VOID(MatmulAsyncTask, (int*, a)(int*, b)(int*, c)(int, k)(int, N)(int, phase)) +SCHOBI_CLOSURE_VOID(MatmulAsyncTask, (int*, a)(int*, b)(int*, c)(int, k)(int, N)(int, phase)) +{ + SCHOBI_CLOSURE_ENV(); + int row = (int)(schobi_index >> 1); + int col = (int)(schobi_index & 1); + matmul(schobi_scheduler, + a + (phase ? k : 0) + row * k * N, + b + (phase ? k * N : 0) + col * k, + c + row * k * N + col * k, k, N); +} + +static void matmul(struct SchobiScheduler* scheduler, int* a, int* b, int* c, int n, int N) +{ + if (n <= MATMUL_BLOCK) + { + matmul_small(a, b, c, n, N); + return; + } + + int k = n / 2; + + /* the phases run in sequence so output locations are not written in parallel */ + for (int phase = 0; phase < MATMUL_PHASES; ++phase) + schobi_parallel_for(scheduler, (MATMUL_QUADRANTS, MatmulAsyncTask, a, b, c, k, N, phase)); +} + +SCHOBI_DECLARE_CLOSURE_VOID(MatmulAsyncRootTask, (int*, a)(int*, b)(int*, c)(int, N)) +SCHOBI_CLOSURE_VOID(MatmulAsyncRootTask, (int*, a)(int*, b)(int*, c)(int, N)) +{ + SCHOBI_CLOSURE_ENV(); + matmul(schobi_scheduler, a, b, c, N, N); +} + +static void matmul_async(struct SchobiScheduler* scheduler, int* a, int* b, int* c, int N) +{ + schobi_parallel_for(scheduler, (1, MatmulAsyncRootTask, a, b, c, N)); +} + +/* ------------------------------------------------------------------------- */ +/* group-shared */ +/* ------------------------------------------------------------------------- */ +/* One task per block of C, accumulating over the k blocks. The index is the block + * in row-major order, so a tile of x consecutive indices is a contiguous strip of + * C and the tasks in a tile share their block row of A. + */ +SCHOBI_DECLARE_CLOSURE_VOID(MatmulGroupTask, (int*, a)(int*, b)(int*, c)(int, N)(int, nb)) +SCHOBI_CLOSURE_VOID(MatmulGroupTask, (int*, a)(int*, b)(int*, c)(int, N)(int, nb)) +{ + SCHOBI_CLOSURE_ENV(); + int bi = (int)schobi_index / nb; + int bj = (int)schobi_index % nb; + int* cblock = c + bi * MATMUL_BLOCKED_BLOCK * N + bj * MATMUL_BLOCKED_BLOCK; + for (int k = 0; k < nb; ++k) + matmul_small(a + bi * MATMUL_BLOCKED_BLOCK * N + k * MATMUL_BLOCKED_BLOCK, + b + k * MATMUL_BLOCKED_BLOCK * N + bj * MATMUL_BLOCKED_BLOCK, + cblock, MATMUL_BLOCKED_BLOCK, N); +} + +static void matmul_group(struct SchobiScheduler* scheduler, int* a, int* b, int* c, int N) +{ + int nb = N / MATMUL_BLOCKED_BLOCK; + unsigned tasks = (unsigned)nb * (unsigned)nb; + schobi_parallel_for_tiled(MATMUL_TILE_X, MATMUL_TILE_Y)( + scheduler, (tasks, MatmulGroupTask, a, b, c, N, nb)); +} + +/* ------------------------------------------------------------------------- */ + +/* the blocked implementations tile the whole matrix, so a size they do not + * divide would leave part of C unwritten + */ +static bool matmul_size_supported(enum BenchConfig config, int N) +{ + switch (config) + { + case BENCH_SEQUENTIAL: + case BENCH_GROUP_SHARED: + return N % MATMUL_BLOCKED_BLOCK == 0; + case BENCH_FORK_JOIN: + break; + } + return true; +} + +static void matmul_run(struct SchobiScheduler* scheduler, enum BenchConfig config, + int* a, int* b, int* c, int N) +{ + switch (config) + { + case BENCH_SEQUENTIAL: + matmul_sequential(a, b, c, N); + return; + case BENCH_GROUP_SHARED: + matmul_group(scheduler, a, b, c, N); + return; + case BENCH_FORK_JOIN: + break; + } + matmul_async(scheduler, a, b, c, N); +} + +static void validate_result(const int* c, int N) +{ + for (int i = 0; i < N; i++) + for (int j = 0; j < N; j++) + { + int res = c[i * N + j]; + if (res != N) + { + printf("Wrong result at (%d,%d) : %d. expected %d\n", i, j, res, N); + fflush(stdout); + abort(); + } + } +} + +int main(int argc, char* argv[]) +{ + unsigned thread_count = bench_default_threads(); + if (argc > 2) + thread_count = (unsigned)atoi(argv[2]); + if (argc < 2) + { + printf("Usage: matmul [threads] [%s|%s]\n", + BENCH_CONFIG_SEQUENTIAL_NAME, BENCH_CONFIG_GROUP_SHARED_NAME); + return 0; + } + int n = atoi(argv[1]); + enum BenchConfig config = bench_parse_config(argc > 3 ? argv[3] : NULL); + + if (!matmul_size_supported(config, n)) + { + printf("matrix size %d is not a multiple of the %s block size\n", n, + bench_config_name(config)); + return 1; + } + + printf("threads: %u\n", config == BENCH_SEQUENTIAL ? 1u : thread_count); + printf("config: %s\n", bench_config_name(config)); + + struct SchobiScheduler* scheduler = NULL; + if (config != BENCH_SEQUENTIAL) + scheduler = bench_start_workers(thread_count); + + size_t cells = (size_t)n * (size_t)n; + int* A = (int*)malloc(cells * sizeof(int)); + int* B = (int*)malloc(cells * sizeof(int)); + int* C = (int*)malloc(cells * sizeof(int)); + for (size_t i = 0; i < cells; ++i) + { + A[i] = 1; + B[i] = 1; + } + + memset(C, 0, cells * sizeof(int)); + matmul_run(scheduler, config, A, B, C, n); /* warmup */ + + printf("runs:\n"); + + memset(C, 0, cells * sizeof(int)); + double start = bench_seconds(); + matmul_run(scheduler, config, A, B, C, n); + uint64_t elapsed_us = bench_elapsed_us(start); + validate_result(C, n); + + if (scheduler != NULL) + schobi_stop_workers(scheduler); + + printf(" - matrix_size: %d\n", n); + printf(" duration: %llu us\n", (unsigned long long)elapsed_us); + printf(" max_rss: %ld KiB\n", peak_memory_usage()); + + free(A); + free(B); + free(C); + return 0; +} diff --git a/cpp/schobi/memusage.h b/cpp/schobi/memusage.h new file mode 100644 index 0000000..043107a --- /dev/null +++ b/cpp/schobi/memusage.h @@ -0,0 +1,36 @@ +#pragma once +/* The C flavor of cpp/2common/memusage.hpp - schobi's benchmarks are C17, and + * that header is C++ (brace-initialized rusage). Same measurement, same units. + */ +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#else +#include +#include +#endif + +/** + * Returns peak memory usage in KiB + */ +static inline long peak_memory_usage(void) +{ +#if defined(_WIN32) + PROCESS_MEMORY_COUNTERS pmc; + if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) + return (long)(pmc.PeakWorkingSetSize / 1024); +#else /* Linux/BSD */ + struct rusage usage; + if (getrusage(RUSAGE_SELF, &usage) == 0) + { + /* BSD based systems return maxrss in bytes, on Linux it is in kilobytes */ +#if defined(BSD) || defined(__FreeBSD__) || defined(__APPLE__) + return usage.ru_maxrss / 1024; +#else + return usage.ru_maxrss; +#endif + } +#endif + return -1; +} diff --git a/cpp/schobi/nqueens.c b/cpp/schobi/nqueens.c new file mode 100644 index 0000000..55c48eb --- /dev/null +++ b/cpp/schobi/nqueens.c @@ -0,0 +1,296 @@ +/* Adapted from the benchmark provided at: + https://github.com/ConorWilliams/libfork/blob/ce40fa0f3178a43f5da8016788d6cfdadc85554f/bench/source/nqueens/libfork.cpp + + Original Copyright Notice: + Copyright (c) Conor Williams + + SPDX-License-Identifier: MPL-2.0 + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* Three implementations, selected by the config argument: + + "sequential" the backtracking search on one thread, no scheduler started. + + (default) fork-join - a group per node, recursing to the full board. + + "group-shared" the search tree expanded breadth-first into a frontier of + partial boards, which one fiber group divides between its + tasks and folds through group barriers. */ + +#include "bench_common.h" +#include "memusage.h" +#include + +#include +#include +#include + +#define NQUEENS_WORK 14 +#define NQUEENS_ANSWER 365596 + +/* a row holds one board, and a node can expand into at most NQUEENS_WORK of them */ +#define NQUEENS_BOARD_BYTES NQUEENS_WORK +#define NQUEENS_MAX_CHILDREN NQUEENS_WORK + +static const unsigned iter_count = 1; + +static void check_answer(int result) +{ + if (result != NQUEENS_ANSWER) + printf("error: expected %d, got %d\n", NQUEENS_ANSWER, result); +} + +static inline bool legal_at(const char* buf, int xMax, char q) +{ + for (int x = 0; x < xMax; ++x) + { + char p = buf[x]; + if (q == p || q == p - (xMax - x) || q == p + (xMax - x)) + return false; + } + return true; +} + +/* fill boards with one row per legal continuation, return how many */ +static unsigned expand(char* boards, const char* buf, int xMax) +{ + unsigned count = 0; + for (int y = 0; y < NQUEENS_WORK; ++y) + { + char q = (char)y; + if (!legal_at(buf, xMax, q)) + continue; + char* child = boards + (size_t)count * NQUEENS_BOARD_BYTES; + memcpy(child, buf, NQUEENS_BOARD_BYTES); + child[xMax] = q; + ++count; + } + return count; +} + +/* ------------------------------------------------------------------------- */ +/* sequential */ +/* ------------------------------------------------------------------------- */ +static int nqueens_sequential(int xMax, const char* buf) +{ + if (xMax == NQUEENS_WORK) + return 1; + + int ret = 0; + for (int y = 0; y < NQUEENS_WORK; ++y) + { + char q = (char)y; + if (!legal_at(buf, xMax, q)) + continue; + char child[NQUEENS_BOARD_BYTES]; + memcpy(child, buf, NQUEENS_BOARD_BYTES); + child[xMax] = q; + ret += nqueens_sequential(xMax + 1, child); + } + return ret; +} + +/* ------------------------------------------------------------------------- */ +/* fork-join */ +/* ------------------------------------------------------------------------- */ +static int nqueens(struct SchobiScheduler* scheduler, int xMax, const char* buf); + +/* The child boards are built once in the parent and the group is handed the + * array; each index reads its own row, so the group needs one copy of the boards + * rather than one per fork. + */ +SCHOBI_DECLARE_CLOSURE(int, NQueensAsyncTask, (const char*, boards)(int, xMax)) +SCHOBI_CLOSURE(int, NQueensAsyncTask, (const char*, boards)(int, xMax)) +{ + SCHOBI_CLOSURE_ENV(); + return nqueens(schobi_scheduler, xMax + 1, + boards + (size_t)schobi_index * NQUEENS_BOARD_BYTES); +} + +static int nqueens(struct SchobiScheduler* scheduler, int xMax, const char* buf) +{ + if (xMax == NQUEENS_WORK) + return 1; + + char boards[NQUEENS_MAX_CHILDREN * NQUEENS_BOARD_BYTES]; + unsigned taskCount = expand(boards, buf, xMax); + if (taskCount == 0) + return 0; + + NQueensAsyncTask* handle = schobi_async(scheduler, (taskCount, NQueensAsyncTask, boards, xMax)); + schobi_await(handle); + int ret = 0; + for (unsigned i = 0; i < taskCount; ++i) + ret += schobi_return_value(handle, i); + schobi_release(handle); + return ret; +} + +/* the root runs inside a one-task group so the recursion sits on a worker rather + * than on the calling thread + */ +SCHOBI_DECLARE_CLOSURE(int, NQueensAsyncRootTask, (int, unused)) +SCHOBI_CLOSURE(int, NQueensAsyncRootTask, (int, unused)) +{ + SCHOBI_CLOSURE_ENV(); + (void)unused; + char buf[NQUEENS_BOARD_BYTES]; + memset(buf, 0, sizeof(buf)); + return nqueens(schobi_scheduler, 0, buf); +} + +static int nqueens_async(struct SchobiScheduler* scheduler) +{ + NQueensAsyncRootTask* root = schobi_async(scheduler, (1, NQueensAsyncRootTask, 0)); + schobi_await(root); + int result = schobi_return_value(root, 0); + schobi_release(root); + return result; +} + +/* ------------------------------------------------------------------------- */ +/* group-shared */ +/* ------------------------------------------------------------------------- */ +/* Each task searches its share of the frontier, publishes the partial, then the + * group folds pairwise: neighbours combine across log2(tasks) rounds with a + * barrier between them. A barrier parks a task on its retained stack, and any + * free worker resumes it, so the exchange never leaves the group. + */ +SCHOBI_DECLARE_CLOSURE_VOID(NQueensGroupTask, (const char*, frontier)(unsigned, count)(unsigned, tasks)(int, depth)(struct BenchPartial*, slots)) +SCHOBI_CLOSURE_VOID(NQueensGroupTask, (const char*, frontier)(unsigned, count)(unsigned, tasks)(int, depth)(struct BenchPartial*, slots)) +{ + SCHOBI_CLOSURE_ENV(); + size_t sum = 0; + for (unsigned i = schobi_index; i < count; i += tasks) + sum += (size_t)nqueens_sequential(depth, frontier + (size_t)i * NQUEENS_BOARD_BYTES); + slots[schobi_index].value = sum; + + /* publish before any neighbour reads */ + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + + /* every task crosses every barrier; in each round the tasks sitting at a + * multiple of twice the stride absorb the neighbour one stride above them + */ + for (unsigned stride = 1; stride < tasks; stride *= 2) + { + if ((schobi_index % (2 * stride)) == 0 && schobi_index + stride < tasks) + slots[schobi_index].value += slots[schobi_index + stride].value; + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + } +} + +/* Expand level by level until the frontier is wide enough to divide. Every board + * in it has the same number of columns placed, so one depth describes all. + */ +static unsigned build_frontier(char** frontier, char** scratch, unsigned target, int* out_depth) +{ + unsigned capacity = target * NQUEENS_MAX_CHILDREN; /* boards, not bytes */ + char* current = (char*)bench_temp_alloc_zeroed((size_t)capacity * NQUEENS_BOARD_BYTES, 1); + char* next = (char*)bench_temp_alloc((size_t)capacity * NQUEENS_BOARD_BYTES, 1); + unsigned count = 1; + int depth = 0; + + while (count < target && depth < NQUEENS_WORK) + { + unsigned produced = 0; + for (unsigned i = 0; i < count; ++i) + { + if (produced + NQUEENS_MAX_CHILDREN > capacity) + break; + produced += expand(next + (size_t)produced * NQUEENS_BOARD_BYTES, + current + (size_t)i * NQUEENS_BOARD_BYTES, depth); + } + if (produced == 0) + break; + char* swap = current; + current = next; + next = swap; + count = produced; + ++depth; + } + + *out_depth = depth; + *frontier = current; + *scratch = next; + return count; +} + +static int nqueens_group(struct SchobiScheduler* scheduler, unsigned tasks) +{ + char* frontier = NULL; + char* scratch = NULL; + int depth = 0; + unsigned count = build_frontier(&frontier, &scratch, tasks, &depth); + struct BenchPartial* slots = bench_partials(tasks); + + /* barriers are fiber-only: every task needs a retained stack to park on */ + schobi_parallel_for_fibers(BENCH_FIBER_STACK_KB, BENCH_FIBER_TILE_X, BENCH_FIBER_TILE_Y)( + scheduler, (tasks, NQueensGroupTask, frontier, count, tasks, depth, slots)); + + int result = (int)slots[0].value; + bench_temp_free(frontier); + bench_temp_free(scratch); + bench_free_partials(slots); + return result; +} + +/* ------------------------------------------------------------------------- */ + +static int nqueens_run(struct SchobiScheduler* scheduler, enum BenchConfig config, unsigned tasks) +{ + switch (config) + { + case BENCH_SEQUENTIAL: + { + char buf[NQUEENS_BOARD_BYTES]; + memset(buf, 0, sizeof(buf)); + return nqueens_sequential(0, buf); + } + case BENCH_GROUP_SHARED: + return nqueens_group(scheduler, tasks); + case BENCH_FORK_JOIN: + break; + } + return nqueens_async(scheduler); +} + +int main(int argc, char* argv[]) +{ + unsigned thread_count = bench_default_threads(); + if (argc > 1) + thread_count = (unsigned)atoi(argv[1]); + enum BenchConfig config = bench_parse_config(argc > 2 ? argv[2] : NULL); + + printf("threads: %u\n", config == BENCH_SEQUENTIAL ? 1u : thread_count); + printf("config: %s\n", bench_config_name(config)); + + struct SchobiScheduler* scheduler = NULL; + if (config != BENCH_SEQUENTIAL) + scheduler = bench_start_workers(thread_count); + unsigned tasks = thread_count * BENCH_TASKS_PER_WORKER; + + check_answer(nqueens_run(scheduler, config, tasks)); /* warmup */ + + double start = bench_seconds(); + + for (unsigned i = 0; i < iter_count; ++i) + { + int result = nqueens_run(scheduler, config, tasks); + check_answer(result); + printf("output: %d\n", result); + } + + uint64_t elapsed_us = bench_elapsed_us(start); + + if (scheduler != NULL) + schobi_stop_workers(scheduler); + + printf("runs:\n"); + printf(" - iteration_count: %u\n", iter_count); + printf(" duration: %llu us\n", (unsigned long long)elapsed_us); + printf(" max_rss: %ld KiB\n", peak_memory_usage()); + return 0; +} diff --git a/cpp/schobi/skynet.c b/cpp/schobi/skynet.c new file mode 100644 index 0000000..27c80ab --- /dev/null +++ b/cpp/schobi/skynet.c @@ -0,0 +1,219 @@ +/* The skynet benchmark as described here: + https://github.com/atemerev/skynet + + Adapted from + https://github.com/tzcnt/tmc-examples/blob/main/examples/skynet/main.cpp + Original author: tzcnt + Unlicense License + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. */ + +/* Three implementations, selected by the config argument: + + "sequential" the tree walked on one thread, no scheduler started. + + (default) fork-join - one group of 10 per node, recursing to the + leaves. + + "group-shared" the tree is cut at the first depth holding at least one node + per task; each task walks its share of those subtrees to the + leaves and the group folds the partials through barriers. + Every leaf is still visited, so the work is the tree's. */ + +#include "bench_common.h" +#include "memusage.h" +#include + +#include +#include + +#define SKYNET_DEPTH 8 +#define SKYNET_FANOUT 10 /* children per node */ +#define SKYNET_EXPECTED 4999999950000000ull + +static const unsigned iter_count = 1; + +/* 10^i for i in [0, SKYNET_DEPTH]: the base spacing of the nodes at each depth */ +static const size_t kPow10[SKYNET_DEPTH + 1] = { + 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 +}; + +/* ------------------------------------------------------------------------- */ +/* sequential */ +/* ------------------------------------------------------------------------- */ +/* one node walked to its leaves without the scheduler */ +static size_t skynet_sequential(size_t base, size_t depth) +{ + if (depth == SKYNET_DEPTH) + return base; + + size_t offset = kPow10[SKYNET_DEPTH - depth - 1]; + size_t count = 0; + for (size_t i = 0; i < SKYNET_FANOUT; ++i) + count += skynet_sequential(base + offset * i, depth + 1); + return count; +} + +/* ------------------------------------------------------------------------- */ +/* fork-join */ +/* ------------------------------------------------------------------------- */ +static size_t skynet_async(struct SchobiScheduler* scheduler, size_t base, size_t depth); + +/* The fan-out is an index range: one group of SKYNET_FANOUT, each index deriving + * its own base number into its own return slot. The tile stays 1x1 so every index + * is individually stealable. + */ +SCHOBI_DECLARE_CLOSURE(size_t, SkynetAsyncTask, (size_t, base)(size_t, offset)(size_t, depth)) +SCHOBI_CLOSURE(size_t, SkynetAsyncTask, (size_t, base)(size_t, offset)(size_t, depth)) +{ + SCHOBI_CLOSURE_ENV(); + return skynet_async(schobi_scheduler, base + offset * schobi_index, depth + 1); +} + +static size_t skynet_async(struct SchobiScheduler* scheduler, size_t base, size_t depth) +{ + if (depth == SKYNET_DEPTH) + return base; + + size_t offset = kPow10[SKYNET_DEPTH - depth - 1]; + + SkynetAsyncTask* handle = + schobi_async(scheduler, (SKYNET_FANOUT, SkynetAsyncTask, base, offset, depth)); + schobi_await(handle); + size_t count = 0; + for (unsigned idx = 0; idx < SKYNET_FANOUT; ++idx) + count += schobi_return_value(handle, idx); + schobi_release(handle); + return count; +} + +/* ------------------------------------------------------------------------- */ +/* group-shared */ +/* ------------------------------------------------------------------------- */ +/* Each task walks its share of the subtrees at the cut depth, publishes its + * partial, then the group folds pairwise: neighbours combine across log2(tasks) + * rounds with a barrier between them. A barrier parks a task on its retained + * stack, and any free worker resumes it, so the exchange never leaves the group. + */ +SCHOBI_DECLARE_CLOSURE_VOID(SkynetGroupTask, (size_t, nodes)(size_t, depth)(unsigned, tasks)(struct BenchPartial*, slots)) +SCHOBI_CLOSURE_VOID(SkynetGroupTask, (size_t, nodes)(size_t, depth)(unsigned, tasks)(struct BenchPartial*, slots)) +{ + SCHOBI_CLOSURE_ENV(); + size_t node_spacing = kPow10[SKYNET_DEPTH - depth]; + size_t sum = 0; + for (size_t i = schobi_index; i < nodes; i += tasks) + sum += skynet_sequential(i * node_spacing, depth); + slots[schobi_index].value = sum; + + /* publish before any neighbour reads */ + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + + /* every task crosses every barrier; in each round the tasks sitting at a + * multiple of twice the stride absorb the neighbour one stride above them + */ + for (unsigned stride = 1; stride < tasks; stride *= 2) + { + if ((schobi_index % (2 * stride)) == 0 && schobi_index + stride < tasks) + slots[schobi_index].value += slots[schobi_index + stride].value; + schobi_group_barrier(SCHOBI_FILE_AND_NAME); + } +} + +static size_t skynet_group(struct SchobiScheduler* scheduler, unsigned tasks) +{ + /* cut the tree at the first depth that holds at least one node per task */ + size_t depth = 0, nodes = 1; + while (nodes < (size_t)tasks && depth < SKYNET_DEPTH) + { + nodes *= SKYNET_FANOUT; + ++depth; + } + + struct BenchPartial* slots = bench_partials(tasks); + + /* barriers are fiber-only: every task needs a retained stack to park on */ + schobi_parallel_for_fibers(BENCH_FIBER_STACK_KB, BENCH_FIBER_TILE_X, BENCH_FIBER_TILE_Y)( + scheduler, (tasks, SkynetGroupTask, nodes, depth, tasks, slots)); + + size_t result = slots[0].value; + bench_free_partials(slots); + return result; +} + +/* ------------------------------------------------------------------------- */ + +static void skynet(struct SchobiScheduler* scheduler, enum BenchConfig config, unsigned tasks) +{ + size_t count; + + switch (config) + { + case BENCH_SEQUENTIAL: + count = skynet_sequential(0, 0); + break; + case BENCH_GROUP_SHARED: + count = skynet_group(scheduler, tasks); + break; + case BENCH_FORK_JOIN: + default: + count = skynet_async(scheduler, 0, 0); + break; + } + + if (count != SKYNET_EXPECTED) + printf("ERROR: wrong result - %zu\n", count); +} + +int main(int argc, char* argv[]) +{ + unsigned thread_count = bench_default_threads(); + if (argc > 1) + thread_count = (unsigned)atoi(argv[1]); + enum BenchConfig config = bench_parse_config(argc > 2 ? argv[2] : NULL); + + printf("threads: %u\n", config == BENCH_SEQUENTIAL ? 1u : thread_count); + printf("config: %s\n", bench_config_name(config)); + + struct SchobiScheduler* scheduler = NULL; + if (config != BENCH_SEQUENTIAL) + scheduler = bench_start_workers(thread_count); + unsigned tasks = thread_count * BENCH_TASKS_PER_WORKER; + + skynet(scheduler, config, tasks); /* warmup */ + + printf("runs:\n"); + double start = bench_seconds(); + + for (unsigned j = 0; j < iter_count; ++j) + skynet(scheduler, config, tasks); + + uint64_t elapsed_us = bench_elapsed_us(start); + + if (scheduler != NULL) + schobi_stop_workers(scheduler); + + printf(" - iteration_count: %u\n", iter_count); + printf(" duration: %llu us\n", (unsigned long long)elapsed_us); + printf(" max_rss: %ld KiB\n", peak_memory_usage()); + return 0; +} diff --git a/cpp/tbb/fib.cpp b/cpp/tbb/fib.cpp index dccbf19..609b7e0 100644 --- a/cpp/tbb/fib.cpp +++ b/cpp/tbb/fib.cpp @@ -27,18 +27,24 @@ // OTHER DEALINGS IN THE SOFTWARE. #include "memusage.hpp" +#include "bench_config.hpp" #include #include #include #include #include +#include static size_t thread_count = std::thread::hardware_concurrency() / 2; static const size_t iter_count = 1; +inline constexpr size_t fib_leaf = 2; // below this a node is its own value +// room for the frontier to overshoot its target on the last expansion pass +inline constexpr size_t fib_frontier_slots_per_task = 4; + size_t fibonacci(size_t n) { - if (n < 2) + if (n < fib_leaf) return n; size_t x, y; @@ -50,6 +56,65 @@ size_t fibonacci(size_t n) { return x + y; } +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// fib(n) is expanded breadth-first into a frontier of subproblems, each of which +// is then summed without the scheduler. Replacing v with v-1 and v-2 keeps +// sum(fib(frontier)) equal to fib(n), so the same tree is walked. + +static size_t fib_serial(size_t n) { + return n < fib_leaf ? n : fib_serial(n - 1) + fib_serial(n - 2); +} + +static size_t +build_frontier(std::vector& frontier, size_t n, size_t target) { + size_t capacity = frontier.size(); + size_t count = 1; + frontier[0] = static_cast(n); + for (;;) { + if (count >= target || count * 2 > capacity) { + break; + } + size_t split = 0; + for (size_t i = count; i-- > 0;) { + unsigned v = frontier[i]; + if (v < fib_leaf) { + continue; + } + frontier[i] = v - 1; + frontier[count + split] = v - 2; + ++split; + } + if (split == 0) { + break; + } + count += split; + } + return count; +} + +size_t fibonacci_grouped(size_t n, size_t tasks) { + std::vector frontier(tasks * fib_frontier_slots_per_task); + size_t count = build_frontier(frontier, n, tasks); + + return tbb::parallel_reduce( + tbb::blocked_range(0, count), size_t{0}, + [&](tbb::blocked_range const& range, size_t init) { + for (size_t i = range.begin(); i != range.end(); ++i) { + init += fib_serial(frontier[i]); + } + return init; + }, + std::plus() + ); +} + +static size_t run_fib(bench::Config config, size_t n, size_t tasks) { + return config == bench::Config::group_shared ? fibonacci_grouped(n, tasks) + : fibonacci(n); +} + int main(int argc, char* argv[]) { if (argc > 2) { thread_count = static_cast(atoi(argv[2])); @@ -59,17 +124,20 @@ int main(int argc, char* argv[]) { exit(0); } size_t n = static_cast(atoi(argv[1])); + bench::Config config = bench::parse_config(argc > 3 ? argv[3] : nullptr); + size_t tasks = thread_count * bench::tasks_per_worker; std::printf("threads: %zu\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); tbb::task_arena arena(thread_count); size_t result; - arena.execute([&] { result = fibonacci(n); }); // warmup + arena.execute([&] { result = run_fib(config, n, tasks); }); // warmup auto startTime = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < iter_count; ++i) { - arena.execute([&] { result = fibonacci(n); }); // warmup + arena.execute([&] { result = run_fib(config, n, tasks); }); std::printf("output: %zu\n", result); } diff --git a/cpp/tbb/matmul.cpp b/cpp/tbb/matmul.cpp index 5d828c5..ee4bbb9 100644 --- a/cpp/tbb/matmul.cpp +++ b/cpp/tbb/matmul.cpp @@ -7,6 +7,7 @@ // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt) +#include "bench_config.hpp" #include "matmul.hpp" #include "memusage.hpp" #include @@ -44,7 +45,41 @@ void matmul(int* a, int* b, int* c, int n, int N) { } } -std::vector run_matmul(tbb::task_arena& executor, int N) { +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// One task per block of C, each accumulating over the k blocks. Every block of C +// has a single writer, so the two-phase split the recursion needs disappears and +// the whole multiply is one dispatch. Same N^3 multiply-accumulates. + +// sized for the cache: a task holds a block of C live while streaming a block +// row of A against a block column of B +inline constexpr int matmul_blocked_block = 128; + +static void matmul_grouped(int* a, int* b, int* c, int N) { + int nb = N / matmul_blocked_block; + tbb::parallel_for( + tbb::blocked_range(0, nb * nb), + [&](tbb::blocked_range const& range) { + for (int idx = range.begin(); idx != range.end(); ++idx) { + int bi = idx / nb; + int bj = idx % nb; + int* cblock = + c + bi * matmul_blocked_block * N + bj * matmul_blocked_block; + for (int k = 0; k < nb; ++k) { + matmul_small( + a + bi * matmul_blocked_block * N + k * matmul_blocked_block, + b + k * matmul_blocked_block * N + bj * matmul_blocked_block, + cblock, matmul_blocked_block, N + ); + } + } + } + ); +} + +std::vector +run_matmul(tbb::task_arena& executor, int N, bench::Config config) { std::vector A(N * N, 1); std::vector B(N * N, 1); std::vector C(N * N, 0); @@ -53,7 +88,11 @@ std::vector run_matmul(tbb::task_arena& executor, int N) { int* b = B.data(); int* c = C.data(); - executor.execute([&] { matmul(a, b, c, N, N); }); + if (config == bench::Config::group_shared) { + executor.execute([&] { matmul_grouped(a, b, c, N); }); + } else { + executor.execute([&] { matmul(a, b, c, N, N); }); + } return C; } @@ -74,9 +113,9 @@ void validate_result(std::vector& C, int N) { } } -void run_one(tbb::task_arena& executor, int N) { +void run_one(tbb::task_arena& executor, int N, bench::Config config) { auto startTime = std::chrono::high_resolution_clock::now(); - std::vector result = run_matmul(executor, N); + std::vector result = run_matmul(executor, N, config); auto endTime = std::chrono::high_resolution_clock::now(); validate_result(result, N); auto totalTimeUs = @@ -95,12 +134,15 @@ int main(int argc, char* argv[]) { exit(0); } int n = atoi(argv[1]); + bench::Config config = bench::parse_config(argc > 3 ? argv[3] : nullptr); + std::printf("threads: %zu\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); tbb::task_arena executor(thread_count); - run_matmul(executor, n); // warmup + run_matmul(executor, n, config); // warmup std::printf("runs:\n"); - run_one(executor, n); + run_one(executor, n, config); } diff --git a/cpp/tbb/nqueens.cpp b/cpp/tbb/nqueens.cpp index 905889c..18e82bc 100644 --- a/cpp/tbb/nqueens.cpp +++ b/cpp/tbb/nqueens.cpp @@ -10,6 +10,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +#include "bench_config.hpp" #include "memusage.hpp" #include @@ -17,7 +18,9 @@ #include #include #include +#include #include +#include static size_t thread_count = std::thread::hardware_concurrency() / 2; static const size_t iter_count = 1; @@ -89,26 +92,134 @@ template void nqueens(int xMax, std::array buf, int& out) { out = ret; }; +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// The search tree is expanded breadth-first into a frontier of partial boards, +// and each board is then searched to the full board without the scheduler. The +// same tree is walked; only the dispatch changes. + +inline constexpr int board_bytes = nqueens_work; +inline constexpr unsigned max_children = nqueens_work; + +static bool legal_at(char const* buf, int xMax, char q) { + for (int x = 0; x < xMax; ++x) { + char p = buf[x]; + if (q == p || q == p - (xMax - x) || q == p + (xMax - x)) { + return false; + } + } + return true; +} + +static int nqueens_serial(int xMax, char const* buf) { + if (xMax == nqueens_work) { + return 1; + } + int ret = 0; + for (int y = 0; y < nqueens_work; ++y) { + char q = static_cast(y); + if (!legal_at(buf, xMax, q)) { + continue; + } + char child[board_bytes]; + std::memcpy(child, buf, board_bytes); + child[xMax] = q; + ret += nqueens_serial(xMax + 1, child); + } + return ret; +} + +// fill boards with one row per legal continuation, return how many +static unsigned expand(char* boards, char const* buf, int xMax) { + unsigned count = 0; + for (int y = 0; y < nqueens_work; ++y) { + char q = static_cast(y); + if (!legal_at(buf, xMax, q)) { + continue; + } + char* child = boards + static_cast(count) * board_bytes; + std::memcpy(child, buf, board_bytes); + child[xMax] = q; + ++count; + } + return count; +} + +static int nqueens_grouped(unsigned target) { + unsigned capacity = target * max_children; // boards, not bytes + std::vector current(static_cast(capacity) * board_bytes, 0); + std::vector next(static_cast(capacity) * board_bytes); + unsigned count = 1; + int depth = 0; + + while (count < target && depth < nqueens_work) { + unsigned produced = 0; + for (unsigned i = 0; i < count; ++i) { + if (produced + max_children > capacity) { + break; + } + produced += expand( + next.data() + static_cast(produced) * board_bytes, + current.data() + static_cast(i) * board_bytes, depth + ); + } + if (produced == 0) { + break; + } + current.swap(next); + count = produced; + ++depth; + } + + char const* frontier = current.data(); + return tbb::parallel_reduce( + tbb::blocked_range(0, count), 0, + [&](tbb::blocked_range const& range, int init) { + for (unsigned i = range.begin(); i != range.end(); ++i) { + init += nqueens_serial( + depth, frontier + static_cast(i) * board_bytes + ); + } + return init; + }, + std::plus() + ); +} + +static int run_nqueens(bench::Config config, unsigned tasks) { + if (config == bench::Config::group_shared) { + return nqueens_grouped(tasks); + } + std::array buf{}; + int result; + nqueens(0, buf, result); + return result; +} + int main(int argc, char* argv[]) { if (argc > 1) { thread_count = static_cast(atoi(argv[1])); } + bench::Config config = bench::parse_config(argc > 2 ? argv[2] : nullptr); + unsigned tasks = + static_cast(thread_count * bench::tasks_per_worker); + std::printf("threads: %zu\n", thread_count); + std::printf("config: %s\n", bench::config_name(config)); tbb::task_arena arena(thread_count); { - std::array buf{}; int result; - arena.execute([&]() { nqueens(0, buf, result); }); + arena.execute([&]() { result = run_nqueens(config, tasks); }); check_answer(result); } auto startTime = std::chrono::high_resolution_clock::now(); for (size_t i = 0; i < iter_count; ++i) { - std::array buf{}; int result; - arena.execute([&]() { nqueens(0, buf, result); }); + arena.execute([&]() { result = run_nqueens(config, tasks); }); check_answer(result); std::printf("output: %d\n", result); } diff --git a/cpp/tbb/skynet.cpp b/cpp/tbb/skynet.cpp index 71ab30a..3aa9181 100644 --- a/cpp/tbb/skynet.cpp +++ b/cpp/tbb/skynet.cpp @@ -28,6 +28,7 @@ // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. +#include "bench_config.hpp" #include "memusage.hpp" #include @@ -39,46 +40,102 @@ static size_t thread_count = std::thread::hardware_concurrency() / 2; static const size_t iter_count = 1; +inline constexpr size_t skynet_depth = 8; +inline constexpr size_t skynet_fanout = 10; +inline constexpr size_t skynet_expected = 4999999950000000ull; + template size_t skynet_one(size_t BaseNum, size_t Depth) { if (Depth == DepthMax) { return BaseNum; } size_t depthOffset = 1; for (size_t i = 0; i < DepthMax - Depth - 1; ++i) { - depthOffset *= 10; + depthOffset *= skynet_fanout; } - std::array results; + std::array results; tbb::task_group tg; - for (size_t i = 0; i < 9; ++i) { + for (size_t i = 0; i < skynet_fanout - 1; ++i) { tg.run([=, &results, idx = i]() { results[idx] = skynet_one(BaseNum + depthOffset * idx, Depth + 1); }); } tg.run_and_wait([=, &results]() { - results[9] = skynet_one(BaseNum + depthOffset * 9, Depth + 1); + results[skynet_fanout - 1] = skynet_one(BaseNum + depthOffset * (skynet_fanout - 1), Depth + 1); }); size_t count = 0; - for (size_t idx = 0; idx < 10; ++idx) { + for (size_t idx = 0; idx < skynet_fanout; ++idx) { count += results[idx]; } return count; } -template void skynet() { - size_t count = skynet_one(0, 0); - if (count != 4999999950000000) { +// -------------------------------------------------------------------------- +// group-shared +// -------------------------------------------------------------------------- +// The tree is cut at the first depth holding at least one node per task, and +// each node of that level is walked to its leaves without the scheduler. Every +// leaf is still visited, so the work is the tree's; only the dispatch changes. + +template size_t skynet_serial(size_t BaseNum, size_t Depth) { + if (Depth == DepthMax) { + return BaseNum; + } + size_t depthOffset = 1; + for (size_t i = 0; i < DepthMax - Depth - 1; ++i) { + depthOffset *= skynet_fanout; + } + size_t count = 0; + for (size_t i = 0; i < skynet_fanout; ++i) { + count += skynet_serial(BaseNum + depthOffset * i, Depth + 1); + } + return count; +} + +template size_t skynet_grouped(size_t tasks) { + size_t depth = 0; + size_t nodes = 1; + while (nodes < tasks && depth < DepthMax) { + nodes *= skynet_fanout; + ++depth; + } + + size_t nodeSpacing = 1; + for (size_t i = 0; i < DepthMax - depth; ++i) { + nodeSpacing *= skynet_fanout; + } + + return tbb::parallel_reduce( + tbb::blocked_range(0, nodes), size_t{0}, + [&](tbb::blocked_range const& range, size_t init) { + for (size_t i = range.begin(); i != range.end(); ++i) { + init += skynet_serial(i * nodeSpacing, depth); + } + return init; + }, + std::plus() + ); +} + +template void skynet(bench::Config config, size_t tasks) { + size_t count = config == bench::Config::group_shared + ? skynet_grouped(tasks) + : skynet_one(0, 0); + if (count != skynet_expected) { std::printf("ERROR: wrong result - %" PRIu64 "\n", count); } } +static bench::Config bench_config = bench::Config::fork_join; +static size_t task_count = 0; + template void loop_skynet() { std::printf("runs:\n"); auto startTime = std::chrono::high_resolution_clock::now(); for (size_t j = 0; j < iter_count; ++j) { - skynet(); + skynet(bench_config, task_count); } auto endTime = std::chrono::high_resolution_clock::now(); auto totalTimeUs = @@ -92,9 +149,13 @@ int main(int argc, char* argv[]) { if (argc > 1) { thread_count = static_cast(atoi(argv[1])); } + bench_config = bench::parse_config(argc > 2 ? argv[2] : nullptr); + task_count = thread_count * bench::tasks_per_worker; + std::printf("threads: %zu\n", thread_count); + std::printf("config: %s\n", bench::config_name(bench_config)); tbb::task_arena arena(thread_count); - arena.execute(skynet<8>); // warmup - arena.execute(loop_skynet<8>); + arena.execute([] { skynet(bench_config, task_count); }); // warmup + arena.execute(loop_skynet); }