From 66ce07311b3e6d879e6cd57d324d1c6630ecc7ba Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Sun, 13 Sep 2026 20:57:17 +0200 Subject: [PATCH 1/6] Add Doxygen/Sphinx documentation pipeline with test-sourced how-to snippets Doc examples live as tagged Catch2 tests under doc/snippets/, compiled and run as part of weaver_tests, so a how-to guide can't silently drift out of sync with the API it demonstrates -- a broken literalinclude reference fails the Sphinx build (run with -W). Wired up as an opt-in WEAVER_BUILD_DOCS CMake target and a separate CI workflow. --- .github/workflows/docs.yml | 59 ++++++++++++++++++++++++++++++++ CMakeLists.txt | 35 +++++++++++++++++++ cmake/config.cmake | 2 ++ doc/Doxyfile.in | 29 ++++++++++++++++ doc/requirements.txt | 2 ++ doc/snippets/float8_snippets.cpp | 29 ++++++++++++++++ doc/sphinx/conf.py | 24 +++++++++++++ doc/sphinx/howto.rst | 18 ++++++++++ doc/sphinx/index.rst | 10 ++++++ 9 files changed, 208 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 doc/Doxyfile.in create mode 100644 doc/requirements.txt create mode 100644 doc/snippets/float8_snippets.cpp create mode 100644 doc/sphinx/conf.py create mode 100644 doc/sphinx/howto.rst create mode 100644 doc/sphinx/index.rst diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..cb4206c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,59 @@ +name: Documentation + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install Doxygen + run: sudo apt-get update && sudo apt-get install -y doxygen + + - name: Cache pip packages + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-${{ hashFiles('doc/requirements.txt') }} + restore-keys: pip-${{ runner.os }}- + + - name: Install Conan2 and Sphinx/Breathe + run: | + pip install conan + pip install -r doc/requirements.txt + + - name: Cache Conan dependencies + id: conan-cache + uses: actions/cache@v3 + with: + path: ~/.conan2 + key: conan2-${{ runner.os }}-docs-${{ hashFiles('**/conanfile.*') }} + restore-keys: conan2-${{ runner.os }}-docs- + + - name: Detect default Conan profile + if: steps.conan-cache.outputs.cache-hit != 'true' + run: conan profile detect + + - name: Install Conan Dependencies + run: conan install --build=missing -of ${{ github.workspace }}/build ${{ github.workspace }} + + - name: Configure CMake + run: > + cmake -S ${{ github.workspace }} + -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/build/build/Release/generators/conan_toolchain.cmake + -DWEAVER_BUILD_DOCS=ON + + # Sphinx runs with -W (warnings-as-errors, set in the `docs` CMake + # target): a \snippet or literalinclude referencing a tag that no + # longer exists fails the build here instead of silently shipping a + # blank code block. + - name: Build documentation + run: cmake --build ${{ github.workspace }}/build --target docs diff --git a/CMakeLists.txt b/CMakeLists.txt index 802320c..741a81a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,15 @@ if("^${CMAKE_SOURCE_DIR}$" STREQUAL "^${PROJECT_SOURCE_DIR}$") ) weaver_target_sources(weaver_tests "src" ${WEAVER_TEST_SRC}) + # Documentation snippets double as tests: every //! [tag] block in + # doc/snippets is referenced verbatim from the Sphinx/Doxygen docs, so + # compiling and running it here is what keeps an example from silently + # drifting out of sync with the API it documents. + set(WEAVER_DOC_SNIPPET_SRC + doc/snippets/float8_snippets.cpp + ) + weaver_target_sources(weaver_tests "doc" ${WEAVER_DOC_SNIPPET_SRC}) + set_target_properties(weaver_tests PROPERTIES CXX_STANDARD ${WEAVER_CXX_STANDARD} CXX_STANDARD_REQUIRED YES @@ -161,3 +170,29 @@ if("^${CMAKE_SOURCE_DIR}$" STREQUAL "^${PROJECT_SOURCE_DIR}$") catch_discover_tests(weaver_tests) endif() endif() + +# Doxygen -> Breathe -> Sphinx documentation, off by default so a plain +# `cmake --build` never needs Doxygen/Python/Sphinx installed. +if(WEAVER_BUILD_DOCS) + find_package(Doxygen REQUIRED) + find_program(SPHINX_EXECUTABLE sphinx-build REQUIRED) + + set(WEAVER_DOXYGEN_XML_DIR "${CMAKE_CURRENT_BINARY_DIR}/doxygen/xml") + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in" + "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" + @ONLY + ) + + add_custom_target(docs + COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" + COMMAND "${CMAKE_COMMAND}" -E env + "WEAVER_DOXYGEN_XML_DIR=${WEAVER_DOXYGEN_XML_DIR}" + "${SPHINX_EXECUTABLE}" -b html -W + "${CMAKE_CURRENT_SOURCE_DIR}/doc/sphinx" + "${CMAKE_CURRENT_BINARY_DIR}/sphinx" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Building Doxygen XML and Sphinx HTML documentation" + VERBATIM + ) +endif() diff --git a/cmake/config.cmake b/cmake/config.cmake index ecd2a34..c4276ea 100644 --- a/cmake/config.cmake +++ b/cmake/config.cmake @@ -31,6 +31,8 @@ set(WEAVER_VECTORIZE "SSE 4.2" CACHE STRING "Vectorization instruction set") set_property(CACHE WEAVER_VECTORIZE PROPERTY STRINGS "Generic" "SSE 4.2" "AVX" "AVX 2" "AVX 512" "NEON") message(STATUS "Compiling for ${WEAVER_VECTORIZE}") +option(WEAVER_BUILD_DOCS "Build the Doxygen/Sphinx documentation ('docs' target)" OFF) + # Configure the compiler options a target function(weaver_configure_target tgt) message(STATUS "Configure target: ${tgt}") diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in new file mode 100644 index 0000000..5e8b4ea --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,29 @@ +# Minimal Doxygen config for FloatWeaver. +# +# This only lists the settings that differ from Doxygen's defaults -- run +# `doxygen -g -` to see the (very long) full set if you need to tweak +# something not covered here. +# +# Configured by CMake (see the WEAVER_BUILD_DOCS option in +# cmake/config.cmake) and consumed by Sphinx/Breathe, not viewed directly: +# GENERATE_HTML is off because Sphinx renders the final site, this step only +# produces the XML Breathe reads. + +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = FloatWeaver +PROJECT_NUMBER = @PROJECT_VERSION@ +PROJECT_BRIEF = "CUDA-style SIMD vector types for the CPU" + +OUTPUT_DIRECTORY = @CMAKE_CURRENT_BINARY_DIR@/doxygen +INPUT = @PROJECT_SOURCE_DIR@/include/weaver +STRIP_FROM_PATH = @PROJECT_SOURCE_DIR@/include + +RECURSIVE = NO +EXTRACT_ALL = NO +WARN_IF_UNDOCUMENTED = NO +QUIET = YES + +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..65c7437 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,2 @@ +sphinx==7.4.7 +breathe==4.35.0 diff --git a/doc/snippets/float8_snippets.cpp b/doc/snippets/float8_snippets.cpp new file mode 100644 index 0000000..b5b6487 --- /dev/null +++ b/doc/snippets/float8_snippets.cpp @@ -0,0 +1,29 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// This file holds documentation examples, not unit tests: each TEST_CASE is +// a `//! [tag] ... //! [tag]` bracketed snippet referenced verbatim from the +// Sphinx docs (via `literalinclude`) and/or from a Doxygen `\snippet` +// command. Keep every snippet self-contained -- a reader copying it out of +// the docs must not need any fixture or helper that isn't shown here. + +TEST_CASE("Add float8", "[doc]") +{ + //! [Add float8] + weaver::float8 x{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }; + weaver::float8 y{ 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + + weaver::float8 z = x + y; + //! [Add float8] + + REQUIRE(weaver::all(z == weaver::float8{ 9.0f })); +} diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py new file mode 100644 index 0000000..707cd92 --- /dev/null +++ b/doc/sphinx/conf.py @@ -0,0 +1,24 @@ +# Sphinx configuration for FloatWeaver. +# +# The Doxygen XML this depends on is produced by the `docs` CMake target +# (see cmake/config.cmake's WEAVER_BUILD_DOCS option), which points +# WEAVER_DOXYGEN_XML_DIR at its own build directory. Running sphinx-build by +# hand instead falls back to the location `doxygen doc/Doxyfile` produces +# when the project is built the way CLAUDE.md documents (`-B build`). + +import os + +project = "FloatWeaver" +copyright = "2023, Basil Fierz" +author = "Basil Fierz" + +extensions = ["breathe"] + +doxygen_xml_dir = os.environ.get( + "WEAVER_DOXYGEN_XML_DIR", + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "build", "doxygen", "xml")), +) +breathe_projects = {"FloatWeaver": doxygen_xml_dir} +breathe_default_project = "FloatWeaver" + +html_theme = "alabaster" diff --git a/doc/sphinx/howto.rst b/doc/sphinx/howto.rst new file mode 100644 index 0000000..f8dd10f --- /dev/null +++ b/doc/sphinx/howto.rst @@ -0,0 +1,18 @@ +How-to guides +============= + +Each guide below is pulled directly from a compiled, executed Catch2 test +(tagged ``[doc]`` in ``doc/snippets/``), so it can't drift out of sync with +the API it demonstrates -- if a guide stops matching reality, the docs build +fails. + +Add two float8 vectors +----------------------- + +``weaver::float8`` behaves like a scalar ``float`` for the usual arithmetic +operators, but computes on 8 lanes at once: + +.. literalinclude:: ../snippets/float8_snippets.cpp + :language: cpp + :start-after: //! [Add float8] + :end-before: //! [Add float8] diff --git a/doc/sphinx/index.rst b/doc/sphinx/index.rst new file mode 100644 index 0000000..de67e09 --- /dev/null +++ b/doc/sphinx/index.rst @@ -0,0 +1,10 @@ +FloatWeaver +=========== + +FloatWeaver is a header-mostly C++17 library for writing CUDA-style SIMD +code that runs on the CPU. + +.. toctree:: + :maxdepth: 2 + + howto From d54bffddcb2e0a9d831920349b098451d89f3948 Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Thu, 17 Sep 2026 23:02:42 +0200 Subject: [PATCH 2/6] Show generated assembly for the float8 how-to example doc/snippets/float8_asm.cpp adds a small function whose only job is to be compiled in isolation and disassembled, so the how-to guide can show the actual vector instruction the compiler emits instead of just asserting FloatWeaver is vectorized. Its operands are taken as parameters rather than literals; literal operands let the compiler constant-fold the addition away, leaving no vector instruction to show. doc/tools/extract_asm.py pulls one function's disassembly out of the object file (objdump -d -C), and a new weaver-asm Sphinx directive (conf.py) embeds it, mirroring how WEAVER_DOXYGEN_XML_DIR already feeds Breathe. --- .github/workflows/docs.yml | 8 ++-- CMakeLists.txt | 36 ++++++++++++++++ doc/snippets/float8_asm.cpp | 36 ++++++++++++++++ doc/sphinx/conf.py | 47 +++++++++++++++++++++ doc/sphinx/howto.rst | 21 ++++++++++ doc/tools/extract_asm.py | 84 +++++++++++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 doc/snippets/float8_asm.cpp create mode 100644 doc/tools/extract_asm.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cb4206c..6e395a1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -52,8 +52,10 @@ jobs: -DWEAVER_BUILD_DOCS=ON # Sphinx runs with -W (warnings-as-errors, set in the `docs` CMake - # target): a \snippet or literalinclude referencing a tag that no - # longer exists fails the build here instead of silently shipping a - # blank code block. + # target): a literalinclude tag that no longer exists, or a + # weaver-asm symbol that got inlined away/renamed, fails the build + # here instead of silently shipping a blank/stale code block. objdump + # (for the weaver-asm assembly listings) and Python3 both ship with + # ubuntu-latest already, no separate install needed. - name: Build documentation run: cmake --build ${{ github.workspace }}/build --target docs diff --git a/CMakeLists.txt b/CMakeLists.txt index 741a81a..bf14a92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,7 @@ if("^${CMAKE_SOURCE_DIR}$" STREQUAL "^${PROJECT_SOURCE_DIR}$") # drifting out of sync with the API it documents. set(WEAVER_DOC_SNIPPET_SRC doc/snippets/float8_snippets.cpp + doc/snippets/float8_asm.cpp ) weaver_target_sources(weaver_tests "doc" ${WEAVER_DOC_SNIPPET_SRC}) @@ -175,7 +176,14 @@ endif() # `cmake --build` never needs Doxygen/Python/Sphinx installed. if(WEAVER_BUILD_DOCS) find_package(Doxygen REQUIRED) + find_package(Python3 COMPONENTS Interpreter REQUIRED) find_program(SPHINX_EXECUTABLE sphinx-build REQUIRED) + # objdump ships with binutils, i.e. whatever gcc/clang toolchain is + # already on hand on Linux/macOS -- WEAVER_BUILD_DOCS is scoped to that, + # not to every CI backend/compiler this project builds against. The + # extracted instructions naturally differ by compiler/OS; that's fine, + # it's showing whatever the configured toolchain actually generated. + find_program(WEAVER_OBJDUMP objdump REQUIRED) set(WEAVER_DOXYGEN_XML_DIR "${CMAKE_CURRENT_BINARY_DIR}/doxygen/xml") configure_file( @@ -184,13 +192,41 @@ if(WEAVER_BUILD_DOCS) @ONLY ) + # doc/snippets/*_asm.cpp functions are compiled here in isolation (their + # own OBJECT library, no LTO, no other call sites in the program) so + # doc/tools/extract_asm.py can disassemble the actual vector instructions + # they generate for the how-to guides, rather than just asserting + # FloatWeaver is vectorized. + add_library(weaver_doc_asm OBJECT doc/snippets/float8_asm.cpp) + weaver_configure_target(weaver_doc_asm) + target_link_libraries(weaver_doc_asm PRIVATE Eigen3::Eigen Catch2::Catch2WithMain) + target_include_directories(weaver_doc_asm PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") + + set(WEAVER_ASM_DIR "${CMAKE_CURRENT_BINARY_DIR}/doc/asm") + set(WEAVER_ASM_ADD_FLOAT8 "${WEAVER_ASM_DIR}/add_float8.txt") + add_custom_command( + OUTPUT "${WEAVER_ASM_ADD_FLOAT8}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${WEAVER_ASM_DIR}" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/doc/tools/extract_asm.py" + --objdump "${WEAVER_OBJDUMP}" + --object "$" + --symbol add_float8 + --output "${WEAVER_ASM_ADD_FLOAT8}" + DEPENDS weaver_doc_asm "$" + COMMENT "Extracting assembly for add_float8" + VERBATIM + ) + add_custom_target(weaver_doc_asm_extract DEPENDS "${WEAVER_ASM_ADD_FLOAT8}") + add_custom_target(docs COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" COMMAND "${CMAKE_COMMAND}" -E env "WEAVER_DOXYGEN_XML_DIR=${WEAVER_DOXYGEN_XML_DIR}" + "WEAVER_ASM_DIR=${WEAVER_ASM_DIR}" "${SPHINX_EXECUTABLE}" -b html -W "${CMAKE_CURRENT_SOURCE_DIR}/doc/sphinx" "${CMAKE_CURRENT_BINARY_DIR}/sphinx" + DEPENDS weaver_doc_asm_extract WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" COMMENT "Building Doxygen XML and Sphinx HTML documentation" VERBATIM diff --git a/doc/snippets/float8_asm.cpp b/doc/snippets/float8_asm.cpp new file mode 100644 index 0000000..50423c6 --- /dev/null +++ b/doc/snippets/float8_asm.cpp @@ -0,0 +1,36 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// Functions in this file are compiled in isolation (see the WEAVER_BUILD_DOCS +// block in the root CMakeLists.txt and doc/tools/extract_asm.py) and +// disassembled so the how-to docs can show the actual vector instructions +// FloatWeaver generates, not just an assertion that it's "fast". +// +// Operands are taken as parameters rather than literals on purpose: literal +// operands let the compiler constant-fold the whole operation away at -O2, +// which would leave a `mov` of a precomputed constant in the disassembly +// instead of the vector instruction we're trying to show. + +//! [Add float8 asm] +weaver::float8 add_float8(weaver::float8 a, weaver::float8 b) +{ + return a + b; +} +//! [Add float8 asm] + +TEST_CASE("Add float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }; + weaver::float8 b{ 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f }; + + REQUIRE(weaver::all(add_float8(a, b) == weaver::float8{ 9.0f })); +} diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index 707cd92..72e4a93 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -8,6 +8,9 @@ import os +from docutils.parsers.rst import Directive +from docutils import nodes + project = "FloatWeaver" copyright = "2023, Basil Fierz" author = "Basil Fierz" @@ -22,3 +25,47 @@ breathe_default_project = "FloatWeaver" html_theme = "alabaster" + + +class WeaverAsmDirective(Directive): + """``.. weaver-asm:: `` -- embeds the disassembly WEAVER_BUILD_DOCS's + extract_asm.py extracted for that symbol (see doc/tools/extract_asm.py and + the weaver_doc_asm target in the root CMakeLists.txt). Mirrors how + WEAVER_DOXYGEN_XML_DIR feeds Breathe: same env-var handoff from CMake, + same "missing -> warning, -W turns it into a build failure" contract as + literalinclude/doxygenfunction elsewhere in these docs. + """ + + required_arguments = 1 + has_content = False + + def run(self): + symbol = self.arguments[0] + asm_dir = os.environ.get("WEAVER_ASM_DIR") + if not asm_dir: + self.state_machine.reporter.warning( + "weaver-asm: WEAVER_ASM_DIR is not set -- build docs via the " + "CMake 'docs' target (WEAVER_BUILD_DOCS=ON) to generate " + "assembly listings.", + line=self.lineno, + ) + return [] + + path = os.path.join(asm_dir, symbol + ".txt") + if not os.path.isfile(path): + self.state_machine.reporter.warning( + f"weaver-asm: no extracted assembly found for '{symbol}' at {path}", + line=self.lineno, + ) + return [] + + with open(path) as f: + text = f.read().rstrip("\n") + + node = nodes.literal_block(text, text) + node["language"] = "nasm" + return [node] + + +def setup(app): + app.add_directive("weaver-asm", WeaverAsmDirective) diff --git a/doc/sphinx/howto.rst b/doc/sphinx/howto.rst index f8dd10f..2dac9ae 100644 --- a/doc/sphinx/howto.rst +++ b/doc/sphinx/howto.rst @@ -16,3 +16,24 @@ operators, but computes on 8 lanes at once: :language: cpp :start-after: //! [Add float8] :end-before: //! [Add float8] + +That single ``+`` isn't sugar over a loop -- it's one SIMD instruction. The +function below is compiled and disassembled by the docs build itself +(``doc/snippets/float8_asm.cpp``); its operands arrive as parameters rather +than literals so the compiler can't fold the addition away at compile time: + +.. literalinclude:: ../snippets/float8_asm.cpp + :language: cpp + :start-after: //! [Add float8 asm] + :end-before: //! [Add float8 asm] + +Which the configured toolchain (here: whatever ``WEAVER_VECTORIZE`` this docs +build used) turns into: + +.. weaver-asm:: add_float8 + +The exact instructions depend on the compiler and target ISA -- an AVX build +emits a single ``vaddps`` on 8-wide ``ymm`` registers, while an SSE build +splits the same 8 lanes across two 4-wide ``xmm`` registers instead. Either +way, this is the real output of the currently configured toolchain, not a +hand-picked example. diff --git a/doc/tools/extract_asm.py b/doc/tools/extract_asm.py new file mode 100644 index 0000000..0c0df77 --- /dev/null +++ b/doc/tools/extract_asm.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Extract one function's disassembly from an object file for the docs. + +Used by the WEAVER_BUILD_DOCS CMake target: each doc/snippets/*_asm.cpp file +defines a small, non-inlined, extern-linkage function whose sole purpose is +to be compiled in isolation and disassembled here, so the how-to guides can +show the real vector instructions FloatWeaver generates instead of just +asserting that it's fast. See doc/snippets/float8_asm.cpp for why the +function takes its operands as parameters rather than literals. + +Output varies by compiler/OS -- that's expected and left as-is, this only +extracts whatever the configured toolchain actually produced. +""" + +import argparse +import re +import subprocess +import sys + +# Matches the label objdump -C (demangle) prints before a function's +# disassembly, e.g. "0000000000000130 , ...)>:" +# Greedy `.+` (not `[^>]+`) because a demangled C++ name can itself contain +# ">" characters, e.g. "add_float8(weaver::VectorScalar, ...)". +_LABEL_RE = re.compile(r"^[0-9a-f]+\s+<(?P.+)>:\s*$") + +# Matches one instruction line, e.g. " 130:\tendbr64" or " 134:\tvaddps ymm0,ymm1,ymm0" +_INSN_RE = re.compile(r"^\s*[0-9a-f]+:\t(?P.*)$") + + +def extract(objdump_output: str, symbol: str) -> list[str]: + lines = objdump_output.splitlines() + prefix = symbol + "(" + insns = [] + in_target = False + for line in lines: + label = _LABEL_RE.match(line) + if label: + in_target = label.group("name").startswith(prefix) + continue + if not in_target: + continue + insn = _INSN_RE.match(line) + if not insn: + # Blank line / section header: the function's body has ended. + break + text = insn.group("insn").split("#", 1)[0].rstrip() + if text: + insns.append(text) + return insns + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--objdump", required=True, help="path to the objdump executable") + parser.add_argument("--object", required=True, help="object file to disassemble") + parser.add_argument("--symbol", required=True, help="plain (undecorated) function name to extract") + parser.add_argument("--output", required=True, help="where to write the extracted instructions") + args = parser.parse_args() + + result = subprocess.run( + [args.objdump, "-d", "--no-show-raw-insn", "-Mintel", "-C", args.object], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"error: {args.objdump} failed:\n{result.stderr}", file=sys.stderr) + return 1 + + insns = extract(result.stdout, args.symbol) + if not insns: + print( + f"error: no disassembly found for symbol '{args.symbol}' in {args.object} " + "-- was it inlined away, or renamed?", + file=sys.stderr, + ) + return 1 + + with open(args.output, "w") as f: + f.write("\n".join(insns) + "\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a0e11f349af67425e4926bb9ab64a9e815b1b106 Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Fri, 18 Sep 2026 20:25:40 +0200 Subject: [PATCH 3/6] Add a select (branchless max) how-to guide, scaling the asm-in-docs pattern --- CMakeLists.txt | 49 +++++++++++++++++++------------ doc/snippets/select_asm.cpp | 29 ++++++++++++++++++ doc/snippets/select_snippets.cpp | 28 ++++++++++++++++++ doc/sphinx/howto.rst | 29 ++++++++++++++++++ doc/tools/extract_asm.py | 50 +++++++++++++++++++------------- 5 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 doc/snippets/select_asm.cpp create mode 100644 doc/snippets/select_snippets.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bf14a92..b7309c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,8 @@ if("^${CMAKE_SOURCE_DIR}$" STREQUAL "^${PROJECT_SOURCE_DIR}$") set(WEAVER_DOC_SNIPPET_SRC doc/snippets/float8_snippets.cpp doc/snippets/float8_asm.cpp + doc/snippets/select_snippets.cpp + doc/snippets/select_asm.cpp ) weaver_target_sources(weaver_tests "doc" ${WEAVER_DOC_SNIPPET_SRC}) @@ -192,31 +194,42 @@ if(WEAVER_BUILD_DOCS) @ONLY ) - # doc/snippets/*_asm.cpp functions are compiled here in isolation (their - # own OBJECT library, no LTO, no other call sites in the program) so + # doc/snippets/*_asm.cpp functions are compiled here in isolation (one + # shared OBJECT library, no LTO, no other call sites in the program) so # doc/tools/extract_asm.py can disassemble the actual vector instructions # they generate for the how-to guides, rather than just asserting - # FloatWeaver is vectorized. - add_library(weaver_doc_asm OBJECT doc/snippets/float8_asm.cpp) + # FloatWeaver is vectorized. Adding another example is just adding its + # *_asm.cpp file below and its function name to WEAVER_ASM_SYMBOLS -- + # extract_asm.py searches every object in the shared library for + # whichever one actually defines each symbol. + add_library(weaver_doc_asm OBJECT + doc/snippets/float8_asm.cpp + doc/snippets/select_asm.cpp + ) weaver_configure_target(weaver_doc_asm) target_link_libraries(weaver_doc_asm PRIVATE Eigen3::Eigen Catch2::Catch2WithMain) target_include_directories(weaver_doc_asm PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") set(WEAVER_ASM_DIR "${CMAKE_CURRENT_BINARY_DIR}/doc/asm") - set(WEAVER_ASM_ADD_FLOAT8 "${WEAVER_ASM_DIR}/add_float8.txt") - add_custom_command( - OUTPUT "${WEAVER_ASM_ADD_FLOAT8}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${WEAVER_ASM_DIR}" - COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/doc/tools/extract_asm.py" - --objdump "${WEAVER_OBJDUMP}" - --object "$" - --symbol add_float8 - --output "${WEAVER_ASM_ADD_FLOAT8}" - DEPENDS weaver_doc_asm "$" - COMMENT "Extracting assembly for add_float8" - VERBATIM - ) - add_custom_target(weaver_doc_asm_extract DEPENDS "${WEAVER_ASM_ADD_FLOAT8}") + set(WEAVER_ASM_SYMBOLS add_float8 select_max) + set(WEAVER_ASM_OUTPUTS "") + foreach(sym IN LISTS WEAVER_ASM_SYMBOLS) + set(out "${WEAVER_ASM_DIR}/${sym}.txt") + add_custom_command( + OUTPUT "${out}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${WEAVER_ASM_DIR}" + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/doc/tools/extract_asm.py" + --objdump "${WEAVER_OBJDUMP}" + --object "$" + --symbol "${sym}" + --output "${out}" + DEPENDS weaver_doc_asm "$" + COMMENT "Extracting assembly for ${sym}" + VERBATIM + ) + list(APPEND WEAVER_ASM_OUTPUTS "${out}") + endforeach() + add_custom_target(weaver_doc_asm_extract DEPENDS ${WEAVER_ASM_OUTPUTS}) add_custom_target(docs COMMAND "${DOXYGEN_EXECUTABLE}" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" diff --git a/doc/snippets/select_asm.cpp b/doc/snippets/select_asm.cpp new file mode 100644 index 0000000..6eea6d1 --- /dev/null +++ b/doc/snippets/select_asm.cpp @@ -0,0 +1,29 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Select branchless max asm] +weaver::float8 select_max(weaver::float8 a, weaver::float8 b) +{ + return weaver::select(a < b, b, a); +} +//! [Select branchless max asm] + +TEST_CASE("Select branchless max (asm demo)", "[doc]") +{ + weaver::float8 a{ 1.0f, 5.0f, 3.0f, 8.0f, 2.0f, 9.0f, 4.0f, 0.0f }; + weaver::float8 b{ 4.0f, 2.0f, 6.0f, 1.0f, 7.0f, 3.0f, 5.0f, 9.0f }; + + REQUIRE(weaver::all(select_max(a, b) == weaver::float8{ 4.0f, 5.0f, 6.0f, 8.0f, 7.0f, 9.0f, 5.0f, 9.0f })); +} diff --git a/doc/snippets/select_snippets.cpp b/doc/snippets/select_snippets.cpp new file mode 100644 index 0000000..41c50e3 --- /dev/null +++ b/doc/snippets/select_snippets.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Select branchless max", "[doc]") +{ + //! [Select branchless max] + weaver::float8 a{ 1.0f, 5.0f, 3.0f, 8.0f, 2.0f, 9.0f, 4.0f, 0.0f }; + weaver::float8 b{ 4.0f, 2.0f, 6.0f, 1.0f, 7.0f, 3.0f, 5.0f, 9.0f }; + + // Equivalent to "for each lane: a[i] < b[i] ? b[i] : a[i]" -- but with no + // branch, so every lane is computed regardless of the others. + weaver::float8 m = weaver::select(a < b, b, a); + //! [Select branchless max] + + REQUIRE(weaver::all(m == weaver::float8{ 4.0f, 5.0f, 6.0f, 8.0f, 7.0f, 9.0f, 5.0f, 9.0f })); +} diff --git a/doc/sphinx/howto.rst b/doc/sphinx/howto.rst index 2dac9ae..f17b598 100644 --- a/doc/sphinx/howto.rst +++ b/doc/sphinx/howto.rst @@ -37,3 +37,32 @@ emits a single ``vaddps`` on 8-wide ``ymm`` registers, while an SSE build splits the same 8 lanes across two 4-wide ``xmm`` registers instead. Either way, this is the real output of the currently configured toolchain, not a hand-picked example. + +Pick a value without branching +------------------------------- + +FloatWeaver code has no real ``if``: an ``if`` would have to pick one branch +for the whole vector, but different lanes can want different answers. Instead +you compute both possibilities and use ``select`` to keep, per lane, +whichever one the (also per-lane) condition asked for -- the branchless +equivalent of ``a[i] < b[i] ? b[i] : a[i]``: + +.. literalinclude:: ../snippets/select_snippets.cpp + :language: cpp + :start-after: //! [Select branchless max] + :end-before: //! [Select branchless max] + +Same isolate-and-disassemble treatment as above +(``doc/snippets/select_asm.cpp``): + +.. literalinclude:: ../snippets/select_asm.cpp + :language: cpp + :start-after: //! [Select branchless max asm] + :end-before: //! [Select branchless max asm] + +.. weaver-asm:: select_max + +Two instructions, no branch: a compare that produces a per-lane mask, then a +blend that picks lanes from ``a`` or ``b`` according to that mask -- every +lane gets computed, and the "wrong" one is simply discarded rather than +skipped. diff --git a/doc/tools/extract_asm.py b/doc/tools/extract_asm.py index 0c0df77..79dde7b 100644 --- a/doc/tools/extract_asm.py +++ b/doc/tools/extract_asm.py @@ -52,32 +52,42 @@ def extract(objdump_output: str, symbol: str) -> list[str]: def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--objdump", required=True, help="path to the objdump executable") - parser.add_argument("--object", required=True, help="object file to disassemble") + parser.add_argument( + "--object", + required=True, + help="';'-joined object file(s) to search -- CMake's $ " + "is passed straight through as a single argument with its list " + "separator intact, not split into multiple argv entries. " + "weaver_doc_asm is one shared OBJECT library, so this expands to one " + "file per doc/snippets/*_asm.cpp; only one of them will actually " + "define --symbol", + ) parser.add_argument("--symbol", required=True, help="plain (undecorated) function name to extract") parser.add_argument("--output", required=True, help="where to write the extracted instructions") args = parser.parse_args() - result = subprocess.run( - [args.objdump, "-d", "--no-show-raw-insn", "-Mintel", "-C", args.object], - capture_output=True, - text=True, - ) - if result.returncode != 0: - print(f"error: {args.objdump} failed:\n{result.stderr}", file=sys.stderr) - return 1 - - insns = extract(result.stdout, args.symbol) - if not insns: - print( - f"error: no disassembly found for symbol '{args.symbol}' in {args.object} " - "-- was it inlined away, or renamed?", - file=sys.stderr, + for obj in args.object.split(";"): + result = subprocess.run( + [args.objdump, "-d", "--no-show-raw-insn", "-Mintel", "-C", obj], + capture_output=True, + text=True, ) - return 1 + if result.returncode != 0: + print(f"error: {args.objdump} failed on {obj}:\n{result.stderr}", file=sys.stderr) + return 1 - with open(args.output, "w") as f: - f.write("\n".join(insns) + "\n") - return 0 + insns = extract(result.stdout, args.symbol) + if insns: + with open(args.output, "w") as f: + f.write("\n".join(insns) + "\n") + return 0 + + print( + f"error: no disassembly found for symbol '{args.symbol}' in any of " + f"{args.object.replace(';', ', ')} -- was it inlined away, or renamed?", + file=sys.stderr, + ) + return 1 if __name__ == "__main__": From 0ad09687e1613f04d4a4e6207e951fbdc0c59d87 Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Fri, 18 Sep 2026 21:31:00 +0200 Subject: [PATCH 4/6] Scale the asm-in-docs how-to pattern to 11 more examples, and clean up the extracted listings Adds sqrt/rcp/rsqrt/sgn/tolerance-compare/any/dot guides plus a new Eigen-integration section (vector arithmetic, squaredNorm, cross, branchless select, AlignedBox merge), each with real compiled-and- disassembled asm like the existing add/select guides. Also trims the CET landing pad and inter-function alignment padding that were cluttering every listing, and annotates each one with which register holds which parameter (hand-verified against the real System V AMD64 disassembly, the only calling convention WEAVER_BUILD_DOCS targets). --- CMakeLists.txt | 69 +++++- doc/snippets/approx_asm.cpp | 52 +++++ doc/snippets/approx_snippets.cpp | 48 ++++ doc/snippets/dot_asm.cpp | 29 +++ doc/snippets/dot_snippets.cpp | 28 +++ doc/snippets/eigen_add_asm.cpp | 34 +++ doc/snippets/eigen_add_snippets.cpp | 30 +++ doc/snippets/eigen_alignedbox_asm.cpp | 42 ++++ doc/snippets/eigen_alignedbox_snippets.cpp | 38 ++++ doc/snippets/eigen_cross_asm.cpp | 37 ++++ doc/snippets/eigen_cross_snippets.cpp | 31 +++ doc/snippets/eigen_norm_asm.cpp | 28 +++ doc/snippets/eigen_norm_snippets.cpp | 28 +++ doc/snippets/eigen_select_asm.cpp | 35 +++ doc/snippets/eigen_select_snippets.cpp | 32 +++ doc/snippets/reduce_asm.cpp | 29 +++ doc/snippets/reduce_snippets.cpp | 32 +++ doc/snippets/sgn_asm.cpp | 28 +++ doc/snippets/sgn_snippets.cpp | 26 +++ doc/snippets/sqrt_asm.cpp | 28 +++ doc/snippets/sqrt_snippets.cpp | 25 +++ doc/snippets/tolerance_asm.cpp | 33 +++ doc/snippets/tolerance_snippets.cpp | 30 +++ doc/sphinx/howto.rst | 243 +++++++++++++++++++++ doc/tools/extract_asm.py | 38 +++- 25 files changed, 1071 insertions(+), 2 deletions(-) create mode 100644 doc/snippets/approx_asm.cpp create mode 100644 doc/snippets/approx_snippets.cpp create mode 100644 doc/snippets/dot_asm.cpp create mode 100644 doc/snippets/dot_snippets.cpp create mode 100644 doc/snippets/eigen_add_asm.cpp create mode 100644 doc/snippets/eigen_add_snippets.cpp create mode 100644 doc/snippets/eigen_alignedbox_asm.cpp create mode 100644 doc/snippets/eigen_alignedbox_snippets.cpp create mode 100644 doc/snippets/eigen_cross_asm.cpp create mode 100644 doc/snippets/eigen_cross_snippets.cpp create mode 100644 doc/snippets/eigen_norm_asm.cpp create mode 100644 doc/snippets/eigen_norm_snippets.cpp create mode 100644 doc/snippets/eigen_select_asm.cpp create mode 100644 doc/snippets/eigen_select_snippets.cpp create mode 100644 doc/snippets/reduce_asm.cpp create mode 100644 doc/snippets/reduce_snippets.cpp create mode 100644 doc/snippets/sgn_asm.cpp create mode 100644 doc/snippets/sgn_snippets.cpp create mode 100644 doc/snippets/sqrt_asm.cpp create mode 100644 doc/snippets/sqrt_snippets.cpp create mode 100644 doc/snippets/tolerance_asm.cpp create mode 100644 doc/snippets/tolerance_snippets.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b7309c7..e2c6377 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,28 @@ if("^${CMAKE_SOURCE_DIR}$" STREQUAL "^${PROJECT_SOURCE_DIR}$") doc/snippets/float8_asm.cpp doc/snippets/select_snippets.cpp doc/snippets/select_asm.cpp + doc/snippets/sqrt_snippets.cpp + doc/snippets/sqrt_asm.cpp + doc/snippets/approx_snippets.cpp + doc/snippets/approx_asm.cpp + doc/snippets/sgn_snippets.cpp + doc/snippets/sgn_asm.cpp + doc/snippets/tolerance_snippets.cpp + doc/snippets/tolerance_asm.cpp + doc/snippets/reduce_snippets.cpp + doc/snippets/reduce_asm.cpp + doc/snippets/dot_snippets.cpp + doc/snippets/dot_asm.cpp + doc/snippets/eigen_add_snippets.cpp + doc/snippets/eigen_add_asm.cpp + doc/snippets/eigen_norm_snippets.cpp + doc/snippets/eigen_norm_asm.cpp + doc/snippets/eigen_cross_snippets.cpp + doc/snippets/eigen_cross_asm.cpp + doc/snippets/eigen_select_snippets.cpp + doc/snippets/eigen_select_asm.cpp + doc/snippets/eigen_alignedbox_snippets.cpp + doc/snippets/eigen_alignedbox_asm.cpp ) weaver_target_sources(weaver_tests "doc" ${WEAVER_DOC_SNIPPET_SRC}) @@ -205,13 +227,57 @@ if(WEAVER_BUILD_DOCS) add_library(weaver_doc_asm OBJECT doc/snippets/float8_asm.cpp doc/snippets/select_asm.cpp + doc/snippets/sqrt_asm.cpp + doc/snippets/approx_asm.cpp + doc/snippets/sgn_asm.cpp + doc/snippets/tolerance_asm.cpp + doc/snippets/reduce_asm.cpp + doc/snippets/dot_asm.cpp + doc/snippets/eigen_add_asm.cpp + doc/snippets/eigen_norm_asm.cpp + doc/snippets/eigen_cross_asm.cpp + doc/snippets/eigen_select_asm.cpp + doc/snippets/eigen_alignedbox_asm.cpp ) weaver_configure_target(weaver_doc_asm) target_link_libraries(weaver_doc_asm PRIVATE Eigen3::Eigen Catch2::Catch2WithMain) target_include_directories(weaver_doc_asm PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") set(WEAVER_ASM_DIR "${CMAKE_CURRENT_BINARY_DIR}/doc/asm") - set(WEAVER_ASM_SYMBOLS add_float8 select_max) + set(WEAVER_ASM_SYMBOLS + add_float8 select_max + sqrt_float8 rcp_float8 rsqrt_float8 sgn_float8 + equal_tol_float8 any_greater_float8 dot_float8 + add_vec3_float8 squared_norm_vec3_float8 cross_vec3_float8 + select_vec3_float8 merge_box2_float8 + ) + + # Where each symbol's arguments arrive, System V AMD64 (WEAVER_BUILD_DOCS + # only ever runs gcc/clang on Linux/macOS, so this is the only calling + # convention in play). Not derived automatically -- see extract_asm.py's + # --params for why -- so double-check this against the real disassembly + # whenever a signature here changes or a new example is added below. + # weaver::floatN/boolN arguments up to 32 bytes are trivial-for-calls and + # arrive directly in the next xmm/ymm register; Eigen::Matrix/AlignedBox + # arguments are not, so they arrive as a pointer in the next integer + # register instead, and when the *return* type is likewise non-trivial + # the caller additionally passes a hidden return-value pointer in RDI + # ahead of the real parameters. + set(WEAVER_ASM_PARAMS_add_float8 "ymm0 = a, ymm1 = b") + set(WEAVER_ASM_PARAMS_select_max "ymm0 = a, ymm1 = b") + set(WEAVER_ASM_PARAMS_sqrt_float8 "ymm0 = a") + set(WEAVER_ASM_PARAMS_rcp_float8 "ymm0 = a") + set(WEAVER_ASM_PARAMS_rsqrt_float8 "ymm0 = a") + set(WEAVER_ASM_PARAMS_sgn_float8 "ymm0 = a") + set(WEAVER_ASM_PARAMS_equal_tol_float8 "ymm0 = a, ymm1 = b, ymm2 = tol") + set(WEAVER_ASM_PARAMS_any_greater_float8 "ymm0 = a, ymm1 = b") + set(WEAVER_ASM_PARAMS_dot_float8 "ymm0 = a, ymm1 = b") + set(WEAVER_ASM_PARAMS_add_vec3_float8 "rdi = return-value pointer, rsi = &a, rdx = &b") + set(WEAVER_ASM_PARAMS_squared_norm_vec3_float8 "rdi = &v") + set(WEAVER_ASM_PARAMS_cross_vec3_float8 "rdi = return-value pointer, rsi = &a, rdx = &b") + set(WEAVER_ASM_PARAMS_select_vec3_float8 "ymm0 = mask, rdi = return-value pointer, rsi = &a, rdx = &b") + set(WEAVER_ASM_PARAMS_merge_box2_float8 "rdi = return-value pointer, rsi = &a, rdx = &b") + set(WEAVER_ASM_OUTPUTS "") foreach(sym IN LISTS WEAVER_ASM_SYMBOLS) set(out "${WEAVER_ASM_DIR}/${sym}.txt") @@ -222,6 +288,7 @@ if(WEAVER_BUILD_DOCS) --objdump "${WEAVER_OBJDUMP}" --object "$" --symbol "${sym}" + --params "${WEAVER_ASM_PARAMS_${sym}}" --output "${out}" DEPENDS weaver_doc_asm "$" COMMENT "Extracting assembly for ${sym}" diff --git a/doc/snippets/approx_asm.cpp b/doc/snippets/approx_asm.cpp new file mode 100644 index 0000000..ecafa1c --- /dev/null +++ b/doc/snippets/approx_asm.cpp @@ -0,0 +1,52 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// C++ standard library +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why these functions take their +// operands as parameters rather than literals. + +//! [Approximate reciprocal float8 asm] +weaver::float8 rcp_float8(weaver::float8 a) +{ + return weaver::rcp(a); +} +//! [Approximate reciprocal float8 asm] + +//! [Approximate reciprocal square root float8 asm] +weaver::float8 rsqrt_float8(weaver::float8 a) +{ + return weaver::rsqrt(a); +} +//! [Approximate reciprocal square root float8 asm] + +TEST_CASE("Approximate reciprocal float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ 2.0f, 4.0f, 8.0f, 5.0f, 1.0f, 10.0f, 16.0f, 20.0f }; + + weaver::float8 r = rcp_float8(a); + for (int i = 0; i < 8; ++i) + REQUIRE(std::fabs(r[i] - 1.0f / a[i]) < 1e-2f * (1.0f / a[i])); +} + +TEST_CASE("Approximate reciprocal square root float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ 4.0f, 9.0f, 16.0f, 25.0f, 1.0f, 100.0f, 64.0f, 36.0f }; + + weaver::float8 r = rsqrt_float8(a); + for (int i = 0; i < 8; ++i) + { + float exact = 1.0f / std::sqrt(a[i]); + REQUIRE(std::fabs(r[i] - exact) < 1e-2f * exact); + } +} diff --git a/doc/snippets/approx_snippets.cpp b/doc/snippets/approx_snippets.cpp new file mode 100644 index 0000000..4012d5e --- /dev/null +++ b/doc/snippets/approx_snippets.cpp @@ -0,0 +1,48 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// C++ standard library +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Approximate reciprocal float8", "[doc]") +{ + //! [Approximate reciprocal float8] + weaver::float8 x{ 2.0f, 4.0f, 8.0f, 5.0f, 1.0f, 10.0f, 16.0f, 20.0f }; + + // An approximation of `1 / x`, not an exact divide -- trades a couple of + // ULPs of accuracy for a much cheaper instruction. + weaver::float8 r = weaver::rcp(x); + //! [Approximate reciprocal float8] + + for (int i = 0; i < 8; ++i) + REQUIRE(std::fabs(r[i] - 1.0f / x[i]) < 1e-2f * (1.0f / x[i])); +} + +TEST_CASE("Approximate reciprocal square root float8", "[doc]") +{ + //! [Approximate reciprocal square root float8] + weaver::float8 x{ 4.0f, 9.0f, 16.0f, 25.0f, 1.0f, 100.0f, 64.0f, 36.0f }; + + // An approximation of `1 / sqrt(x)` that avoids both the divide and the + // square root -- the common case for normalizing a vector. + weaver::float8 r = weaver::rsqrt(x); + //! [Approximate reciprocal square root float8] + + for (int i = 0; i < 8; ++i) + { + float exact = 1.0f / std::sqrt(x[i]); + REQUIRE(std::fabs(r[i] - exact) < 1e-2f * exact); + } +} diff --git a/doc/snippets/dot_asm.cpp b/doc/snippets/dot_asm.cpp new file mode 100644 index 0000000..663604d --- /dev/null +++ b/doc/snippets/dot_asm.cpp @@ -0,0 +1,29 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Dot product float8 asm] +float dot_float8(weaver::float8 a, weaver::float8 b) +{ + return a.dot(b); +} +//! [Dot product float8 asm] + +TEST_CASE("Dot product float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + weaver::float8 b{ 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f }; + + REQUIRE(dot_float8(a, b) == 16.0f); +} diff --git a/doc/snippets/dot_snippets.cpp b/doc/snippets/dot_snippets.cpp new file mode 100644 index 0000000..1b8b059 --- /dev/null +++ b/doc/snippets/dot_snippets.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Dot product float8", "[doc]") +{ + //! [Dot product float8] + weaver::float8 x{ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + weaver::float8 y{ 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f }; + + // Unlike the elementwise ops above, this collapses all 8 lanes into one + // plain `float` -- a horizontal reduction, not a per-lane result. + float d = x.dot(y); + //! [Dot product float8] + + REQUIRE(d == 16.0f); +} diff --git a/doc/snippets/eigen_add_asm.cpp b/doc/snippets/eigen_add_asm.cpp new file mode 100644 index 0000000..c2666c2 --- /dev/null +++ b/doc/snippets/eigen_add_asm.cpp @@ -0,0 +1,34 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Eigen vector add with float8 asm] +Eigen::Matrix add_vec3_float8( + Eigen::Matrix a, + Eigen::Matrix b) +{ + return a + b; +} +//! [Eigen vector add with float8 asm] + +TEST_CASE("Eigen vector add with float8 (asm demo)", "[doc][eigen]") +{ + Eigen::Matrix a{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix b{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + + auto c = add_vec3_float8(a, b); + REQUIRE(weaver::all(c.x() == weaver::float8{5.0f})); + REQUIRE(weaver::all(c.y() == weaver::float8{7.0f})); + REQUIRE(weaver::all(c.z() == weaver::float8{9.0f})); +} diff --git a/doc/snippets/eigen_add_snippets.cpp b/doc/snippets/eigen_add_snippets.cpp new file mode 100644 index 0000000..a7c0da9 --- /dev/null +++ b/doc/snippets/eigen_add_snippets.cpp @@ -0,0 +1,30 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Eigen vector add with float8", "[doc][eigen]") +{ + //! [Eigen vector add with float8] + // weaver::float8 works as the Scalar of an Eigen::Matrix directly, no + // special vector type required -- this Vector3 holds 8 3D points. + Eigen::Matrix a{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix b{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + + Eigen::Matrix c = a + b; + //! [Eigen vector add with float8] + + REQUIRE(weaver::all(c.x() == weaver::float8{5.0f})); + REQUIRE(weaver::all(c.y() == weaver::float8{7.0f})); + REQUIRE(weaver::all(c.z() == weaver::float8{9.0f})); +} diff --git a/doc/snippets/eigen_alignedbox_asm.cpp b/doc/snippets/eigen_alignedbox_asm.cpp new file mode 100644 index 0000000..13d1d80 --- /dev/null +++ b/doc/snippets/eigen_alignedbox_asm.cpp @@ -0,0 +1,42 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Eigen +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Merge axis-aligned boxes with float8 asm] +Eigen::AlignedBox merge_box2_float8( + Eigen::AlignedBox a, + Eigen::AlignedBox b) +{ + return a.merged(b); +} +//! [Merge axis-aligned boxes with float8 asm] + +TEST_CASE("Merge axis-aligned boxes with float8 (asm demo)", "[doc][eigen]") +{ + Eigen::AlignedBox a{ + Eigen::Matrix{ weaver::float8{-1.0f}, weaver::float8{-1.0f} }, + Eigen::Matrix{ weaver::float8{1.0f}, weaver::float8{1.0f} } + }; + Eigen::AlignedBox b{ + Eigen::Matrix{ weaver::float8{0.0f}, weaver::float8{0.0f} }, + Eigen::Matrix{ weaver::float8{2.0f}, weaver::float8{2.0f} } + }; + + auto merged = merge_box2_float8(a, b); + REQUIRE(weaver::all(merged.min().x() == weaver::float8{-1.0f})); + REQUIRE(weaver::all(merged.max().x() == weaver::float8{2.0f})); +} diff --git a/doc/snippets/eigen_alignedbox_snippets.cpp b/doc/snippets/eigen_alignedbox_snippets.cpp new file mode 100644 index 0000000..ce472f3 --- /dev/null +++ b/doc/snippets/eigen_alignedbox_snippets.cpp @@ -0,0 +1,38 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Eigen +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Merge axis-aligned boxes with float8", "[doc][eigen]") +{ + //! [Merge axis-aligned boxes with float8] + // 8 independent 2D boxes, merged in one call -- Eigen::AlignedBox needs + // no changes to work with weaver::float8 as its Scalar. + Eigen::AlignedBox a{ + Eigen::Matrix{ weaver::float8{-1.0f}, weaver::float8{-1.0f} }, + Eigen::Matrix{ weaver::float8{1.0f}, weaver::float8{1.0f} } + }; + Eigen::AlignedBox b{ + Eigen::Matrix{ weaver::float8{0.0f}, weaver::float8{0.0f} }, + Eigen::Matrix{ weaver::float8{2.0f}, weaver::float8{2.0f} } + }; + + Eigen::AlignedBox merged = a.merged(b); + //! [Merge axis-aligned boxes with float8] + + REQUIRE(weaver::all(merged.min().x() == weaver::float8{-1.0f})); + REQUIRE(weaver::all(merged.max().x() == weaver::float8{2.0f})); +} diff --git a/doc/snippets/eigen_cross_asm.cpp b/doc/snippets/eigen_cross_asm.cpp new file mode 100644 index 0000000..3dd8e0b --- /dev/null +++ b/doc/snippets/eigen_cross_asm.cpp @@ -0,0 +1,37 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Eigen +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Eigen cross product with float8 asm] +Eigen::Matrix cross_vec3_float8( + Eigen::Matrix a, + Eigen::Matrix b) +{ + return a.cross(b); +} +//! [Eigen cross product with float8 asm] + +TEST_CASE("Eigen cross product with float8 (asm demo)", "[doc][eigen]") +{ + Eigen::Matrix a{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix b{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + + auto c = cross_vec3_float8(a, b); + REQUIRE(weaver::all(c.x() == weaver::float8{-3.0f})); + REQUIRE(weaver::all(c.y() == weaver::float8{6.0f})); + REQUIRE(weaver::all(c.z() == weaver::float8{-3.0f})); +} diff --git a/doc/snippets/eigen_cross_snippets.cpp b/doc/snippets/eigen_cross_snippets.cpp new file mode 100644 index 0000000..a17473b --- /dev/null +++ b/doc/snippets/eigen_cross_snippets.cpp @@ -0,0 +1,31 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Eigen +#include + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Eigen cross product with float8", "[doc][eigen]") +{ + //! [Eigen cross product with float8] + Eigen::Matrix a{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix b{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + + Eigen::Matrix c = a.cross(b); + //! [Eigen cross product with float8] + + REQUIRE(weaver::all(c.x() == weaver::float8{-3.0f})); + REQUIRE(weaver::all(c.y() == weaver::float8{6.0f})); + REQUIRE(weaver::all(c.z() == weaver::float8{-3.0f})); +} diff --git a/doc/snippets/eigen_norm_asm.cpp b/doc/snippets/eigen_norm_asm.cpp new file mode 100644 index 0000000..4d9c3b4 --- /dev/null +++ b/doc/snippets/eigen_norm_asm.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Eigen squared norm with float8 asm] +weaver::float8 squared_norm_vec3_float8(Eigen::Matrix v) +{ + return v.squaredNorm(); +} +//! [Eigen squared norm with float8 asm] + +TEST_CASE("Eigen squared norm with float8 (asm demo)", "[doc][eigen]") +{ + Eigen::Matrix v{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + + REQUIRE(weaver::all(squared_norm_vec3_float8(v) == weaver::float8{14.0f})); +} diff --git a/doc/snippets/eigen_norm_snippets.cpp b/doc/snippets/eigen_norm_snippets.cpp new file mode 100644 index 0000000..6dac371 --- /dev/null +++ b/doc/snippets/eigen_norm_snippets.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Eigen squared norm with float8", "[doc][eigen]") +{ + //! [Eigen squared norm with float8] + Eigen::Matrix v{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + + // squaredNorm() is an Eigen algorithm, not a FloatWeaver primitive -- + // nobody wrote it for this Scalar type, Eigen just calls operator* and + // operator+ generically. + weaver::float8 lenSq = v.squaredNorm(); + //! [Eigen squared norm with float8] + + REQUIRE(weaver::all(lenSq == weaver::float8{14.0f})); +} diff --git a/doc/snippets/eigen_select_asm.cpp b/doc/snippets/eigen_select_asm.cpp new file mode 100644 index 0000000..1be1f64 --- /dev/null +++ b/doc/snippets/eigen_select_asm.cpp @@ -0,0 +1,35 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Branchless select over an Eigen vector asm] +Eigen::Matrix select_vec3_float8( + weaver::VectorScalar mask, + Eigen::Matrix a, + Eigen::Matrix b) +{ + return weaver::select(mask, a, b); +} +//! [Branchless select over an Eigen vector asm] + +TEST_CASE("Branchless select over an Eigen vector (asm demo)", "[doc][eigen]") +{ + Eigen::Matrix fast{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix slow{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + weaver::VectorScalar useFast{ true, false, true, false, true, false, true, false }; + + auto v = select_vec3_float8(useFast, fast, slow); + REQUIRE(v.x()[0] == 1.0f); + REQUIRE(v.x()[1] == 4.0f); +} diff --git a/doc/snippets/eigen_select_snippets.cpp b/doc/snippets/eigen_select_snippets.cpp new file mode 100644 index 0000000..e50b505 --- /dev/null +++ b/doc/snippets/eigen_select_snippets.cpp @@ -0,0 +1,32 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Branchless select over an Eigen vector", "[doc][eigen]") +{ + //! [Branchless select over an Eigen vector] + // Two candidate velocities for 8 particles at once. + Eigen::Matrix fast{ weaver::float8{1.0f}, weaver::float8{2.0f}, weaver::float8{3.0f} }; + Eigen::Matrix slow{ weaver::float8{4.0f}, weaver::float8{5.0f}, weaver::float8{6.0f} }; + + weaver::VectorScalar useFast{ true, false, true, false, true, false, true, false }; + + // The same branchless pattern as "Pick a value without branching", but + // applied component-wise to a whole Eigen vector instead of one scalar. + Eigen::Matrix v = weaver::select(useFast, fast, slow); + //! [Branchless select over an Eigen vector] + + REQUIRE(v.x()[0] == 1.0f); + REQUIRE(v.x()[1] == 4.0f); +} diff --git a/doc/snippets/reduce_asm.cpp b/doc/snippets/reduce_asm.cpp new file mode 100644 index 0000000..452dba1 --- /dev/null +++ b/doc/snippets/reduce_asm.cpp @@ -0,0 +1,29 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Check a condition across all lanes asm] +bool any_greater_float8(weaver::float8 a, weaver::float8 b) +{ + return weaver::any(a > b); +} +//! [Check a condition across all lanes asm] + +TEST_CASE("Check a condition across all lanes (asm demo)", "[doc]") +{ + weaver::float8 limit{ 9.0f }; + + REQUIRE_FALSE(any_greater_float8(weaver::float8{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f }, limit)); + REQUIRE(any_greater_float8(weaver::float8{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 20.0f }, limit)); +} diff --git a/doc/snippets/reduce_snippets.cpp b/doc/snippets/reduce_snippets.cpp new file mode 100644 index 0000000..999208b --- /dev/null +++ b/doc/snippets/reduce_snippets.cpp @@ -0,0 +1,32 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Check a condition across all lanes", "[doc]") +{ + //! [Check a condition across all lanes] + weaver::float8 x{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 20.0f }; + weaver::float8 limit{ 9.0f }; + + // Per-lane comparisons don't decide anything by themselves -- `any` + // reduces the 8-wide result down to the single bool a real `if` needs. + if (weaver::any(x > limit)) + { + // at least one lane exceeded the limit + } + //! [Check a condition across all lanes] + + REQUIRE(weaver::any(x > limit)); + REQUIRE_FALSE(weaver::all(x > limit)); +} diff --git a/doc/snippets/sgn_asm.cpp b/doc/snippets/sgn_asm.cpp new file mode 100644 index 0000000..3122150 --- /dev/null +++ b/doc/snippets/sgn_asm.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Branchless sign float8 asm] +weaver::float8 sgn_float8(weaver::float8 a) +{ + return weaver::sgn(a); +} +//! [Branchless sign float8 asm] + +TEST_CASE("Branchless sign float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ -3.0f, 0.0f, 2.0f, -0.5f, 7.0f, -7.0f, 0.0f, 1.0f }; + + REQUIRE(weaver::all(sgn_float8(a) == weaver::float8{ -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 1.0f })); +} diff --git a/doc/snippets/sgn_snippets.cpp b/doc/snippets/sgn_snippets.cpp new file mode 100644 index 0000000..328cef3 --- /dev/null +++ b/doc/snippets/sgn_snippets.cpp @@ -0,0 +1,26 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Branchless sign float8", "[doc]") +{ + //! [Branchless sign float8] + weaver::float8 x{ -3.0f, 0.0f, 2.0f, -0.5f, 7.0f, -7.0f, 0.0f, 1.0f }; + + // -1 / 0 / 1 per lane, with no per-lane branch on the sign. + weaver::float8 s = weaver::sgn(x); + //! [Branchless sign float8] + + REQUIRE(weaver::all(s == weaver::float8{ -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 1.0f })); +} diff --git a/doc/snippets/sqrt_asm.cpp b/doc/snippets/sqrt_asm.cpp new file mode 100644 index 0000000..d8e0cc9 --- /dev/null +++ b/doc/snippets/sqrt_asm.cpp @@ -0,0 +1,28 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Square root float8 asm] +weaver::float8 sqrt_float8(weaver::float8 a) +{ + return weaver::sqrt(a); +} +//! [Square root float8 asm] + +TEST_CASE("Square root float8 (asm demo)", "[doc]") +{ + weaver::float8 a{ 4.0f, 9.0f, 16.0f, 25.0f, 1.0f, 100.0f, 64.0f, 36.0f }; + + REQUIRE(weaver::all(sqrt_float8(a) == weaver::float8{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f })); +} diff --git a/doc/snippets/sqrt_snippets.cpp b/doc/snippets/sqrt_snippets.cpp new file mode 100644 index 0000000..dece4ec --- /dev/null +++ b/doc/snippets/sqrt_snippets.cpp @@ -0,0 +1,25 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Square root float8", "[doc]") +{ + //! [Square root float8] + weaver::float8 x{ 4.0f, 9.0f, 16.0f, 25.0f, 1.0f, 100.0f, 64.0f, 36.0f }; + + weaver::float8 r = weaver::sqrt(x); + //! [Square root float8] + + REQUIRE(weaver::all(r == weaver::float8{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f })); +} diff --git a/doc/snippets/tolerance_asm.cpp b/doc/snippets/tolerance_asm.cpp new file mode 100644 index 0000000..84cd8cf --- /dev/null +++ b/doc/snippets/tolerance_asm.cpp @@ -0,0 +1,33 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_asm.cpp for why this function takes its operands +// as parameters rather than literals. + +//! [Compare floats with a tolerance asm] +weaver::VectorScalar equal_tol_float8(weaver::float8 a, weaver::float8 b, weaver::float8 tol) +{ + return weaver::equal(a, b, tol); +} +//! [Compare floats with a tolerance asm] + +TEST_CASE("Compare floats with a tolerance (asm demo)", "[doc]") +{ + weaver::float8 a{ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + weaver::float8 b{ 1.0000001f, 1.1f, 0.9f, 1.0f, 2.0f, 1.00001f, 1.0f, 1.0f }; + + auto close = equal_tol_float8(a, b, weaver::float8{ 1e-4f }); + + bool expected[8] = { true, false, false, true, false, true, true, true }; + for (int i = 0; i < 8; ++i) + REQUIRE(close[i] == expected[i]); +} diff --git a/doc/snippets/tolerance_snippets.cpp b/doc/snippets/tolerance_snippets.cpp new file mode 100644 index 0000000..67594a9 --- /dev/null +++ b/doc/snippets/tolerance_snippets.cpp @@ -0,0 +1,30 @@ +// Copyright 2023 Basil Fierz +// +// Use of this source code is governed by an MIT - style +// license that can be found in the LICENSE file or at +// https ://opensource.org/licenses/MIT. + +// Catch2 +#include + +// FloatWeaver +#include + +// See doc/snippets/float8_snippets.cpp for the tagging convention this file +// follows. + +TEST_CASE("Compare floats with a tolerance", "[doc]") +{ + //! [Compare floats with a tolerance] + weaver::float8 a{ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + weaver::float8 b{ 1.0000001f, 1.1f, 0.9f, 1.0f, 2.0f, 1.00001f, 1.0f, 1.0f }; + + // A relative-tolerance compare, per lane -- see the reference this is + // based on in vector_types.h. + weaver::VectorScalar close = weaver::equal(a, b, weaver::float8{ 1e-4f }); + //! [Compare floats with a tolerance] + + bool expected[8] = { true, false, false, true, false, true, true, true }; + for (int i = 0; i < 8; ++i) + REQUIRE(close[i] == expected[i]); +} diff --git a/doc/sphinx/howto.rst b/doc/sphinx/howto.rst index f17b598..32efc0f 100644 --- a/doc/sphinx/howto.rst +++ b/doc/sphinx/howto.rst @@ -66,3 +66,246 @@ Two instructions, no branch: a compare that produces a per-lane mask, then a blend that picks lanes from ``a`` or ``b`` according to that mask -- every lane gets computed, and the "wrong" one is simply discarded rather than skipped. + +Take a square root +------------------- + +Not just ``+`` compiles down to a single instruction -- most of the +````-style free functions in ``namespace weaver`` do too: + +.. literalinclude:: ../snippets/sqrt_snippets.cpp + :language: cpp + :start-after: //! [Square root float8] + :end-before: //! [Square root float8] + +.. literalinclude:: ../snippets/sqrt_asm.cpp + :language: cpp + :start-after: //! [Square root float8 asm] + :end-before: //! [Square root float8 asm] + +.. weaver-asm:: sqrt_float8 + +Approximate reciprocal and reciprocal square root +--------------------------------------------------- + +``rcp`` and ``rsqrt`` trade a couple of ULPs of accuracy for hardware +approximation instructions that are cheaper than a real divide or square +root -- useful when normalizing a vector or computing a rough inverse where +exactness doesn't matter: + +.. literalinclude:: ../snippets/approx_snippets.cpp + :language: cpp + :start-after: //! [Approximate reciprocal float8] + :end-before: //! [Approximate reciprocal float8] + +.. literalinclude:: ../snippets/approx_asm.cpp + :language: cpp + :start-after: //! [Approximate reciprocal float8 asm] + :end-before: //! [Approximate reciprocal float8 asm] + +.. weaver-asm:: rcp_float8 + +.. literalinclude:: ../snippets/approx_snippets.cpp + :language: cpp + :start-after: //! [Approximate reciprocal square root float8] + :end-before: //! [Approximate reciprocal square root float8] + +.. literalinclude:: ../snippets/approx_asm.cpp + :language: cpp + :start-after: //! [Approximate reciprocal square root float8 asm] + :end-before: //! [Approximate reciprocal square root float8 asm] + +.. weaver-asm:: rsqrt_float8 + +Branchless sign +----------------- + +``sgn`` is a real library function, not a contrived example -- it's what a +branchless sign-without-``abs`` costs in practice: a mask, an and, an or, and +an and: + +.. literalinclude:: ../snippets/sgn_snippets.cpp + :language: cpp + :start-after: //! [Branchless sign float8] + :end-before: //! [Branchless sign float8] + +.. literalinclude:: ../snippets/sgn_asm.cpp + :language: cpp + :start-after: //! [Branchless sign float8 asm] + :end-before: //! [Branchless sign float8 asm] + +.. weaver-asm:: sgn_float8 + +Compare floats with a tolerance +---------------------------------- + +Exact float equality is rarely what you want. ``weaver::equal`` implements a +relative-tolerance compare per lane -- a realistic idiom, a few instructions +longer than a plain ``==``: + +.. literalinclude:: ../snippets/tolerance_snippets.cpp + :language: cpp + :start-after: //! [Compare floats with a tolerance] + :end-before: //! [Compare floats with a tolerance] + +.. literalinclude:: ../snippets/tolerance_asm.cpp + :language: cpp + :start-after: //! [Compare floats with a tolerance asm] + :end-before: //! [Compare floats with a tolerance asm] + +.. weaver-asm:: equal_tol_float8 + +Check a condition across all lanes +------------------------------------- + +Individual lanes can never branch, but FloatWeaver code is still allowed a +*real* ``if`` -- as long as it branches on a single reduced bool, not on a +per-lane result. ``any``/``all``/``none`` are how a per-lane comparison +becomes that bool: + +.. literalinclude:: ../snippets/reduce_snippets.cpp + :language: cpp + :start-after: //! [Check a condition across all lanes] + :end-before: //! [Check a condition across all lanes] + +.. literalinclude:: ../snippets/reduce_asm.cpp + :language: cpp + :start-after: //! [Check a condition across all lanes asm] + :end-before: //! [Check a condition across all lanes asm] + +.. weaver-asm:: any_greater_float8 + +A compare, a ``movmsk`` to pull the per-lane mask bits into a scalar +register, then a plain integer test -- this is the one place in these guides +where a scalar, not a vector, comes out the other end. + +Horizontal reduction: dot product +------------------------------------- + +Every example so far keeps 8 independent results, one per lane. +``dot`` doesn't: it collapses all 8 lanes of both operands into a single +``float``, so the instruction shape looks different from the rest of this +guide -- a multiply-reduce followed by a lane-extraction step, not a +per-lane op: + +.. literalinclude:: ../snippets/dot_snippets.cpp + :language: cpp + :start-after: //! [Dot product float8] + :end-before: //! [Dot product float8] + +.. literalinclude:: ../snippets/dot_asm.cpp + :language: cpp + :start-after: //! [Dot product float8 asm] + :end-before: //! [Dot product float8 asm] + +.. weaver-asm:: dot_float8 + +Using FloatWeaver with Eigen +------------------------------- + +``weaver::float4``/``float8``/``float16`` work as the ``Scalar`` of an +``Eigen::Matrix`` directly (see ``include/weaver/eigen.h``) -- no separate +vector type, no adapter code at the call site. The guides below show that +this isn't just type-level plumbing: ordinary Eigen algorithms, written with +no knowledge of FloatWeaver, compile down to the same kind of vector +instructions as the ``weaver``-native examples above. + +Eigen vector arithmetic +-------------------------- + +The baseline case: an ``Eigen::Matrix`` is a +``Vector3`` holding 8 points at once, and ``operator+`` works exactly as it +would for ``Eigen::Vector3f``: + +.. literalinclude:: ../snippets/eigen_add_snippets.cpp + :language: cpp + :start-after: //! [Eigen vector add with float8] + :end-before: //! [Eigen vector add with float8] + +.. literalinclude:: ../snippets/eigen_add_asm.cpp + :language: cpp + :start-after: //! [Eigen vector add with float8 asm] + :end-before: //! [Eigen vector add with float8 asm] + +.. weaver-asm:: add_vec3_float8 + +Three ``vaddps``, one per vector component -- proof the ``NumTraits``/ +``numext`` wiring costs nothing over calling ``weaver::float8``'s own +``operator+`` directly. + +Squared norm and Eigen algorithms +------------------------------------- + +``squaredNorm()`` was never written for ``weaver::float8`` -- Eigen builds it +generically out of ``operator*`` and ``operator+``, the same way it would for +any other ``Scalar``: + +.. literalinclude:: ../snippets/eigen_norm_snippets.cpp + :language: cpp + :start-after: //! [Eigen squared norm with float8] + :end-before: //! [Eigen squared norm with float8] + +.. literalinclude:: ../snippets/eigen_norm_asm.cpp + :language: cpp + :start-after: //! [Eigen squared norm with float8 asm] + :end-before: //! [Eigen squared norm with float8 asm] + +.. weaver-asm:: squared_norm_vec3_float8 + +Cross product +---------------- + +``cross()`` is the classic shuffle-and-multiply pattern -- three multiplies +and three fused multiply-subtracts, still one screenful of assembly: + +.. literalinclude:: ../snippets/eigen_cross_snippets.cpp + :language: cpp + :start-after: //! [Eigen cross product with float8] + :end-before: //! [Eigen cross product with float8] + +.. literalinclude:: ../snippets/eigen_cross_asm.cpp + :language: cpp + :start-after: //! [Eigen cross product with float8 asm] + :end-before: //! [Eigen cross product with float8 asm] + +.. weaver-asm:: cross_vec3_float8 + +Branchless select over an Eigen vector +------------------------------------------- + +The same branchless pattern as "Pick a value without branching" above, but +applied to a whole vector-valued quantity at once -- e.g. choosing one of two +candidate velocities for 8 particles in a single call, one blend per +component: + +.. literalinclude:: ../snippets/eigen_select_snippets.cpp + :language: cpp + :start-after: //! [Branchless select over an Eigen vector] + :end-before: //! [Branchless select over an Eigen vector] + +.. literalinclude:: ../snippets/eigen_select_asm.cpp + :language: cpp + :start-after: //! [Branchless select over an Eigen vector asm] + :end-before: //! [Branchless select over an Eigen vector asm] + +.. weaver-asm:: select_vec3_float8 + +Merge axis-aligned boxes +---------------------------- + +This is closer to why FloatWeaver exists than a toy example: +``Eigen::AlignedBox`` needs no changes at all to operate on 8 boxes +simultaneously. ``merged()`` takes the componentwise min of both boxes' +minimums and the max of their maximums: + +.. literalinclude:: ../snippets/eigen_alignedbox_snippets.cpp + :language: cpp + :start-after: //! [Merge axis-aligned boxes with float8] + :end-before: //! [Merge axis-aligned boxes with float8] + +.. literalinclude:: ../snippets/eigen_alignedbox_asm.cpp + :language: cpp + :start-after: //! [Merge axis-aligned boxes with float8 asm] + :end-before: //! [Merge axis-aligned boxes with float8 asm] + +.. weaver-asm:: merge_box2_float8 diff --git a/doc/tools/extract_asm.py b/doc/tools/extract_asm.py index 79dde7b..6e9bb34 100644 --- a/doc/tools/extract_asm.py +++ b/doc/tools/extract_asm.py @@ -26,6 +26,22 @@ # Matches one instruction line, e.g. " 130:\tendbr64" or " 134:\tvaddps ymm0,ymm1,ymm0" _INSN_RE = re.compile(r"^\s*[0-9a-f]+:\t(?P.*)$") +# The CET landing pad gcc/clang emit as the first instruction of every +# function on Linux (-fcf-protection, on by default) -- carries no +# information about what the function computes, so it's dropped rather than +# shown as if it were part of the example. +_CET_LANDING_PAD_RE = re.compile(r"^endbr(32|64)$") + +# Inter-function alignment padding: objdump keeps printing instruction-shaped +# lines after a function's last `ret`/`jmp` right up to the next label, +# because the assembler pads the gap between functions with nops so the next +# symbol starts on an aligned address. None of this executes as part of the +# function -- it's a side effect of *where* the linker put the next symbol, +# not of what this one does -- so it's stripped from the end of the listing. +# Safe to do unconditionally: real function bodies don't end in a run of nops, +# so this only ever matches genuine trailing padding. +_PADDING_RE = re.compile(r"^(data16\s+)*(cs\s+)?(nop\b.*|xchg\s+ax,ax)$") + def extract(objdump_output: str, symbol: str) -> list[str]: lines = objdump_output.splitlines() @@ -46,6 +62,12 @@ def extract(objdump_output: str, symbol: str) -> list[str]: text = insn.group("insn").split("#", 1)[0].rstrip() if text: insns.append(text) + + if insns and _CET_LANDING_PAD_RE.match(insns[0]): + insns.pop(0) + while insns and _PADDING_RE.match(insns[-1]): + insns.pop() + return insns @@ -64,6 +86,19 @@ def main() -> int: ) parser.add_argument("--symbol", required=True, help="plain (undecorated) function name to extract") parser.add_argument("--output", required=True, help="where to write the extracted instructions") + parser.add_argument( + "--params", + default="", + help="optional 'reg = param, reg = param, ...' note on where the function's " + "arguments arrive, written as a leading comment. Not derived from the " + "disassembly -- the System V AMD64 calling convention (what " + "WEAVER_BUILD_DOCS always targets: gcc/clang on Linux/macOS) is " + "deterministic given a signature, but reliably re-deriving it in Python " + "would mean reimplementing its parameter-classification rules; instead " + "this is filled in by hand next to the symbol in WEAVER_ASM_SYMBOLS, " + "the same place a new example's *_asm.cpp already gets added, and " + "checked against the real disassembly when it's added.", + ) args = parser.parse_args() for obj in args.object.split(";"): @@ -78,8 +113,9 @@ def main() -> int: insns = extract(result.stdout, args.symbol) if insns: + lines = ([f"; {args.params}"] if args.params else []) + insns with open(args.output, "w") as f: - f.write("\n".join(insns) + "\n") + f.write("\n".join(lines) + "\n") return 0 print( From 477740dadf54a096c6017768fdac0f807b26df1f Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Fri, 18 Sep 2026 21:59:15 +0200 Subject: [PATCH 5/6] Deploy the built docs to GitHub Pages on push to main Adds a deploy job (gated to main pushes, so PRs still only build and validate) that uploads the Sphinx output as a Pages artifact and publishes it via actions/deploy-pages. --- .github/workflows/docs.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6e395a1..b1ecfcb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,16 @@ on: pull_request: branches: ['main'] +# Only one Pages deployment at a time; a push while one is still running +# waits for it rather than racing it, but never cancels an in-progress +# deployment (a half-published site would be worse than a delayed one). +concurrency: + group: 'pages' + cancel-in-progress: false + +permissions: + contents: read + jobs: docs: runs-on: ubuntu-latest @@ -59,3 +69,30 @@ jobs: # ubuntu-latest already, no separate install needed. - name: Build documentation run: cmake --build ${{ github.workspace }}/build --target docs + + # Runs on every push and PR (see above), but the artifact is only ever + # deployed by the `deploy` job below, which is gated to pushes on + # `main` -- so a PR still gets full build validation without + # publishing whatever's in the PR branch. + - name: Upload documentation artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ${{ github.workspace }}/build/sphinx + + deploy: + needs: docs + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From ab648f2f530b40599fbd4846ca39a40ecd11bbba Mon Sep 17 00:00:00 2001 From: Basil Fierz Date: Sun, 20 Sep 2026 21:10:07 +0200 Subject: [PATCH 6/6] Fix sqrt how-to guide to tolerance-compare, matching the library's own convention The arm-linux-gnueabihf/QEMU CI leg failed: NEON has no native sqrt instruction, so weaver::sqrt there is built from an approximate rsqrt (see intrinsics_neon.cpp), not bit-exact like the x86 vsqrtps path. The guide's REQUIRE used exact `==`, which happened to round-trip on every other backend but missed by an ULP here. Switched to weaver::equal(..., tol) with 1e-5f, the same tolerance the library's own "Sqrt FloatN" test (src/tests/simd.cpp) already uses for the same reason. --- doc/snippets/sqrt_asm.cpp | 4 +++- doc/snippets/sqrt_snippets.cpp | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/snippets/sqrt_asm.cpp b/doc/snippets/sqrt_asm.cpp index d8e0cc9..41299f0 100644 --- a/doc/snippets/sqrt_asm.cpp +++ b/doc/snippets/sqrt_asm.cpp @@ -24,5 +24,7 @@ TEST_CASE("Square root float8 (asm demo)", "[doc]") { weaver::float8 a{ 4.0f, 9.0f, 16.0f, 25.0f, 1.0f, 100.0f, 64.0f, 36.0f }; - REQUIRE(weaver::all(sqrt_float8(a) == weaver::float8{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f })); + // See doc/snippets/sqrt_snippets.cpp for why this is a tolerance compare. + weaver::float8 expected{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f }; + REQUIRE(weaver::all(weaver::equal(sqrt_float8(a), expected, weaver::float8{ 1e-5f }))); } diff --git a/doc/snippets/sqrt_snippets.cpp b/doc/snippets/sqrt_snippets.cpp index dece4ec..0783fc0 100644 --- a/doc/snippets/sqrt_snippets.cpp +++ b/doc/snippets/sqrt_snippets.cpp @@ -21,5 +21,10 @@ TEST_CASE("Square root float8", "[doc]") weaver::float8 r = weaver::sqrt(x); //! [Square root float8] - REQUIRE(weaver::all(r == weaver::float8{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f })); + // Not every backend has a native sqrt instruction -- NEON without one + // builds it from an approximate rsqrt, so this isn't bit-exact across + // backends and is compared with a tolerance like the library's own + // "Sqrt FloatN" test (src/tests/simd.cpp) does. + weaver::float8 expected{ 2.0f, 3.0f, 4.0f, 5.0f, 1.0f, 10.0f, 8.0f, 6.0f }; + REQUIRE(weaver::all(weaver::equal(r, expected, weaver::float8{ 1e-5f }))); }