diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f11b00b..28dc2a7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,148 +2,123 @@ name: CI on: push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] jobs: linux: + name: Linux Verification + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - os: [ubuntu-22.04, ubuntu-24.04] - compiler: [gcc, clang] - generator: [Ninja, "Unix Makefiles"] - runs-on: ${{ matrix.os }} - name: Linux-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.generator }} - steps: - - uses: actions/checkout@v4 - - run: | - sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" 17 all - sudo apt install -y cmake ninja-build ccache libomp-dev - echo "CC=${{ matrix.compiler }}" >> $GITHUB_ENV - if [[ "${{ matrix.compiler }}" == "gcc" ]]; then - echo "CXX=g++" >> $GITHUB_ENV - else - echo "CXX=clang++" >> $GITHUB_ENV - fi - echo "LDFLAGS=-L/usr/lib/llvm-17/lib" >> $GITHUB_ENV - echo "CPPFLAGS=-I/usr/lib/llvm-17/include" >> $GITHUB_ENV - - uses: actions/cache@v4 - with: - path: build - key: linux-${{ matrix.compiler }}-${{ matrix.generator }}-build-${{ github.sha }} - restore-keys: linux-${{ matrix.compiler }}-${{ matrix.generator }}-build- - - uses: actions/cache@v4 - with: - path: ~/.ccache - key: linux-${{ matrix.compiler }}-${{ matrix.generator }}-ccache-${{ github.sha }} - restore-keys: linux-${{ matrix.compiler }}-${{ matrix.generator }}-ccache- - - run: | - cmake -S . -B build -G "${{ matrix.generator }}" \ - -DCMAKE_C_COMPILER=${{ env.CC }} \ - -DCMAKE_CXX_COMPILER=${{ env.CXX }} \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - run: cmake --build build --config Release --verbose - - run: ctest --test-dir build --output-on-failure - - mac: - strategy: - fail-fast: false - matrix: - os: [macos-13, macos-14, macos-15] - generator: [Ninja, "Unix Makefiles"] - exclude: - - os: macos-13 - runs-on: ${{ matrix.os }} - name: macOS-${{ matrix.os }}-${{ matrix.generator }} + compiler: [g++, clang++] + generator: [Unix Makefiles, Ninja] + steps: - uses: actions/checkout@v4 - - run: | - brew install llvm libomp cmake ninja ccache - HOMEBREW_PREFIX=$(brew --prefix) - echo "PATH=${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/opt/llvm/bin:$PATH" >> $GITHUB_ENV - echo "LDFLAGS=-L${HOMEBREW_PREFIX}/opt/libomp/lib -L${HOMEBREW_PREFIX}/opt/llvm/lib" >> $GITHUB_ENV - echo "CPPFLAGS=-I${HOMEBREW_PREFIX}/opt/libomp/include -I${HOMEBREW_PREFIX}/opt/llvm/include" >> $GITHUB_ENV - echo "CMAKE_PREFIX_PATH=${HOMEBREW_PREFIX}/opt/libomp;${HOMEBREW_PREFIX}/opt/llvm" >> $GITHUB_ENV - echo "CC=clang" >> $GITHUB_ENV - echo "CXX=clang++" >> $GITHUB_ENV - - uses: actions/cache@v4 - with: - path: build - key: mac-${{ matrix.generator }}-build-${{ github.sha }} - restore-keys: mac-${{ matrix.generator }}-build- - - uses: actions/cache@v4 - with: - path: ~/.ccache - key: mac-${{ matrix.generator }}-ccache-${{ github.sha }} - restore-keys: mac-${{ matrix.generator }}-ccache- - - run: | - cmake -S . -B build -G "${{ matrix.generator }}" \ - -DCMAKE_C_COMPILER=${{ env.CC }} \ - -DCMAKE_CXX_COMPILER=${{ env.CXX }} \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - - run: cmake --build build --config Release --verbose - - run: ctest --test-dir build --output-on-failure + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build g++ clang libtbb-dev + + - name: Set compiler path + run: echo "CXX=${{ matrix.compiler }}" >> $GITHUB_ENV + shell: bash + + - name: Configure CMake + run: > + cmake -S . -B build + -G "${{ matrix.generator }}" + -DCMAKE_CXX_COMPILER=${{ env.CXX }} + shell: bash + + - name: Build + run: cmake --build build --config Release + + - name: Run tests + run: ctest --test-dir build --output-on-failure --build-config Release windows: + name: Windows Verification + runs-on: windows-latest strategy: fail-fast: false matrix: - os: [windows-2022] - runs-on: ${{ matrix.os }} - name: Windows-${{ matrix.os }}-MSVC + compiler: [cl] + generator: ["Visual Studio 17 2022"] + steps: - uses: actions/checkout@v4 - - run: choco install cmake ninja ccache -y - - run: cmake -S . -B build -A x64 - - run: cmake --build build --config Release --verbose - - run: ctest --test-dir build --output-on-failure - windows-alt: + - name: Install dependencies + run: choco install cmake ninja -y + + - name: Install oneTBB via vcpkg + run: | + git clone https://github.com/microsoft/vcpkg + ./vcpkg/bootstrap-vcpkg.bat + ./vcpkg/vcpkg install tbb:x64-windows + echo "CMAKE_TOOLCHAIN_FILE=$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake" >> $env:GITHUB_ENV + shell: pwsh + + - name: Debug vcpkg TBB install (optional) + run: | + dir vcpkg/installed/x64-windows/share/tbb + dir vcpkg/installed/x64-windows/lib + shell: pwsh + + - name: Set up MSVC environment + uses: ilammy/msvc-dev-cmd@v1 + + - name: Configure CMake + run: > + cmake -S . -B build + -G "${{ matrix.generator }}" + -DCMAKE_CXX_COMPILER="${{ matrix.compiler }}" + -DCMAKE_TOOLCHAIN_FILE="${{ env.CMAKE_TOOLCHAIN_FILE }}" + shell: bash + + - name: Build + run: cmake --build build --config Release + + - name: Run tests + run: ctest --test-dir build --output-on-failure --build-config Release + macos: + name: macOS Verification + runs-on: macos-latest strategy: fail-fast: false matrix: - os: [windows-2019, windows-2022] - compiler: [mingw, llvm] - generator: [Ninja, "Unix Makefiles"] - exclude: - - os: windows-2019 - compiler: mingw - runs-on: ${{ matrix.os }} - name: Windows-${{ matrix.os }}-${{ matrix.compiler }}-${{ matrix.generator }} + compiler: [clang++, g++-13] + generator: [Unix Makefiles, Ninja] + steps: - uses: actions/checkout@v4 - - run: | - choco install cmake ninja ccache llvm -y - choco install mingw --version=12.2.0 - echo "CC=${{ matrix.compiler }}" >> $GITHUB_ENV - if [[ "${{ matrix.compiler }}" == "mingw" ]]; then - echo "CXX=g++" >> $GITHUB_ENV - else - echo "CXX=clang++" >> $GITHUB_ENV - fi - shell: bash - - run: | - cmake -S . -B build -G "${{ matrix.generator }}" \ - -DCMAKE_C_COMPILER=${{ env.CC }} \ - -DCMAKE_CXX_COMPILER=${{ env.CXX }} \ - -DCMAKE_C_COMPILER_LAUNCHER=ccache \ - -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + + - name: Install dependencies + run: brew install cmake ninja tbb + + - name: Install GCC + if: matrix.compiler == 'g++-13' + run: brew install gcc + + - name: Set compiler path + run: echo "CXX=${{ matrix.compiler }}" >> $GITHUB_ENV shell: bash - - name: Add MinGW to PATH (Windows) + + - name: Configure CMake + run: > + cmake -S . -B build + -G "${{ matrix.generator }}" + -DCMAKE_CXX_COMPILER=${{ env.CXX }} shell: bash - run: | - echo "C:/mingw64/bin" >> $GITHUB_PATH - echo "C:/mingw64/lib" >> $GITHUB_PATH - cp "C:/mingw64/bin/libgomp-1.dll" build/tests/ + - name: Build - shell: bash - run: | - export PATH="/c/mingw64/bin:$PATH" - cmake --build build --config Release --verbose - - run: ctest --test-dir build --output-on-failure + run: cmake --build build --config Release + - name: Run tests + run: ctest --test-dir build --output-on-failure --build-config Release \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bdf0de9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "oneTBB"] + path = libs/oneTBB + url = https://github.com/oneapi-src/oneTBB.git diff --git a/CMakeLists.txt b/CMakeLists.txt index febd543..400a732 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,8 +14,11 @@ FetchContent_Declare( GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.5.2 ) + FetchContent_MakeAvailable(Catch2) +find_package(TBB CONFIG REQUIRED) + # Create target for the library (header-only) add_library(pubsub INTERFACE) @@ -25,6 +28,9 @@ target_include_directories(pubsub INTERFACE $ ) +target_compile_definitions(pubsub INTERFACE -DWITH_TBB) +target_link_libraries(pubsub INTERFACE TBB::tbb) + # Install the headers install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(TARGETS pubsub EXPORT pubsubTargets) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index fcbbc6e..ed6eccd 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -38,9 +38,10 @@ if(ENABLE_BM) ) # Linking - target_link_libraries(benchmark_pubsub + target_link_libraries(benchmark_pubsub PUBLIC benchmark::benchmark benchmark::benchmark_main + pubsub ) if(ENABLE_BOOST) diff --git a/benchmarks/main.cpp b/benchmarks/main.cpp index 879f9de..fc44465 100644 --- a/benchmarks/main.cpp +++ b/benchmarks/main.cpp @@ -1,83 +1,82 @@ #include -#include #include +#include #include +#include +#include -// ========== pubsub-lib ========== -#include "pubsub.h" // path to your pubsub-lib header +#include "pubsub.h" -// ======= Benchmark Setup ======= +#ifdef WITH_OMP +#include +#endif -// Simple event struct +// ========== Event Definition ========== constexpr auto MyEvent = pubsub::Event(); -// Global for pubsub-lib -pubsub::Publisher pub; -auto token = pub.subscribe([](int data) { - int dummy = data; - benchmark::DoNotOptimize(dummy); -}); - -// ================= pubsub-lib Benchmark ================= -static void BM_PubSubLib(benchmark::State& state) { - for (auto _ : state) { - pub.emit(42); - } +// ========== Simulated Heavy Work ========== +void heavy_callback_workload(int x) { + volatile uint64_t sum = x; + for (int i = 1; i <= 1000; ++i) + sum += i * i; + benchmark::DoNotOptimize(sum); } -BENCHMARK(BM_PubSubLib); -// =============== pubsub-lib async Benchmark ============== -static void BM_PubSubLib_ASYNC(benchmark::State& state) { - for (auto _ : state) { - pub.emit_async(42); +// ========== Create Publisher with N Heavy Subscribers ========== +std::unique_ptr create_publisher_with_heavy_subs(int num_subs) { + auto pub = std::make_unique(); + for (int i = 0; i < num_subs; ++i) { + pub->subscribe([](int x) { + heavy_callback_workload(x); + }); } + return pub; } -BENCHMARK(BM_PubSubLib_ASYNC); -// ================= Boost.Signals2 Benchmark ================= -#ifdef USE_BOOST -#include +// ========== Global Unordered Map of Prebuilt Publishers ========== +std::vector subscriber_counts = {1, 10, 100, 500, 1000}; +std::unordered_map> heavy_publishers; -static void BM_BoostSignals2(benchmark::State& state) { - boost::signals2::signal signal; - signal.connect([](int data) { - int dummy = data; - benchmark::DoNotOptimize(dummy); - }); +// ========== Benchmark Macro ========== +#define DEFINE_HEAVY_EMIT_BENCH(name, emit_method) \ + static void name(benchmark::State& state) { \ + int subs = state.range(0); \ + auto it = heavy_publishers.find(subs); \ + if (it == heavy_publishers.end()) state.SkipWithError("Missing pub"); \ + auto& pub = it->second; \ + for (auto _ : state) { \ + pub->emit_method; \ + } \ + } \ + BENCHMARK(name)->Args({1})->Args({10})->Args({100})->Args({500})->Args({1000}); - for (auto _ : state) { - signal(42); - } -} -BENCHMARK(BM_BoostSignals2); -#endif +// ========== Emit Variants ========== +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_Sync, emit(42)) -// ================= Qt Signals Benchmark ================= -#ifdef USE_QT -#include +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_StdAsync, emit_thread_async(42)) -class QtEmitter : public QObject { - Q_OBJECT -signals: - void mySignal(int); -}; +#if defined(__cpp_lib_execution) +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_StdExec_seq, emit_async(std::execution::seq, 42)) +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_StdExec_par, emit_async(std::execution::par, 42)) +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_StdExec_par_unseq, emit_async(std::execution::par_unseq, 42)) +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_StdExec_unseq, emit_async(std::execution::unseq, 42)) +#endif -static void BM_QtSignals(benchmark::State& state) { - QtEmitter emitter; - QObject::connect(&emitter, &QtEmitter::mySignal, [](int val) { - int dummy = val; - benchmark::DoNotOptimize(dummy); - }); +#ifdef WITH_OMP +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_OpenMP, emit_omp_async(42)) +#endif - for (auto _ : state) { - emit emitter.mySignal(42); - } -} -BENCHMARK(BM_QtSignals); +#ifdef WITH_TBB +DEFINE_HEAVY_EMIT_BENCH(BM_PubSub_Emit_TBB, emit_tbb_async(42)) #endif -BENCHMARK_MAIN(); +// ========== Manual Main ========== +int main(int argc, char** argv) { + for (int count : subscriber_counts) { + heavy_publishers[count] = create_publisher_with_heavy_subs(count); + } -#ifdef USE_QT -#include "main.moc" -#endif \ No newline at end of file + benchmark::Initialize(&argc, argv); + benchmark::RunSpecifiedBenchmarks(); + return 0; +} diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index d6f76f2..118c699 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -3,5 +3,6 @@ file(GLOB EXAMPLE_SOURCES "*.cpp") foreach(example ${EXAMPLE_SOURCES}) get_filename_component(example_name ${example} NAME_WE) add_executable(${example_name} ${example}) - target_include_directories(${example_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) + target_include_directories(${example_name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include) + target_link_libraries(${example_name} PUBLIC pubsub) endforeach() diff --git a/include/pubsub.h b/include/pubsub.h index 5c4db81..dabb5e4 100644 --- a/include/pubsub.h +++ b/include/pubsub.h @@ -9,6 +9,16 @@ #include #include #include +#include +#include + +#ifdef WITH_OMP + #include +#endif + +#ifdef WITH_TBB + #include +#endif #include "unique_couter.h" @@ -157,11 +167,50 @@ namespace pubsub { * a queue and calls them when a thread in the pool is free */ template - void emit_async(Args... args) { + void emit_thread_async(Args... args) { for (const auto& cb : callbacks) { (void)std::async(std::launch::async, cb, args...); } } + +#ifdef WITH_OMP + /** + * @brief Emit an event asynchronously using OpenMP. + */ + template + void emit_omp_async(Args... args) { + #pragma omp parallel for + for (const auto& cb : callbacks) { + cb(args...); + } + } +#endif + +#ifdef WITH_TBB + /** + * @brief Emit an event asynchronously using oneTBB. + */ + template + void emit_tbb_async(Args... args) { + tbb::parallel_for_each(callbacks.begin(), callbacks.end(), [&](const auto& cb) { + cb(args...); + }); + } +#endif + +#if defined(__cpp_lib_execution) + /** + * @brief Emit an event asynchronously using . + */ + template> + void emit_async(ExecutionPolicy policy, Args... args) { + std::for_each(policy, callbacks.begin(), callbacks.end(), + [&](const auto& cb) { + cb(args...); + } + ); + } +#endif }; /** @@ -213,6 +262,35 @@ namespace pubsub { get_handler()->emit(args...); } + /** + * @brief Emit an event asynchronously to all listeners. + */ + template requires IsEvent + void emit_thread_async(Args... args) { + get_handler()->emit_thread_async(args...); + } + +#ifdef WITH_OMP + /** + * @brief Emit an event asynchronously using OpenMP to all listeners. + */ + template requires IsEvent + void emit_omp_async(Args... args) { + get_handler()->emit_omp_async(args...); + } +#endif + +#ifdef WITH_TBB + /** + * @brief Emit an event asynchronously using oneTBB to all listeners. + */ + template requires IsEvent + void emit_tbb_async(Args... args) { + get_handler()->emit_tbb_async(args...); + } +#endif + +#if defined(__cpp_lib_execution) /** * @brief Emit an event asynchronously to all listeners. */ @@ -220,6 +298,7 @@ namespace pubsub { void emit_async(Args... args) { get_handler()->emit_async(args...); } +#endif /** * @brief Unsubscribe an object from the event. diff --git a/tests/test_pubsub.cpp b/tests/test_pubsub.cpp index 426d983..f4ad96f 100644 --- a/tests/test_pubsub.cpp +++ b/tests/test_pubsub.cpp @@ -93,7 +93,7 @@ TEST_CASE("Async event delivery still invokes subscribers") { TestSubscriber sub; sub.subscribe_to(pub); - pub.emit_async(99); + pub.emit_thread_async(99); std::this_thread::sleep_for(std::chrono::milliseconds(100)); REQUIRE(sub.data_sum == 99); }