Refactor benchmark CI configuration and add span benchmark comment workflow

- Updated span_benchmark.yml to streamline job configurations and improve artifact handling.
- Introduced span_benchmark_comment.yml to automate PR comment updates with benchmark results.
- Enhanced CMakeLists.txt to support benchmark builds with new GSL_BENCHMARK option.
- Removed README.md from benchmark directory and adjusted check_regression.py for cleaner report generation.
- Modified span_bench.cpp to include necessary headers for benchmarking.
This commit is contained in:
Mohammad Abdul Gafoor 2026-06-08 12:38:05 +00:00
parent ca7905d901
commit ff48618829
7 changed files with 154 additions and 517 deletions

View file

@ -1,256 +1,94 @@
# .github/workflows/span_benchmark.yml
#
# Benchmarks gsl::span vs std::span on every PR across all supported compilers.
# Strategy: in-run ratio (gsl_ns / std_ns) — noise-resistant because both
# spans run in the same process, same machine, same moment.
#
# All dependencies (google-benchmark v1.9.0, GSL v4.1.0) are pulled via
# FetchContent inside the repo's CMakeLists.txt — no separate install steps.
# The build/_deps directory is cached, keyed on CMakeLists.txt hash, so
# repeated runs don't re-clone anything.
#
# Three separate job groups (one per OS) because `runs-on` can't share a
# matrix with OS-specific shell/path differences cleanly:
#
# benchmark-linux → ubuntu-latest → GCC-13, GCC-14, Clang-17, Clang-18
# benchmark-windows → windows-latest → MSVC 2022, clang-cl
# benchmark-macos → macos-14 → Apple Clang (Xcode latest)
#
# A final `comment` job waits for all three groups, collects every JSON
# artifact, runs check_regression.py, and posts (or updates) one PR comment.
name: Span Benchmark
on:
pull_request:
branches: [main]
# Cancel stale runs when a new commit is pushed to the same PR.
concurrency:
group: span-bench-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# LINUX — GCC 13/14 · Clang 17/18 (× C++20 and C++23)
# ─────────────────────────────────────────────────────────────────────────────
benchmark-linux:
name: Linux / ${{ matrix.label }}
name: Linux / ${{ format('{0}-cpp{1}', matrix.cxx, matrix.cppstd) }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# ── GCC ────────────────────────────────────────────────────────
- label: GCC-13-cpp20
cxx: g++-13
cppstd: 20
extra_flags: ""
- label: GCC-14-cpp20
cxx: g++-14
cppstd: 20
extra_flags: ""
- label: GCC-14-cpp23
cxx: g++-14
cppstd: 23
extra_flags: ""
# ── Clang ──────────────────────────────────────────────────────
- label: Clang-17-cpp20
cxx: clang++-17
cppstd: 20
extra_flags: ""
- label: Clang-18-cpp20
cxx: clang++-18
cppstd: 20
extra_flags: ""
- label: Clang-18-cpp23
cxx: clang++-18
cppstd: 23
extra_flags: ""
# ── Clang + -fbounds-safety (core motivation of issue #1167) ───
# Safe Buffers enforcement is what triggered this whole tracking effort.
# - label: Clang-18-cpp20-bounds-safety
# cxx: clang++-18
# cppstd: 20
# extra_flags: "-fbounds-safety"
# ----------------------------------------------------------------------------
# ⚠️ Note: As of early 2026, the feature is not yet fully available or stable in mainline, official releases (like Clang 20, 21, or 22).
# ----------------------------------------------------------------------------
cxx: [g++-13, g++-14, clang++-16, clang++-17, clang++-18]
cppstd: [20, 23]
exclude:
- cxx: g++-13
cppstd: 23
- cxx: clang++-17
cppstd: 23
steps:
- uses: actions/checkout@v4
# ── Compiler install ───────────────────────────────────────────────
# GCC-13 and Clang pre-17 are already on ubuntu-latest (24.04).
- name: Install GCC 14
if: startsWith(matrix.cxx, 'g++-14')
run: |
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
sudo apt-get update -qq
sudo apt-get install -y g++-14
- name: Install Clang 17
if: matrix.cxx == 'clang++-17'
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
| sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-17 main" \
| sudo tee /etc/apt/sources.list.d/llvm-17.list
sudo apt-get update -qq && sudo apt-get install -y clang-17
- name: Install Clang 18
if: matrix.cxx == 'clang++-18'
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
| sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main" \
| sudo tee /etc/apt/sources.list.d/llvm-18.list
sudo apt-get update -qq && sudo apt-get install -y clang-18
# ── FetchContent cache ─────────────────────────────────────────────
# Caches the cloned sources for google-benchmark, googletest, and GSL.
# Key is the hash of CMakeLists.txt — busts automatically on any
# GIT_TAG bump without manual intervention.
- name: Cache FetchContent dependencies
uses: actions/cache@v4
with:
path: build/_deps
key: fetchcontent-linux-${{ matrix.cxx }}-${{ hashFiles('CMakeLists.txt') }}
key: fetchcontent-linux-${{ matrix.cxx }}-${{ hashFiles('benchmark/CMakeLists.txt') }}
restore-keys: |
fetchcontent-linux-${{ matrix.cxx }}-
# ── Configure → Build → Run ────────────────────────────────────────
# FetchContent handles benchmark + GSL — no separate install needed.
# -DCMAKE_CXX_STANDARD overrides the default C++20 set in CMakeLists
# so we can test C++23 configs from the matrix.
- name: Configure
run: |
cmake -S benchmark -B build \
cmake -S . -B build \
-DGSL_BENCHMARK=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER=${{ matrix.cxx }} \
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} \
-DCMAKE_CXX_FLAGS="${{ matrix.extra_flags }}"
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }}"
- name: Build
run: cmake --build build --target span_bench -j$(nproc)
# 10 repetitions → mean + stddev aggregates in the JSON output.
# benchmark_report_aggregates_only keeps the file compact.
- name: Run benchmark
run: |
./build/span_bench \
--benchmark_format=json \
--benchmark_repetitions=10 \
--benchmark_report_aggregates_only=true \
--benchmark_out=results_${{ matrix.label }}.json
--benchmark_out=results_${{ format('{0}-cpp{1}', matrix.cxx, matrix.cppstd) }}.json
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: bench-${{ matrix.label }}
path: results_${{ matrix.label }}.json
name: bench-${{ format('{0}-cpp{1}', matrix.cxx, matrix.cppstd) }}
path: results_${{ format('{0}-cpp{1}', matrix.cxx, matrix.cppstd) }}.json
retention-days: 7
# ─────────────────────────────────────────────────────────────────────────────
# WINDOWS — MSVC 2022 · clang-cl (× C++20 and C++23)
#
# Notes:
# • Uses the Visual Studio 17 2022 generator for both MSVC and clang-cl.
# The -T ClangCL toolset switch selects the clang-cl frontend that ships
# bundled with VS 2022 — no extra install needed.
# • Multi-config generator: output binary is at
# build\Release\span_bench.exe (FetchContent flat layout, no subdir).
# • FetchContent cache is keyed per-toolset because MSVC and clang-cl
# produce ABI-incompatible object files.
# • PowerShell is used throughout (shell: pwsh) for consistent quoting.
# ─────────────────────────────────────────────────────────────────────────────
benchmark-windows:
name: Windows / ${{ matrix.label }}
name: Windows / ${{ format('{0}-cpp{1}', matrix.toolset || 'MSVC', matrix.cppstd) }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
# ── MSVC 2022 ──────────────────────────────────────────────────
- label: MSVC-2022-cpp20
toolset: ""
cppstd: 20
extra_flags: ""
- label: MSVC-2022-cpp23
toolset: ""
cppstd: 23
extra_flags: ""
# ── clang-cl (bundled with VS 2022) ────────────────────────────
- label: clang-cl-cpp20
toolset: "ClangCL"
cppstd: 20
extra_flags: ""
- label: clang-cl-cpp23
toolset: "ClangCL"
cppstd: 23
extra_flags: ""
generator: [ 'Visual Studio 17 2022' ]
toolset: [ '', 'ClangCL' ]
cppstd: [ 20, 23 ]
steps:
- uses: actions/checkout@v4
# ── FetchContent cache ─────────────────────────────────────────────
- name: Cache FetchContent dependencies
uses: actions/cache@v4
with:
path: build/_deps
key: fetchcontent-windows-${{ matrix.toolset }}-${{ hashFiles('CMakeLists.txt') }}
key: fetchcontent-windows-${{ matrix.toolset }}-${{ hashFiles('benchmark/CMakeLists.txt') }}
restore-keys: |
fetchcontent-windows-${{ matrix.toolset }}-
# ── Configure → Build → Run ────────────────────────────────────────
- name: Configure
shell: pwsh
run: |
$tsArg = if ("${{ matrix.toolset }}" -ne "") { @("-T", "${{ matrix.toolset }}") } else { @() }
cmake -S benchmark -B build `
-G "Visual Studio 17 2022" @tsArg `
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} `
-DCMAKE_CXX_FLAGS="${{ matrix.extra_flags }}"
cmake -S . -B build `
-DGSL_BENCHMARK=ON `
-G "${{ matrix.generator }}" @tsArg `
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }}"
- name: Build
shell: pwsh
@ -258,8 +96,6 @@ jobs:
cmake --build build --target span_bench `
--config Release -j $env:NUMBER_OF_PROCESSORS
# Multi-config generator places the binary under build\Release\
- name: Run benchmark
shell: pwsh
run: |
@ -267,147 +103,54 @@ jobs:
--benchmark_format=json `
--benchmark_repetitions=10 `
--benchmark_report_aggregates_only=true `
--benchmark_out=results_${{ matrix.label }}.json
--benchmark_out=results_${{ format('{0}-cpp{1}', matrix.toolset || 'MSVC', matrix.cppstd) }}.json
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: bench-${{ matrix.label }}
path: results_${{ matrix.label }}.json
name: bench-${{ format('{0}-cpp{1}', matrix.toolset || 'MSVC', matrix.cppstd) }}
path: results_${{ format('{0}-cpp{1}', matrix.toolset || 'MSVC', matrix.cppstd) }}.json
retention-days: 7
# ─────────────────────────────────────────────────────────────────────────────
# MACOS — Apple Clang via Xcode (latest) (× C++20 and C++23)
#
# Notes:
# • macos-14 = Apple Silicon (M1). Switch to macos-13 for Intel if needed.
# • No extra compiler install — Apple Clang from Xcode is used directly.
# • -j uses sysctl (macOS equivalent of nproc).
# ─────────────────────────────────────────────────────────────────────────────
benchmark-macos:
name: macOS / ${{ matrix.label }}
runs-on: macos-14
name: macOS / ${{ format('AppleClang-cpp{0}', matrix.cppstd) }}
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- label: AppleClang-cpp20
cppstd: 20
extra_flags: ""
- label: AppleClang-cpp23
cppstd: 23
extra_flags: ""
cppstd: [ 20, 23 ]
steps:
- uses: actions/checkout@v4
# ── FetchContent cache ─────────────────────────────────────────────
- name: Cache FetchContent dependencies
uses: actions/cache@v4
with:
path: build/_deps
key: fetchcontent-macos-${{ matrix.cppstd }}-${{ hashFiles('CMakeLists.txt') }}
key: fetchcontent-macos-${{ matrix.cppstd }}-${{ hashFiles('benchmark/CMakeLists.txt') }}
restore-keys: |
fetchcontent-macos-${{ matrix.cppstd }}-
# ── Configure → Build → Run ────────────────────────────────────────
- name: Configure
run: |
cmake -S benchmark -B build \
cmake -S . -B build \
-DGSL_BENCHMARK=ON \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} \
-DCMAKE_CXX_FLAGS="${{ matrix.extra_flags }}"
-DCMAKE_CXX_STANDARD=${{ matrix.cppstd }}"
- name: Build
run: cmake --build build --target span_bench -j$(sysctl -n hw.logicalcpu)
- name: Run benchmark
run: |
./build/span_bench \
--benchmark_format=json \
--benchmark_repetitions=10 \
--benchmark_report_aggregates_only=true \
--benchmark_out=results_${{ matrix.label }}.json
--benchmark_out=results_${{ format('AppleClang-cpp{0}', matrix.cppstd) }}.json
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: bench-${{ matrix.label }}
path: results_${{ matrix.label }}.json
retention-days: 7
# ─────────────────────────────────────────────────────────────────────────────
# COMMENT — collect every result artifact → one PR comment
#
# Waits for all three OS groups. `if: always()` ensures this runs even when
# some benchmark jobs fail — partial results are always reported.
# The CI check fails at the very end (after posting) if any regression found.
# ─────────────────────────────────────────────────────────────────────────────
comment:
name: Post PR comment
needs: [benchmark-linux, benchmark-windows, benchmark-macos]
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Download all result artifacts
uses: actions/download-artifact@v4
with:
pattern: bench-*
merge-multiple: true # flatten all artifacts into CWD
# continue-on-error so the comment step always runs even on regression.
- name: Generate regression report
id: report
run: |
python3 benchmark/check_regression.py \
--threshold 0.15 \
--output comment.md \
--workflow-url "https://github.com/${{ github.repository }}/blob/tree/main/.github/workflows/span_benchmark.yml" \
results_*.json
continue-on-error: true
- name: Find existing bot comment
uses: peter-evans/find-comment@v3
id: find_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "<!-- span-bench-report -->"
# One comment per PR, updated on every push — not a flood of new ones.
- name: Create or update PR comment
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find_comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: comment.md
edit-mode: replace
# Surface the failure visibly in the CI check panel.
- name: Fail if regression detected
if: steps.report.outcome == 'failure'
run: |
echo "::error::Performance regression detected. See the PR comment for the full table."
exit 1
name: bench-${{ format('AppleClang-cpp{0}', matrix.cppstd) }}
path: results_${{ format('AppleClang-cpp{0}', matrix.cppstd) }}.json
retention-days: 7

View file

@ -0,0 +1,76 @@
name: Span Benchmark Comment
on:
workflow_run:
workflows: ["Span Benchmark"]
types: [completed]
permissions:
pull-requests: write
jobs:
comment:
name: Post PR comment
runs-on: ubuntu-latest
if: always()
steps:
- uses: actions/checkout@v6
# Download all bench-* artifacts from the triggering workflow run.
- name: Download all result artifacts
uses: actions/download-artifact@v4
with:
pattern: bench-*
merge-multiple: true
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Generate regression report
id: report
run: |
python3 benchmark/check_regression.py \
--threshold 0.15 \
--output comment.md \
results_*.json
continue-on-error: true
# Resolve the PR number from the triggering workflow run.
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const runs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
});
const pr = runs.data.find(p =>
p.head.sha === context.payload.workflow_run.head_sha
);
return pr ? pr.number : null;
- name: Find existing bot comment
if: steps.pr.outputs.result != 'null'
uses: peter-evans/find-comment@v3
id: find_comment
with:
issue-number: ${{ steps.pr.outputs.result }}
comment-author: github-actions[bot]
body-includes: "<!-- span-bench-report -->"
- name: Create or update PR comment
if: steps.pr.outputs.result != 'null'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find_comment.outputs.comment-id }}
issue-number: ${{ steps.pr.outputs.result }}
body-path: comment.md
edit-mode: replace
- name: Fail if regression detected
if: steps.report.outcome == 'failure'
run: |
echo "::error::Performance regression detected. See the PR comment for the full table."
exit 1

View file

@ -10,6 +10,7 @@ string(COMPARE EQUAL ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR} PROJECT_IS_
option(GSL_INSTALL "Generate and install GSL target" ${PROJECT_IS_TOP_LEVEL})
option(GSL_TEST "Build and perform GSL tests" ${PROJECT_IS_TOP_LEVEL})
option(GSL_BENCHMARK "Build span benchmarks" ${PROJECT_IS_TOP_LEVEL})
# The implementation generally assumes a platform that implements C++14 support
target_compile_features(GSL INTERFACE "cxx_std_14")
@ -19,6 +20,10 @@ add_subdirectory(include)
target_sources(GSL INTERFACE $<BUILD_INTERFACE:${GSL_SOURCE_DIR}/GSL.natvis>)
if(GSL_BENCHMARK)
add_subdirectory(benchmark)
endif()
if (GSL_TEST)
enable_testing()
add_subdirectory(tests)

View file

@ -1,87 +1,57 @@
# benchmark/CMakeLists.txt
#
# Self-contained build for the gsl::span vs std::span benchmark.
# All dependencies are fetched automatically via FetchContent:
# - google/benchmark v1.9.0
# - google/googletest release-1.12.1 (benchmark's internal dep)
# - microsoft/GSL v4.1.0
#
# Usage (from this directory):
# cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# cmake --build build --target span_bench
# ./build/span_bench --benchmark_format=json --benchmark_repetitions=10 \
# --benchmark_report_aggregates_only=true \
# --benchmark_out=results.json
#
# To override the compiler or standard (as CI does):
# cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_CXX_COMPILER=clang++-18 \
# -DCMAKE_CXX_STANDARD=23
cmake_minimum_required(VERSION 3.14...3.16)
cmake_minimum_required(VERSION 3.20)
project(GSLBenchmarks LANGUAGES CXX)
project(gsl_span_benchmark CXX)
# ── C++ standard ───────────────────────────────────────────────────────────────
# Minimum is 20 because std::span (required for this benchmark) is C++20 only.
# If a consumer passes GSL_CXX_STANDARD < 20 we clamp and warn.
set(GSL_CXX_STANDARD "20" CACHE STRING "Use c++ standard")
# ── C++ standard ──────────────────────────────────────────────────────────────
# Default C++20 (minimum for std::span). CI overrides via -DCMAKE_CXX_STANDARD.
set(CMAKE_CXX_STANDARD 20 CACHE STRING "C++ standard")
if(GSL_CXX_STANDARD LESS 20)
message(WARNING
"GSL_CXX_STANDARD=${GSL_CXX_STANDARD} is too old for the span benchmark "
"(std::span requires C++20). Overriding to 20.")
set(GSL_CXX_STANDARD "20" CACHE STRING "Use c++ standard" FORCE)
endif()
set(CMAKE_CXX_STANDARD ${GSL_CXX_STANDARD})
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS NO)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type")
# Makes Visual Studio organise targets into folders
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# ── FetchContent dependencies ──────────────────────────────────────────────────
include(FetchContent)
# Suppress benchmark's own test suite — we don't need it.
# Suppress benchmark's own test suite
set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
GIT_SHALLOW ON
)
set(BENCHMARK_ENABLE_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
googlebenchmark
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG v1.9.0
GIT_TAG v1.9.5
GIT_SHALLOW ON
)
FetchContent_MakeAvailable(googlebenchmark)
FetchContent_MakeAvailable(googletest googlebenchmark)
# Use the GSL from the local checkout — NOT a pinned release tag.
# The benchmark/ folder sits one level below the repo root, so
# CMAKE_CURRENT_SOURCE_DIR/.. resolves to the repo root which contains
# the real include/gsl/span header being modified by the PR.
# This is what makes the CI actually test the PR's changes.
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. gsl_root)
# ── Benchmark executable ───────────────────────────────────────────────────────
add_executable(span_bench span_bench.cpp)
# Link benchmark::benchmark only — NOT benchmark::benchmark_main.
# span_bench.cpp uses the BENCHMARK_MAIN() macro which expands to its own
# main(), so linking benchmark_main would cause a duplicate-symbol linker error.
target_link_libraries(span_bench PRIVATE
benchmark::benchmark # provides the benchmark framework + runner
Microsoft.GSL::GSL # provides gsl::span and friends
benchmark::benchmark
Microsoft.GSL::GSL
)
set_target_properties(span_bench PROPERTIES FOLDER "benchmarks")
# ── Optimisation flags ─────────────────────────────────────────────────────────
# -O3 and -march=native let the compiler apply the same optimisations to both
# gsl::span and std::span, keeping the comparison fair.
# -fno-omit-frame-pointer keeps perf/profiling usable if you need it locally.
target_compile_options(span_bench PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:
-O3
-march=native
-fno-omit-frame-pointer
>
$<$<CXX_COMPILER_ID:MSVC>:
/O2
/GL # whole-program optimisation
/GL
>
)

View file

@ -1,144 +0,0 @@
# `gsl::span` vs `std::span` Benchmark
Performance parity tracking between `gsl::span` and `std::span` across all supported compilers, platforms, and C++ standards — as part of [microsoft/GSL#1167](https://github.com/microsoft/GSL/issues/1167) and [microsoft/GSL#1165](https://github.com/microsoft/GSL/issues/1165).
---
## Overview
`gsl::span` should be a zero-overhead abstraction over `std::span`. This benchmark suite verifies that claim continuously — on every PR — so performance regressions are caught before they land in main.
The comparison strategy is **in-run ratio** (`gsl_ns / std_ns`): both spans are measured in the same process on the same machine at the same moment, so runner noise cancels out. A ratio close to `1.0` means parity. If `gsl::span` is more than **15% slower** than `std::span` on any benchmark, CI flags it and posts a detailed table in the PR comment.
---
## Benchmarks
All benchmarks run on a sorted vector of 1000 integers.
| Benchmark | What it tests |
|---|---|
| `IsSorted` | `std::is_sorted` via span iterators |
| `IsSortedRanges` | `std::ranges::is_sorted` via the span range interface |
| `IsSortedCustom` | Custom hand-rolled `is_sorted` loop via span iterators |
| `MinElementAlgorithm` | `std::min_element` via span iterators |
| `MinElementRangeFor` | Range-for loop with a custom min accumulator |
Each benchmark has a `StdSpan` and `GslSpan` variant. The Python script pairs them by name and computes the ratio.
---
## CI Matrix
The benchmark runs on every pull request across 13 configurations:
| OS | Compiler | C++ Standard |
|---|---|---|
| ubuntu-latest | GCC 13 | C++20 |
| ubuntu-latest | GCC 14 | C++20, C++23 |
| ubuntu-latest | Clang 17 | C++20 |
| ubuntu-latest | Clang 18 | C++20, C++23 |
| windows-latest | MSVC 2022 | C++20, C++23 |
| windows-latest | clang-cl (VS 2022 bundled) | C++20, C++23 |
| macos-14 (Apple Silicon) | Apple Clang (Xcode latest) | C++20, C++23 |
Results from all 13 jobs are collected and posted as a **single PR comment**, updated on every push.
---
## Repository Layout
```
benchmark/
├── CMakeLists.txt # self-contained build — fetches benchmark + googletest
├── span_bench.cpp # the benchmark source
├── check_regression.py # parses JSON results, writes the PR comment markdown
└── README.md # this file
.github/workflows/
└── span_benchmark.yml # CI workflow
```
The benchmark folder is self-contained. `CMakeLists.txt` fetches `google/benchmark` (v1.9.0) and `google/googletest` via `FetchContent`. GSL itself is sourced from the **local repo checkout** — not a pinned tag — so the benchmark always tests the code in the PR, not a release.
---
## Running Locally
**Prerequisites:** CMake ≥ 3.20, a C++20-capable compiler, internet access for `FetchContent`.
```bash
# From the benchmark/ directory:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --target span_bench
# Run with JSON output (matches what CI does):
./build/span_bench \
--benchmark_format=json \
--benchmark_repetitions=10 \
--benchmark_report_aggregates_only=true \
--benchmark_out=results.json
# Generate the regression report locally:
python3 check_regression.py --threshold 0.15 results.json
```
To test a specific compiler or standard:
```bash
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER=clang++-18 \
-DCMAKE_CXX_STANDARD=23
```
---
## Regression Detection
The `check_regression.py` script:
1. Reads one or more Google Benchmark JSON files (one per CI matrix config)
2. Pairs `*StdSpan` benchmarks with their `*GslSpan` counterparts by name
3. Computes `ratio = gsl_mean / std_mean` using the 10-repetition mean
4. Flags any ratio above `1 + threshold` (default **15%**) as a regression
5. Writes a Markdown table per config, collected into a single PR comment
6. Exits with code `1` if any regression is found — failing the CI check
```
python3 check_regression.py [--threshold 0.15] [--output report.md] results_*.json
```
### Example PR comment output
```
## 📊 gsl::span vs std::span benchmark results
### `GCC-14-cpp20`
| Benchmark | std mean | std σ | gsl mean | gsl σ | ratio | status |
|----------------------|----------|--------|----------|--------|-------|----------|
| IsSorted | 124.1 ns | ±1.2% | 125.3 ns | ±1.4% | 1.01× | ✅ 1.01× |
| IsSortedRanges | 88.4 ns | ±0.9% | 89.1 ns | ±1.1% | 1.01× | ✅ 1.01× |
| IsSortedCustom | 112.6 ns | ±1.5% | 113.2 ns | ±1.3% | 1.01× | ✅ 1.01× |
| MinElementAlgorithm | 95.2 ns | ±1.0% | 96.0 ns | ±1.2% | 1.01× | ✅ 1.01× |
| MinElementRangeFor | 98.7 ns | ±1.1% | 99.4 ns | ±0.8% | 1.01× | ✅ 1.01× |
```
Status icons:
- ✅ — within the threshold (parity)
- 🔴 — `gsl::span` is more than 15% slower (regression, CI fails)
- 🟢 — `gsl::span` is more than 15% faster (improvement, noted but not a failure)
---
## Noise Considerations
GitHub-hosted runners are shared VMs with a typical noise floor of **±1015%** on absolute timing. The ratio strategy mitigates this because both span variants experience the same CPU conditions simultaneously. The 15% threshold is chosen to sit just above the noise floor — tight enough to catch real regressions, loose enough to avoid false positives on every PR.
To further reduce variance, each benchmark runs **10 repetitions** and the script uses the **mean** (not a single sample) for the ratio calculation. The stddev column in the comment table lets reviewers eyeball how stable each measurement was.
---
## Background
This benchmark was created in response to [microsoft/GSL#1167](https://github.com/microsoft/GSL/issues/1167), which highlighted the need to maintain performance parity with `std::span`, especially when [Safe Buffers / `-fbounds-safety`](https://clang.llvm.org/docs/SafeBuffers.html) are enabled. The initial benchmark scaffolding was provided by @galenelias.

View file

@ -131,7 +131,7 @@ def fmt_stddev(stddev: float, mean: float) -> str:
# ─── report builder ───────────────────────────────────────────────────────────
def build_report(json_paths: list[str], threshold: float, workflow_url: str = None) -> tuple[str, bool]:
def build_report(json_paths: list[str], threshold: float) -> tuple[str, bool]:
"""
Returns (markdown_text, had_regression).
"""
@ -202,6 +202,7 @@ def build_report(json_paths: list[str], threshold: float, workflow_url: str = No
config_regression = False
for p in pairs:
ratio, status = ratio_and_status(p["gsl_mean"], p["std_mean"], threshold)
ratio_str = "" if ratio is None else f"{ratio:.2f}×"
if "regression" in status:
had_regression = True
config_regression = True
@ -212,7 +213,7 @@ def build_report(json_paths: list[str], threshold: float, workflow_url: str = No
f"| {fmt_stddev(p['std_stddev'], p['std_mean'])} "
f"| {fmt(p['gsl_mean'])} "
f"| {fmt_stddev(p['gsl_stddev'], p['gsl_mean'])} "
f"| {ratio:.2f}× "
f"| {ratio_str} "
f"| {status} |"
)
@ -229,14 +230,6 @@ def build_report(json_paths: list[str], threshold: float, workflow_url: str = No
if not found_any:
lines.append("> ❌ No benchmark results could be loaded.")
# Footer
lines.append("---")
lines.append(
"_Ratio = `gsl_ns / std_ns`. "
"Values close to 1.0 mean performance parity. "
f"Run by [span-benchmark CI]({workflow_url})._"
)
return "\n".join(lines), had_regression
@ -264,15 +257,9 @@ def main():
metavar="FILE",
help="Write Markdown report to FILE instead of stdout.",
)
parser.add_argument(
"--workflow-url",
default=None,
metavar="URL",
help="GitHub URL to the workflow YAML (shows in the report footer).",
)
args = parser.parse_args()
report, had_regression = build_report(args.json_files, args.threshold, args.workflow_url)
report, had_regression = build_report(args.json_files, args.threshold)
if args.output:
Path(args.output).write_text(report, encoding="utf-8")

View file

@ -1,10 +1,10 @@
// Setup:
// 1) cmake . -DCMAKE_BUILD_TYPE=Release
// 2) cmake --build . --config Release
#include "gsl/span"
#include <algorithm>
#include <benchmark/benchmark.h>
#include <numeric>
#include <limits>
#include <ranges>
#include <span>
#include <vector>
static std::vector<int> make_vector()
{