diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ac4444e --- /dev/null +++ b/.clang-format @@ -0,0 +1,29 @@ +BasedOnStyle: Chromium +IndentWidth: 2 +ColumnLimit: 120 + +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowAllArgumentsOnNextLine: false +SortIncludes: false + +# Available starting from clang_format 18 +# AllowShortCompoundRequirementOnASingleLine: true + +AccessModifierOffset: '-2' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignEscapedNewlines: Left +AlignTrailingComments: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +BinPackArguments: true +BinPackParameters: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true +IndentPPDirectives: AfterHash +SortUsingDeclarations: true \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..252f4f2 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,25 @@ +--- +Checks: "*, + -abseil-*, + -altera-*, + -android-*, + -fuchsia-*, + -google-*, + -llvm*, + -modernize-use-trailing-return-type, + -zircon-*, + -readability-else-after-return, + -readability-static-accessed-through-instance, + -readability-avoid-const-params-in-decls, + -cppcoreguidelines-non-private-member-variables-in-classes, + -misc-non-private-member-variables-in-classes, + -hicpp-named-parameter, + -clang-diagnostic-unknown-attributes, + -readability-named-parameter, + -*-avoid-c-arrays, + -*-magic-numbers, + -*-union-access +" +WarningsAsErrors: '' +HeaderFilterRegex: '' +FormatStyle: none \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9cb2f56 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the Y:\che\palliate codebase based on best match to current usage at 16/01/2022 +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference + +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +end_of_line = lf + +[*.py] +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dcf18cf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.h linguist-language=cpp + +* text=auto eol=lf diff --git a/.github/conan.profile b/.github/conan.profile new file mode 100644 index 0000000..a31a3d5 --- /dev/null +++ b/.github/conan.profile @@ -0,0 +1,11 @@ +[settings] +arch=x86_64 +compiler=gcc +compiler.cppstd=23 +compiler.libcxx=libstdc++11 +compiler.version=13 +os=Linux + +[buildenv] +CC=/usr/bin/gcc +CXX=/usr/bin/g++ diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml new file mode 100644 index 0000000..12264ca --- /dev/null +++ b/.github/workflows/codegen.yml @@ -0,0 +1,41 @@ +name: Run codegen + +on: + push: + branches: ["master", "develop"] + workflow_dispatch: + +# only run one at a time, discard unfinished runs +concurrency: + group: "codegen" + cancel-in-progress: true + +jobs: + codegen: + permissions: + contents: write + pull-requests: write + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup palgen + uses: palliate/palgen@master + with: + run: false + requirements: tools/requirements.txt + + - name: Run codegen + run: palgen --debug codegen + + - name: PR changes + uses: peter-evans/create-pull-request@v6 + with: + commit-message: Rerun codegen + author: Codegen Bot + branch: codegen/uncommitted + delete-branch: true + title: Codegen update + draft: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4734932 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: ["master", "develop"] + pull_request: + branches: ["master"] + workflow_dispatch: + +jobs: + run-tests: + runs-on: ubuntu-latest + container: fedora:latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: dnf -y install conan + + - name: Set up Conan profile + run: | + conan profile detect + cp .github/conan.profile `conan profile path default` + + - uses: actions/checkout@v3 + - name: Build + run: conan build . --build=missing -o sanitizers=True -s build_type=Debug + + - name: Test Package + run: | + conan editable add . + conan test test_package slo/0.1 -s build_type=Debug + + - name: Test + run: ./build/Debug/slo_test diff --git a/.gitignore b/.gitignore index 728d411..f765140 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,21 @@ *.out *.app +# Python +*.pyc +**/__pycache__/ + # IDE files .vscode + +# Build files +**/build/ + +# misc +**/compile_commands.json +**/scratchpad*.* +**/CMakeUserPresets.json + +# Caches +.cache +.pytest_cache \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..211bfad --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.15) +include(cmake/warnings.cmake) + +project(slo CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +add_library(slo INTERFACE) + +set_property(TARGET slo PROPERTY CXX_STANDARD 23) + +target_include_directories(slo INTERFACE + $ + $) + + +option(BUILD_TESTING "Enable tests" ON) +option(ENABLE_SANITIZERS "Enable asan and ubsan" OFF) +option(ENABLE_COVERAGE "Enable coverage instrumentation" OFF) +option(ENABLE_BENCHMARK "Enable benchmarks" OFF) + +if(NOT BUILD_TESTING STREQUAL OFF) + message(STATUS "Building unit tests") + + enable_testing() + add_executable(slo_test "") + + enable_warnings(slo_test) + + find_package(GTest REQUIRED) + target_link_libraries(slo_test PRIVATE slo) + target_link_libraries(slo_test PRIVATE GTest::gtest GTest::gtest_main GTest::gmock) + + include(GoogleTest) + gtest_discover_tests(slo_test) + + add_subdirectory(test) + + if(ENABLE_COVERAGE) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(slo_test PRIVATE -g -O0 --coverage -fprofile-arcs -ftest-coverage -fprofile-abs-path) + target_link_libraries(slo_test PRIVATE gcov) + message(STATUS "Instrumenting for coverage") + else() + message(FATAL_ERROR "Currently only GCC is supported for coverage instrumentation.") + endif() + endif() + + if(ENABLE_SANITIZERS AND WIN32) + message(STATUS "Enabling sanitizers") + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(slo_test INTERFACE -fsanitize=address) + target_link_options(slo_test INTERFACE -fsanitize=address) + endif() + elseif(ENABLE_SANITIZERS) + message(STATUS "Enabling sanitizers") + target_compile_options(slo_test INTERFACE -fsanitize=address,undefined) + target_link_options(slo_test INTERFACE -fsanitize=address,undefined) + endif() +endif() + +if (ENABLE_BENCHMARK) + message(STATUS "Building runtime benchmarks") + add_executable(slo_benchmark "") + add_subdirectory(benchmark) + find_package(benchmark REQUIRED) + target_link_libraries(slo_benchmark PRIVATE benchmark::benchmark_main) + target_link_libraries(slo_benchmark PRIVATE slo) +endif() + +## headers +set_target_properties(slo PROPERTIES PUBLIC_HEADER "include/slo") +install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/ + DESTINATION include) + +## binaries +install(TARGETS ${TARGET} + ARCHIVE DESTINATION lib # Windows import libraries (.lib) + RUNTIME DESTINATION bin # Windows DLLs + LIBRARY DESTINATION lib) # shared libraries (ie Linux .so) diff --git a/README.md b/README.md index 6e3a603..8eb5c1d 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ -# variant \ No newline at end of file +# `slo::variant` - a fast variant + +This C++23 library implements various tricks to speed up compile time of a variant type. While there are some slight differences, this library aims to fulfill most of the requirements specified in [[variant]](https://standards.pydong.org/c++23/variant). This was initially written as reference implementation for the techniques discussed in [a deep-dive blog post about unions](https://pydong.org/posts/implementing-variant/). + +## Differences +### Storage +A variant is essentially just a record of a sum type and a discriminator. The most natural way to implement this in C++ is by defining variant as a non-union class-type that has two members: the actual union type and some small integer as discriminator. Unfortunately this can introduce additional, undesired padding. + +However, if all alternative types are standard-layout types and the compiler has C++26 `std::is_within_lifetime` (or an intrinsic that can be used to implement it) we can potentially do a little better. If those conditions are met `slo::variant` will automatically prefer the inverted storage strategy. Essentially it works by wrapping every alternative in a struct type that has the tag as first data member - outside of constant evaluated context the tag can be inspected through any alternative (even inactive ones!) since they all share the same common initial sequence. Since we can't just access the tag through an inactive member In constant evaluated context, we need to do a bit more mental gymnastics. To retrieve the tag we must find the currently active alternative - `std::is_within_lifetime` can help with that. Once we have found the active member, we can access the tag through it. + +### Underlying union layout +To generate the underlying union from a list of types, most implementations opt to define it as a recursive union type. Since this recursive type must be walked for a lot of operations (notably `std::get` and the in-place constructors), its recursion depth is a major contributor to bad compile times. Some libraries such as boost:variant2 alleviate that issue by partially specializing the recursive union for a bunch of alternatives at once if possible. This does flatten the recursion depth quite a bit, but it is still linear. + +To get rid of crazy recursion depth with very large variants, `slo::Variant` switches strategy if instantiated with a lot of alternatives. The alternative strategy uses a complete binary tree layout for the underlying union instead. The trade-off is essentially a little bit of time to generate the tree once template gets instantiated for much quicker and also more consistently quick to compile `get`, in-place constructors and so on. + +### Wrapping existing unions +Sometimes for some reason you already have a bare union. To allow you to use it safely, like you could've if it was a variant, the `slo::Union` alias template can be used to generate a variant from it. The `slo::Union` template takes pointers to data members instead of types - so instead of `slo::variant` you would write `slo::Union<&Foo::member_1, &Foo::member_2>`. + +The interesting thing about wrapping a bare union like this is that we now have a flat union type. None of the recursion worries matter anymore and as an added bonus we can (optionally) attempt to hide the discriminator in tail padding of the union member. + +### Visitation +`slo::visit` can use one of 3 visitation strategies +- Generate a sufficiently large switch with macros, discard all unneeded cases. The Microsoft STL way. +- Trigger an optimization pass in GCC/Clang to turn a fold expression into a switch. +- Generate an array of pointers to generated dispatcher functions + +The latter is the fall-back strategy. This is the only strategy that requires an indirect call. Interestingly at the time of writing libstdc++ and libc++ both _always_ generate an array of function pointers when visiting multiple variants - even if the total amount of possible states is very low. `slo::visit` does not - it will also generate jump tables when visiting multiple variants. + + +## Usage +TODO + + +## FAQ +### Why `slo`? Isn't this supposed to be fast? +**S**tandard **L**ibrary **O**ptimizations. Also turns out not many people use the `slo` namespace in their (public) projects. + +## License + +`slo::variant` is provided under the [MIT License](LICENSE). Feel free to use and modify it in your projects. The techniques used in this library are explained in the aforementioned blog post, which is published under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. + + +## Contributing + +If you'd like to contribute to `slo::variant`, please fork the repository, make your changes, and submit a pull request. Contributions are highly welcomed - including but not limited to bug reports, bug fixes, new features, and improvements. + + +## Contact + +For questions or issues related to this library, please [open an issue](https://github.com/tsche/variant/issues). diff --git a/benchmark/algorithm/cbt.cpp b/benchmark/algorithm/cbt.cpp new file mode 100644 index 0000000..580a14c --- /dev/null +++ b/benchmark/algorithm/cbt.cpp @@ -0,0 +1,120 @@ +#include +#include +#include +#include +#define SLO_USE_PACK_INDEXING ON + + + +#if USING(SLO_USE_PACK_INDEXING) || __cpp_pack_indexing >= 202311L +# define SLO_TYPE_AT(Idx, Pack) Pack[Idx] +#define SLO_HAS_TYPE_PACK_INDEXING 1 +#elif __has_builtin(__type_pack_element) +# define SLO_TYPE_AT(Idx, Pack) __type_pack_element +#define SLO_HAS_TYPE_PACK_INDEXING 0 +#else +#define SLO_HAS_TYPE_PACK_INDEXING 0 +# define SLO_TYPE_AT(Idx, Pack) slo::util::type_at> +#endif + +template +union UnionType {}; + +namespace indexing { +namespace detail { + +template