Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3a250cf
Add a microbenchmark for testing context swapping overhead.
insertinterestingnamehere Mar 20, 2026
03ce306
Fix using the test argparsing header from C++.
insertinterestingnamehere Apr 7, 2026
e83a6db
Add minimal syscall benchmark.
insertinterestingnamehere Apr 7, 2026
acbc337
Add os thread fork/join benchmark.
insertinterestingnamehere Apr 7, 2026
13c7949
Add os_thread_futex_handoff benchmark.
insertinterestingnamehere Apr 9, 2026
c6aed48
Add benchmark measuring function call overheads for various numbers o…
insertinterestingnamehere Apr 10, 2026
0591808
Require C++17 for building tests.
insertinterestingnamehere Apr 10, 2026
107e1f5
Add a benchmark that attempts to force branch mispredicted function c…
insertinterestingnamehere Apr 10, 2026
6578d25
Rewrite function call overhead benchmark to not require a bunch of ho…
insertinterestingnamehere Apr 10, 2026
870f8c6
Add mini nemesis many-to-one benchmark.
insertinterestingnamehere Apr 20, 2026
870be7d
Restructure ifdefs to allow using c++23 or later in the benchmarks.
insertinterestingnamehere Apr 30, 2026
5a8fef3
Make the cast to volatile explicit in the function call microbenchmar…
insertinterestingnamehere Apr 30, 2026
c1e3cc1
Use C++26 for the benchmark suite. The benchmarks aren't required for…
insertinterestingnamehere Apr 30, 2026
e82d1b0
Make qt_atomics usable from within C++ and use that directly to get t…
insertinterestingnamehere Apr 30, 2026
601015c
Add benchmarks for a few different atomic workloads.
insertinterestingnamehere Apr 30, 2026
8ab36e2
Add contended atomic exchange benchmark.
insertinterestingnamehere May 28, 2026
a5d8960
Fix race condition in mini nemesis queue benchmark.
insertinterestingnamehere May 29, 2026
7f28e19
Switch nemesis_many_to_one benchmark to specify the total number of w…
insertinterestingnamehere May 29, 2026
dfd2fc4
Revive pure qthreads thread-ring benchmark.
insertinterestingnamehere May 29, 2026
89dadf0
Fix typo in CI job naming.
insertinterestingnamehere May 29, 2026
f8b2e9b
Make gcc16 happy with the atomic fetch_sub benchmark by using a fence…
insertinterestingnamehere Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v6
- name: netbsd
- name: dragonflybsd
uses: vmactions/dragonflybsd-vm@v1
with:
copyback: false
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ include(CMakePackageConfigHelpers)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

set(QTHREADS_BUILD_BENCHMARKS OFF CACHE BOOL "Whether or not to build the qthreads benchmarks.")

add_subdirectory(src)

if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
Expand Down
26 changes: 24 additions & 2 deletions include/qt_atomic_wait.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
#ifndef QT_ATOMIC_WAIT_H
#define QT_ATOMIC_WAIT_H

#include <assert.h>
#include <stdint.h>

#ifdef __cplusplus
#include <atomic>
#include <cassert>
#include <cstdint>
#else
#include <assert.h>
#include <stdatomic.h>
#include <stdint.h>
#endif

#include "qt_asserts.h"
#include "qt_os.h"
Expand All @@ -16,16 +25,29 @@

// Linux only has 32-bit futexes so that's the only size that's possible to
// standardize.
#ifdef __cplusplus
#define qt_atomic_wait_t std::atomic<std::uint32_t>
#define qt_atomic_wait_empty 0u
#define qt_atomic_wait_full UINT32_MAX
#define qt_atomic_wait_set_empty(a) \
((a)->store(qt_atomic_wait_empty, std::memory_order_relaxed))
#define qt_atomic_wait_set_full(a) \
((a)->store(qt_atomic_wait_full, std::memory_order_relaxed))
#define qt_atomic_wait_load(a) ((a)->load(std::memory_order_relaxed))
#define qt_atomic_wait_store(a, v) \
((a)->store((v), std::memory_order_relaxed
#else
#define qt_atomic_wait_t _Atomic uint32_t
#define qt_atomic_wait_empty 0u
#define qt_atomic_wait_full UINT32_MAX
#define qt_atomic_wait_set_empty(a) \
atomic_store_explicit((a), 0u, memory_order_relaxed)
atomic_store_explicit((a), qt_atomic_wait_empty, memory_order_relaxed)
#define qt_atomic_wait_set_full(a) \
atomic_store_explicit((a), UINT32_MAX, memory_order_relaxed)
atomic_store_explicit((a), qt_atomic_wait_full, memory_order_relaxed)
#define qt_atomic_wait_load(a) atomic_load_explicit((a), memory_order_relaxed)
#define qt_atomic_wait_store(a, v) \
atomic_store_explicit((a), v, memory_order_relaxed)
#endif

// Futex-like atomic wait functionality that's guaranteed to use
// the appropriate OS thread pausing functionality (e.g. futex).
Expand Down
17 changes: 10 additions & 7 deletions include/qt_atomics.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
#ifndef QT_ATOMICS_H
#define QT_ATOMICS_H

// Rely on atomic types getting included from qthread/qthread.h
// It does a little work to navigate <stdatomic.h> vs <atomic>

#include <pthread.h>

#include <stdatomic.h>
#include <sys/time.h>

#include <qthread/common.h>
Expand Down Expand Up @@ -53,9 +55,10 @@

typedef struct qt_spin_exclusive_s { /* added to allow fast critical section
ordering */
aligned_t _Atomic enter; /* and not call pthreads spin_lock -- hard to debug
*/
aligned_t _Atomic exit; /* near the lock under gdb -- 4/1/11 akp */
QT_Atomic(
aligned_t) enter; /* and not call pthreads spin_lock -- hard to debug
*/
QT_Atomic(aligned_t) exit; /* near the lock under gdb -- 4/1/11 akp */
} qt_spin_exclusive_t;

void qt_spin_exclusive_lock(qt_spin_exclusive_t *);
Expand Down Expand Up @@ -98,7 +101,7 @@ void qt_spin_exclusive_unlock(qt_spin_exclusive_t *);
{ \
uint32_t val = \
atomic_fetch_add_explicit(&(x)->s.users, 1, memory_order_relaxed); \
while (val != atomic_load_explicit((_Atomic uint32_t *)&(x)->s.ticket, \
while (val != atomic_load_explicit((QT_Atomic(uint32_t) *)&(x)->s.ticket, \
memory_order_acquire)) \
SPINLOCK_BODY(); \
}
Expand All @@ -112,7 +115,7 @@ void qt_spin_exclusive_unlock(qt_spin_exclusive_t *);
static inline int QTHREAD_TRYLOCK_TRY(qt_spin_trylock_t *x) {
qt_spin_trylock_t newcmp, cmp;
uint64_t tmp =
atomic_load_explicit((_Atomic uint64_t *)x, memory_order_relaxed);
atomic_load_explicit((QT_Atomic(uint64_t) *)x, memory_order_relaxed);
cmp = *(qt_spin_trylock_t *)&tmp;

if (cmp.s.users != cmp.s.ticket) { return 0; }
Expand Down Expand Up @@ -294,7 +297,7 @@ qthread_internal_incr_mod_(aligned_t *operand,

static inline void *qt_internal_atomic_swap_ptr(void **addr, void *newval) {
void *oldval =
atomic_load_explicit((void *_Atomic *)addr, memory_order_relaxed);
atomic_load_explicit((QT_Atomic(void *) *)addr, memory_order_relaxed);
void *tmp;

while ((tmp = qthread_cas_ptr(addr, oldval, newval)) != oldval) {
Expand Down
2 changes: 2 additions & 0 deletions include/qthread/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
#endif

#ifdef __cplusplus
#ifndef restrict
#ifdef __GNUC__
#define restrict __restrict
#else
#define restrict
#endif
#endif
#endif

#ifdef __GNUC__
#define QTHREAD_TRAP() __builtin_trap()
Expand Down
13 changes: 13 additions & 0 deletions include/qthread/qthread.h
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,19 @@ typedef union qt_spin_trylock_s {
QT_Atomic(haligned_t) ticket;
QT_Atomic(haligned_t) users;
} s;

#ifdef __cplusplus
// Tell the C++ compiler to not complain about constructors not getting
// called. These things are trivally initializable and mixed-precision
// accesses are allowed on all the architectures we currently care about.
qt_spin_trylock_s() noexcept {}

qt_spin_trylock_s &operator=(qt_spin_trylock_s const &other) noexcept {
u = other.u;
return *this;
}
#endif

} qt_spin_trylock_t;

typedef struct {
Expand Down
8 changes: 8 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ set(QTHREADS_SOURCES
patterns/wavefront.c
)

set(QTHREADS_BENCHMARK_HELPERS
benchmarks/swapcontext.c
)

if(QTHREADS_BUILD_BENCHMARKS)
list(APPEND QTHREADS_SOURCES ${QTHREADS_BENCHMARK_HELPERS})
endif()

add_library(qthread ${QTHREADS_SOURCES})

if ("${QTHREADS_CONTEXT_SWAP_IMPL}" STREQUAL "fastcontext")
Expand Down
74 changes: 74 additions & 0 deletions src/benchmarks/swapcontext.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include <stdint.h>
#include <stdlib.h>

#include <qthread/qtimer.h>

#include "qt_context.h"
#include "qt_visibility.h"

#ifdef USE_SYSTEM_SWAPCONTEXT
#define QT_MAKECONTEXT makecontext
#define QT_GETCONTEXT getcontext
#define QT_SWAPCONTEXT swapcontext
#else
#define QT_MAKECONTEXT qt_makectxt
#define QT_GETCONTEXT getcontext
#define QT_SWAPCONTEXT qt_swapctxt
#endif

#ifdef QTHREAD_MAKECONTEXT_SPLIT
#error \
"Context swapping benchmark does not currently support split makecontext."
#endif

#define SWAP_BENCH_STACK_SIZE 32768u

typedef struct {
qt_context_t inner;
qt_context_t outer;
} context_pair;

// This never returns, it just immediately swaps back to
// the outer context (passed as an argument) whenever entered.
static void *ctx_swap_inner(void *arg) {
context_pair *contexts = arg;
while (1) {
// Swap back to the outer one
QT_SWAPCONTEXT(&contexts->inner, &contexts->outer);
}
}

API_FUNC double qt_ctx_swap_bench(uint64_t num_swaps) {
context_pair contexts;
// Save current context
QT_GETCONTEXT(&contexts.outer);
// Make a context to switch to, running ctx_swap_inner.
// Initialization like this is required by the makecontext API.
// Weird, but okay.
QT_GETCONTEXT(&contexts.inner);
// TODO: can we get away with just using alloca here instead?
contexts.inner.uc_stack.ss_sp = malloc(SWAP_BENCH_STACK_SIZE);
contexts.inner.uc_stack.ss_size = SWAP_BENCH_STACK_SIZE;
QT_MAKECONTEXT(
&contexts.inner, (void (*)(void))&ctx_swap_inner, 1, &contexts);
// Start timer
qtimer_t timer = qtimer_create();
qtimer_start(timer);
// Actual benchmark.
// Swap into and out of the inner context repeatedly
// without executing any other work.
// This is all on the same thread and everything is small
// enough to at least keep everything in the l1 cace on
// nearly any hardware these days, so this should allow
// us to get a decent estimate of the isolated cost of
// a context swap.
for (uint64_t i = 0u; i < num_swaps / 2; i++) {
// Swap to the inner context;
QT_SWAPCONTEXT(&contexts.outer, &contexts.inner);
}
qtimer_stop(timer);
double time_elapsed = qtimer_secs(timer);
qtimer_destroy(timer);
free(contexts.inner.uc_stack.ss_sp);
return time_elapsed;
}
13 changes: 9 additions & 4 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
set(CMAKE_CXX_STANDARD 11)

set(QTHREADS_BUILD_TESTS ON CACHE BOOL "Whether or not to build the qthreads tests.")

if (${QTHREADS_BUILD_TESTS})
include_directories("." "utils/rng")
if (${QTHREADS_BUILD_TESTS} OR ${QTHREADS_BUILD_BENCHMARKS})
set(CMAKE_CXX_STANDARD 17)

include_directories("." "utils/rng")
add_subdirectory(utils/rng)

function(qthreads_test name)
Expand All @@ -26,7 +25,13 @@ if (${QTHREADS_BUILD_TESTS})
set_property(TEST ${name} PROPERTY ENVIRONMENT "QT_NUM_SHEPHERDS=2;QT_NUM_WORKERS_PER_SHEPHERD=1")
set_property(TEST ${name} PROPERTY C_STANDARD "C11")
endfunction()
endif()

if (${QTHREADS_BUILD_BENCHMARKS})
add_subdirectory(benchmarks)
endif()

if (${QTHREADS_BUILD_TESTS})
add_subdirectory(basics)
add_subdirectory(features)
add_subdirectory(internal)
Expand Down
11 changes: 9 additions & 2 deletions test/argparsing.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#ifndef TEST_ARGPARSING_H
#define TEST_ARGPARSING_H

#if defined(__cplusplus) && !defined(restrict)
#if defined(__GNUC__)
#define restrict __restrict
#else
#define restrict
#endif
#endif

#if defined(__cplusplus) && __cplusplus < 202302L
#include <atomic>
#define ARGP_Atomic(T) std::atomic<T>
Expand Down Expand Up @@ -123,8 +131,7 @@ inline static void iprintf(char const *restrict format, ...) {
va_end(ap);
}
}

#endif // if defined(__CYGWIN32__)
#endif

#endif // ifndef TEST_ARGPARSING_H
/* vim:set expandtab: */
19 changes: 19 additions & 0 deletions test/benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Allow benchmarks to access qthreads internal headers.
# In particular, qt_atomic_wait.h provides platform-generic wrappers around futexes.
include_directories("../../include")

set(CMAKE_CXX_STANDARD 26)

qthreads_test_cpp(atomic_accumulate)
qthreads_test_cpp(atomic_exchange_contended)
qthreads_test_cpp(atomic_fetch_min)
qthreads_test_cpp(atomic_fetch_sub)
qthreads_test_cpp(function_call)
qthreads_test_cpp(function_call_unpredicted)
qthreads_test_cpp(nemesis_many_to_one)
qthreads_test_cpp(os_thread_fork_join)
qthreads_test_cpp(os_thread_futex_handoff)
qthreads_test(swapcontext)
qthreads_test(syscall)

add_subdirectory(generic)
20 changes: 20 additions & 0 deletions test/benchmarks/atomic_accumulate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include "atomic_bench.hpp"

// Similar to atomic reduction operations (See C++ P3111)
std::size_t increment(std::size_t val,
std::atomic<std::size_t> &entry) noexcept {
entry.fetch_add(val, std::memory_order_relaxed);
return 1uz;
}

std::size_t increment_na(std::size_t val,
std::size_t volatile &entry) noexcept {
entry += val;
return 1uz;
}

int main() {
atomic_bench<0uz, increment>();
atomic_bench_baseline<0uz, increment_na>();
return 0;
}
Loading
Loading