diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..94cfc10 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +# http://clang.llvm.org/docs/ClangFormatStyleOptions.html +BasedOnStyle: LLVM +ColumnLimit: 100 +IndentWidth: 4 +IndentPPDirectives: AfterHash +IndentRequiresClause: true +PointerAlignment: Left +QualifierAlignment: Right +ReferenceAlignment: Left +AllowShortFunctionsOnASingleLine: false +Standard: Cpp20 diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 0000000..ba223d0 --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,65 @@ +# Copyright (c) 2021-2024 Morwenn +# SPDX-License-Identifier: MIT + +name: MacOS Builds + +on: + push: + paths: + - '.github/workflows/build-macos.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + pull_request: + paths: + - '.github/workflows/build-macos.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + +jobs: + build: + runs-on: macos-11 + + strategy: + fail-fast: false + matrix: + cxx: + - g++-10 + - $(brew --prefix llvm)/bin/clang++ # Clang 17 + config: + # Release build + - build_type: Release + # Debug builds + - build_type: Debug + sanitize: address + - build_type: Debug + sanitize: undefined + + steps: + - uses: actions/checkout@v4 + - uses: seanmiddleditch/gha-setup-ninja@master + + - name: Configure CMake + working-directory: ${{runner.workspace}} + run: | + export CXX=${{matrix.cxx}} + cmake -H${{github.event.repository.name}} -Bbuild \ + -DCMAKE_CONFIGURATION_TYPES=${{matrix.config.build_type}} \ + -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} \ + -DGFX_TIMSORT_SANITIZE=${{matrix.config.sanitize}} \ + -GNinja \ + -DBUILD_BENCHMARKS=ON + + - name: Build the test suite + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake --build . --config ${{matrix.config.build_type}} -j 2 + + - name: Run the test suite + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}}/build + run: ctest -C ${{matrix.config.build_type}} diff --git a/.github/workflows/build-mingw.yml b/.github/workflows/build-mingw.yml new file mode 100644 index 0000000..49d0ab7 --- /dev/null +++ b/.github/workflows/build-mingw.yml @@ -0,0 +1,51 @@ +# Copyright (c) 2021-2024 Morwenn +# SPDX-License-Identifier: MIT + +name: MinGW-w64 Builds + +on: + push: + paths: + - '.github/workflows/build-mingw.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + pull_request: + paths: + - '.github/workflows/build-mingw.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + +jobs: + build: + runs-on: windows-2022 + + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + + - name: Configure CMake + shell: pwsh + working-directory: ${{runner.workspace}} + run: | + cmake -H${{github.event.repository.name}} -Bbuild ` + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} ` + -G"MinGW Makefiles" ` + -DBUILD_BENCHMARKS=ON + + - name: Build the test suite + working-directory: ${{runner.workspace}}/build + run: cmake --build . --config ${{matrix.build_type}} -j 2 + + - name: Run the test suite + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}}/build + run: ctest -C ${{matrix.build_type}} diff --git a/.github/workflows/build-msvc.yml b/.github/workflows/build-msvc.yml new file mode 100644 index 0000000..7d519b6 --- /dev/null +++ b/.github/workflows/build-msvc.yml @@ -0,0 +1,52 @@ +# Copyright (c) 2021-2024 Morwenn +# SPDX-License-Identifier: MIT + +name: MSVC Builds + +on: + push: + paths: + - '.github/workflows/build-msvc.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + pull_request: + paths: + - '.github/workflows/build-msvc.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + +jobs: + build: + runs-on: windows-2022 + + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v4 + + - name: Configure CMake + shell: pwsh + working-directory: ${{runner.workspace}} + run: | + cmake -H${{github.event.repository.name}} -Bbuild ` + -DCMAKE_CONFIGURATION_TYPES=${{matrix.build_type}} ` + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} ` + -G"Visual Studio 17 2022" -A x64 ` + -DBUILD_BENCHMARKS=ON + + - name: Build the test suite + working-directory: ${{runner.workspace}}/build + run: cmake --build . --config ${{matrix.build_type}} -j 2 + + - name: Run the test suite + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}}/build + run: ctest -C ${{matrix.build_type}} diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml new file mode 100644 index 0000000..005099b --- /dev/null +++ b/.github/workflows/build-ubuntu.yml @@ -0,0 +1,82 @@ +# Copyright (c) 2021-2024 Morwenn +# SPDX-License-Identifier: MIT + +name: Ubuntu Builds + +on: + push: + paths: + - '.github/workflows/build-ubuntu.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + pull_request: + paths: + - '.github/workflows/build-ubuntu.yml' + - 'CMakeLists.txt' + - 'cmake/**' + - 'include/gfx/timsort.hpp' + - 'tests/*' + +jobs: + build: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: false + matrix: + cxx: + - g++-10 + - clang++-11 + config: + # Release build + - build_type: Release + # Debug builds + - build_type: Debug + valgrind: ON + - build_type: Debug + sanitize: address + - build_type: Debug + sanitize: undefined + + steps: + - uses: actions/checkout@v4 + + - name: Install Valgrind + if: ${{matrix.config.valgrind == 'ON'}} + run: sudo apt update && sudo apt install -y valgrind + + - name: Configure CMake + working-directory: ${{runner.workspace}} + env: + CXX: ${{matrix.cxx}} + run: | + cmake -H${{github.event.repository.name}} -Bbuild \ + -DCMAKE_CONFIGURATION_TYPES=${{matrix.config.build_type}} \ + -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} \ + -DGFX_TIMSORT_SANITIZE=${{matrix.config.sanitize}} \ + -DGFX_TIMSORT_USE_VALGRIND=${{matrix.config.valgrind}} \ + -G"Unix Makefiles" \ + -DBUILD_BENCHMARKS=ON + + - name: Build the test suite + shell: bash + working-directory: ${{runner.workspace}}/build + run: cmake --build . --config ${{matrix.config.build_type}} -j 2 + + - name: Run the test suite + if: ${{matrix.config.valgrind != 'ON'}} + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}}/build + run: ctest -C ${{matrix.config.build_type}} + + - name: Run the test suite with Memcheck + if: ${{matrix.config.valgrind == 'ON'}} + env: + CTEST_OUTPUT_ON_FAILURE: 1 + working-directory: ${{runner.workspace}}/build + run: | + ctest -T memcheck -C ${{matrix.config.build_type}} -j 2 + find ./Testing/Temporary -name "MemoryChecker.*.log" -size +1300c | xargs cat; diff --git a/.gitignore b/.gitignore index 026851b..6ef5472 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .bin *.gc* coverage.txt +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2e74b8f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,81 @@ +cmake_minimum_required(VERSION 3.14.0) + +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +project(timsort VERSION 3.0.1 LANGUAGES CXX) + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +# Project options +option(BUILD_TESTING "Build the tests" ON) +option(BUILD_BENCHMARKS "Build the benchmarks" OFF) + +# Create gfx::timsort as an interface library +add_library(timsort INTERFACE) + +target_include_directories(timsort INTERFACE + $ + $ +) + +target_compile_features(timsort INTERFACE cxx_std_20) + +add_library(gfx::timsort ALIAS timsort) + +# Install targets and files +install( + TARGETS timsort + EXPORT gfx-timsort-targets + DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +install( + EXPORT gfx-timsort-targets + NAMESPACE gfx:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gfx +) + +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/gfx/timsort.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gfx +) + +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/gfx-timsort-config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake/gfx-timsort-config.cmake + INSTALL_DESTINATION + ${CMAKE_INSTALL_LIBDIR}/cmake/gfx +) + +write_basic_package_version_file( + ${CMAKE_BINARY_DIR}/cmake/gfx-timsort-config-version.cmake + COMPATIBILITY SameMajorVersion + ARCH_INDEPENDENT +) + +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/cmake/gfx-timsort-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cmake/gfx-timsort-config-version.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/gfx +) + +# Export target so that it can be used in subdirectories +export( + EXPORT gfx-timsort-targets + FILE ${CMAKE_CURRENT_BINARY_DIR}/cmake/gfx-timsort-targets.cmake + NAMESPACE gfx:: +) + +# Build tests and/or benchmarks if this is the main project +if (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + if (BUILD_TESTING) + enable_testing() + add_subdirectory(tests) + endif() + + if (BUILD_BENCHMARKS) + add_subdirectory(benchmarks) + endif() +endif() diff --git a/Makefile b/Makefile deleted file mode 100644 index 7e9ecad..0000000 --- a/Makefile +++ /dev/null @@ -1,39 +0,0 @@ - -COMPILE := $(CXX) -I. -Wall -Wextra -g $(CXXFLAGS) -OPTIMIZE := -DNDEBUG -O2 - -LIB_BOOST_TEST := -lboost_unit_test_framework-mt - -all: - @echo This library is a C++ header file only. - -.bin: - mkdir -p .bin - -test: test/test.cpp timsort.hpp .bin - $(COMPILE) $(LIB_BOOST_TEST) $< -o .bin/$@ - time ./.bin/$@ - -test-with-optimization: test/test.cpp timsort.hpp .bin - $(COMPILE) $(OPTIMIZE) $(LIB_BOOST_TEST) $< -o .bin/$@ - time ./.bin/$@ - -test-with-std-move: test/test.cpp timsort.hpp .bin - $(COMPILE) $(LIB_BOOST_TEST) -std=c++11 -DENABLE_STD_MOVE $< -o .bin/$@ - time ./.bin/$@ - -bench: example/bench.cpp timsort.hpp .bin - $(COMPILE) $(OPTIMIZE) $< -o .bin/$@ - $(CXX) -v - ./.bin/$@ - -coverage: - make test CXXFLAGS="-coverage -O0" - gcov test.gcda | grep -A 1 "File './timsort.hpp'" - mv timsort.hpp.gcov coverage.txt - rm -rf *.gc* - -clean: - rm -rf *~ .bin coverage.txt - -.PHONY: test bench coverage clean diff --git a/README.md b/README.md index b25dd03..5002c1c 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,235 @@ -TimSort -================== +[![Latest Release](https://img.shields.io/badge/release-3.0.1-blue.svg)](https://github.com/timsort/cpp-TimSort/releases/tag/v3.0.1) +[![Conan Package](https://img.shields.io/badge/conan-cpp--TimSort%2F3.0.1-blue.svg)](https://conan.io/center/recipes/timsort?version=3.0.1) +[![Pitchfork Layout](https://img.shields.io/badge/standard-PFL-orange.svg)](https://github.com/vector-of-bool/pitchfork) -A C++ implementation of TimSort, O(n log n) in worst case and stable sort algorithm, ported from Python's and OpenJDK's. +## TimSort -This is a bit slower than `std::sort()` on randomized sequences, and much -faster on partially-sorted sequences. +A C++ implementation of TimSort, an O(n log n) stable sorting algorithm, ported from Python's and OpenJDK's. -SYNOPSIS -================== +See also the following links for a detailed description of TimSort: +* http://svn.python.org/projects/python/trunk/Objects/listsort.txt +* http://en.wikipedia.org/wiki/Timsort - #include "timsort.hpp" +This version of the library requires at least C++20. Older versions of the library, available in different branches, +offer support for older standards though implement fewer features: +* Branch `2.x.y` is compatible with C++11, and is slightly more permissive in some reagard due to the lack of + concepts to constrain its interface (it notaby supports iterators without postfix operator++/--). +* Branch `1.x.y` is compatible with C++03. +Older versions are not actively maintained anymore. If you need extended support for those, please open specific +issues for the problems you want solved. - std::vector a; +According to the benchmarks, `gfx::timsort` is slower than [`std::ranges::sort`][std-sort] on randomized sequences, +but faster on partially-sorted ones. It can be used as a drop-in replacement for [`std::ranges::stable_sort`][std-stable-sort], +with the difference that it can't fall back to a O(n log² n) algorithm when there isn't enough extra heap memory +available. - // initialize a +Merging sorted ranges efficiently is an important part of the TimSort algorithm. This library exposes `gfx::timmerge` +in the public API, a drop-in replacement for [`std::ranges::inplace_merge`][std-inplace-merge] with the difference +that it can't fall back to a O(n log n) algorithm when there isn't enough extra heap memory available. According to +the benchmarks, `gfx::timmerge` is slower than `std::ranges::inplace_merge` on heavily/randomly overlapping subranges +of simple elements, but it is faster for complex elements such as `std::string`, and on sparsely overlapping subranges. - gfx::timsort(a.begin(), a.end(), std::less()); +The ibrary exposes the following functions in namespace `gfx`: -TEST -================== +```cpp +// timsort -Run `make test` for testing and `make coverage` for test coverage. +template < + std::random_access_iterator Iterator, + std::sentinel_for Sentinel, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable +auto timsort(Iterator first, Sentinel last, + Compare compare={}, Projection projection={}) + -> Iterator; -SEE ALSO -================== +template < + std::ranges::random_access_range Range, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable, Compare, Projection> +auto timsort(Range &range, Compare compare={}, Projection projection={}) + -> std::ranges::borrowed_iterator_t; -* http://svn.python.org/projects/python/trunk/Objects/listsort.txt -* http://en.wikipedia.org/wiki/Timsort +// timmerge + +template < + std::random_access_iterator Iterator, + std::sentinel_for Sentinel, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable +auto timmerge(Iterator first, Iterator middle, Sentinel last, + Compare compare={}, Projection projection={}) + -> Iterator; + +template < + std::ranges::random_access_range Range, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable, Compare, Projection> +auto timmerge(Range &&range, std::ranges::iterator_t middle, + Compare compare={}, Projection projection={}) + -> std::ranges::borrowed_iterator_t; +``` + +## EXAMPLE + +Example of using timsort with a defaulted comparison function and a projection function to sort a vector of strings +by length: + +```cpp +#include +#include +#include + +size_t len(const std::string& str) { + return str.size(); +} + +// Sort a vector of strings by length +std::vector collection = { /* ... */ }; +gfx::timsort(collection, {}, &len); +``` + +## INSTALLATION & COMPATIBILITY + +[![Ubuntu Builds](https://github.com/timsort/cpp-TimSort/actions/workflows/build-ubuntu.yml/badge.svg?branch=3.x.y)](https://github.com/timsort/cpp-TimSort/actions/workflows/build-ubuntu.yml) +[![MSVC Builds](https://github.com/timsort/cpp-TimSort/actions/workflows/build-msvc.yml/badge.svg?branch=3.x.y)](https://github.com/timsort/cpp-TimSort/actions/workflows/build-msvc.yml) +[![MinGW-w64 Builds](https://github.com/timsort/cpp-TimSort/actions/workflows/build-mingw.yml/badge.svg?branch=3.x.y)](https://github.com/timsort/cpp-TimSort/actions/workflows/build-mingw.yml) +[![MacOS Builds](https://github.com/timsort/cpp-TimSort/actions/workflows/build-macos.yml/badge.svg?branch=3.x.y)](https://github.com/timsort/cpp-TimSort/actions/workflows/build-macos.yml) + +The library is tested with the following compilers: +* Ubuntu: GCC 10, Clang 11 +* Windows: MSVC 19.37.32826.1, MinGW-w64 GCC 12 +* MacOS: GCC 10, Clang 17 + +The library can be installed on the system via [CMake][cmake] (at least 3.14) with the following commands: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --install build +``` + +Alternatively the library is also available [Conan Center][conan-center] and can be directly installed in your local +[Conan][conan] cache with the following command: + +```sh +conan install --requires=timsort/3.0.1 +``` -BENCHMARK -================== +## DIAGNOSTICS & INFORMATION -bench.cpp, invoked by `make bench`, is a simple benchmark. -An example output is as follows (timing scale: sec.): +The following configuration macros allow `gfx::timsort` and `gfx::timmerge` to emit diagnostics, which can be helpful +to diagnose issues: +* Defining `GFX_TIMSORT_ENABLE_ASSERT` light inserts assertions in key locations in the algorithm to avoid logic errors. +* Defining `GFX_TIMSORT_ENABLE_AUDIT` inserts assertions that verify pre- and postconditions. These verifications can + slow the algorithm down significantly. Enable the audits only while testing or debugging. Enabling audits automatically + enables lighter assertions too. +* Defining `GFX_TIMSORT_ENABLE_LOG` inserts logs in key locations, which allow to follow more closely the flow of the + algorithm. + +**cpp-TimSort** follows semantic versioning and provides the following macros to retrieve the current major, minor +and patch versions: + +```cpp +GFX_TIMSORT_VERSION_MAJOR +GFX_TIMSORT_VERSION_MINOR +GFX_TIMSORT_VERSION_PATCH +``` + +## TESTS + +The tests are written with Catch2 and can be compiled with CMake and run through CTest. + +When using the project's main `CMakeLists.txt`, the CMake option `BUILD_TESTING` is `ON` by default unless the +project is included as a subdirectory. The following CMake options are available to change the way the tests are +built with CMake: +* `GFX_TIMSORT_USE_VALGRIND`: if `ON`, the tests will be run through Valgrind (`OFF` by default) +* `GFX_TIMSORT_SANITIZE`: this variable takes a comma-separated list of sanitizers options to run the tests (empty by default) + +## BENCHMARKS + +Benchmarks are available in the `benchmarks` subdirectory, and can be constructed directly by passing the option +`-DBUILD_BENCHMARKS=ON` to CMake during the configuration step. + +Example bench_sort output (timing scale: sec.): - c++ -I. -Wall -Wextra -g -DNDEBUG -O2 example/bench.cpp -o .bin/bench c++ -v - Apple clang version 4.1 (tags/Apple/clang-421.11.66) (based on LLVM 3.1svn) - Target: x86_64-apple-darwin12.2.0 + Apple LLVM version 7.0.0 (clang-700.0.72) + Target: x86_64-apple-darwin14.5.0 Thread model: posix + c++ -I. -Wall -Wextra -g -DNDEBUG -O2 -std=c++11 example/bench.cpp -o .bin/bench ./.bin/bench RANDOMIZED SEQUENCE [int] size 100000 - std::sort 0.667322 - std::stable_sort 0.895223 - timsort 1.274456 - [boost::rational] + std::sort 0.695253 + std::stable_sort 0.868916 + timsort 1.255825 + [std::string] size 100000 - std::sort 4.152952 - std::stable_sort 5.136133 - timsort 5.781330 + std::sort 3.438217 + std::stable_sort 4.122629 + timsort 5.791845 REVERSED SEQUENCE [int] size 100000 - std::sort 0.087842 - std::stable_sort 0.234953 - timsort 0.017438 - [boost::rational] + std::sort 0.045461 + std::stable_sort 0.575431 + timsort 0.019139 + [std::string] size 100000 - std::sort 2.114911 - std::stable_sort 2.247124 - timsort 0.281315 + std::sort 0.586707 + std::stable_sort 2.715778 + timsort 0.345099 SORTED SEQUENCE [int] size 100000 - std::sort 0.086000 - std::stable_sort 0.151913 - timsort 0.010536 - [boost::rational] + std::sort 0.021876 + std::stable_sort 0.087993 + timsort 0.008042 + [std::string] + size 100000 + std::sort 0.402458 + std::stable_sort 2.436326 + timsort 0.298639 + +Example bench_merge output (timing scale: milliseconds; omitted detailed results for different +middle iterator positions, reformatted to improve readability): + + c++ -v + Using built-in specs. + ... + Target: x86_64-pc-linux-gnu + ... + gcc version 10.2.0 (GCC) + c++ -I ../include -Wall -Wextra -g -DNDEBUG -O2 -std=c++11 bench_merge.cpp -o bench_merge + ./bench_merge size 100000 - std::sort 2.102378 - std::stable_sort 2.408591 - timsort 0.258270 + element type\algorithm: std::inplace_merge timmerge + RANDOMIZED SEQUENCE + [int] approx. average 33.404430 37.047990 + [std::string] approx. average 324.964249 210.297207 + REVERSED SEQUENCE + [int] approx. average 11.441404 4.017482 + [std::string] approx. average 305.649503 114.773898 + SORTED SEQUENCE + [int] approx. average 4.291098 0.105571 + [std::string] approx. average 158.238114 0.273858 + +Detailed bench_merge results for different middle iterator positions can be found at +https://github.com/timsort/cpp-TimSort/wiki/Benchmark-results + + + [cmake]: https://cmake.org/ + [conan]: https://conan.io/ + [conan-center]: https://conan.io/center + [std-inplace-merge]: https://en.cppreference.com/w/cpp/algorithm/ranges/inplace_merge + [std-sort]: https://en.cppreference.com/w/cpp/algorithm/ranges/sort + [std-stable-sort]: https://en.cppreference.com/w/cpp/algorithm/ranges/stable_sort diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..ec47dbf --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,6 @@ + +foreach(filename bench_merge.cpp bench_sort.cpp) + get_filename_component(name ${filename} NAME_WE) + add_executable(${name} ${filename}) + target_link_libraries(${name} PRIVATE gfx::timsort) +endforeach() diff --git a/benchmarks/bench_merge.cpp b/benchmarks/bench_merge.cpp new file mode 100644 index 0000000..797ab8a --- /dev/null +++ b/benchmarks/bench_merge.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2021 Igor Kushnir . + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "benchmarker.hpp" + +namespace +{ + std::vector generate_middle_positions(int size) { + std::vector result = { + 0, 1, 2, 5, 100, size/100, size/20, size/5, size/3, size/2, 3*size/4, + 6*size/7, 24*size/25, 90*size/91, size-85, size-8, size-2, size-1, size + }; + + // The code below can remove or reorder elements if size is small. + + auto logical_end = std::remove_if(result.begin(), result.end(), [size](int middle) { + return middle < 0 || middle > size; + }); + result.erase(logical_end, result.end()); + + std::sort(result.begin(), result.end()); + logical_end = std::unique(result.begin(), result.end()); + result.erase(logical_end, result.end()); + + return result; + } + + using Result = std::valarray; + Result zeroResult() { return Result(2); } +} + +template +struct Bench { + void operator()(const std::vector &source) const { + const int size = static_cast(source.size()); + const auto middle_positions = generate_middle_positions(size); + + int prev_middle = 0; + auto prev_result = zeroResult(); + auto result_sum = zeroResult(); + + std::cerr << "middle\\algorithm:\tstd::inplace_merge\ttimmerge" << std::endl; + constexpr int width = 10; + constexpr const char* padding = " \t"; + + std::vector a(source.size()); + for (auto middle : middle_positions) { + std::copy(source.begin(), source.end(), a.begin()); + std::sort(a.begin(), a.begin() + middle); + std::sort(a.begin() + middle, a.end()); + const auto result = run(a, middle); + + if (middle != prev_middle) { + // Trapezoidal rule for approximating the definite integral. + result_sum += 0.5 * (result + prev_result) + * static_cast(middle - prev_middle); + prev_middle = middle; + } + prev_result = result; + + std::cerr << std::setw(width) << middle + << " \t" << std::setw(width) << result[0] + << padding << std::setw(width) << result[1] + << std::endl; + } + + if (size != 0) { + result_sum /= static_cast(size); + std::cerr << "approx. average" + << " \t" << std::setw(width) << result_sum[0] + << padding << std::setw(width) << result_sum[1] + << std::endl; + } + } + +private: + static Result run(const std::vector &a, const int middle) { + std::vector b(a.size()); + const auto assert_is_sorted = [&b] { + if (!std::is_sorted(b.cbegin(), b.cend())) { + std::cerr << "Not sorted!" << std::endl; + std::abort(); + } + }; + + auto result = zeroResult(); + for (auto *total_time_ms : { &result[0], &result[1] }) { + using Clock = std::chrono::steady_clock; + decltype(Clock::now() - Clock::now()) total_time{0}; + + for (int i = 0; i < 100; ++i) { + std::copy(a.begin(), a.end(), b.begin()); + const auto time_begin = Clock::now(); + + if (total_time_ms == &result[0]) { + std::inplace_merge(b.begin(), b.begin() + middle, b.end()); + } else { + gfx::timmerge(b.begin(), b.begin() + middle, b.end()); + } + + const auto time_end = Clock::now(); + total_time += time_end - time_begin; + + // Verifying that b is sorted should prevent the compiler from optimizing anything out. + assert_is_sorted(); + } + + *total_time_ms = std::chrono::duration_cast< + std::chrono::microseconds>(total_time).count() / 1000.0; + } + return result; + } +}; + +int main(int argc, const char *argv[]) { + const int size = argc > 1 ? std::stoi(argv[1]) : 100 * 1000; + Benchmarker benchmarker(size); + benchmarker.run(); +} diff --git a/benchmarks/bench_sort.cpp b/benchmarks/bench_sort.cpp new file mode 100644 index 0000000..4f9539d --- /dev/null +++ b/benchmarks/bench_sort.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include "benchmarker.hpp" + +template +struct Bench { + void operator()(const std::vector &a) const { + { + std::vector b(a); + + std::clock_t start = std::clock(); + for (int i = 0; i < 100; ++i) { + std::copy(a.begin(), a.end(), b.begin()); + std::sort(b.begin(), b.end()); + } + std::clock_t stop = std::clock(); + + std::cerr << "std::sort " << (double(stop - start) / CLOCKS_PER_SEC) << std::endl; + } + + { + std::vector b(a); + + std::clock_t start = std::clock(); + for (int i = 0; i < 100; ++i) { + std::copy(a.begin(), a.end(), b.begin()); + std::stable_sort(b.begin(), b.end()); + } + std::clock_t stop = clock(); + + std::cerr << "std::stable_sort " << (double(stop - start) / CLOCKS_PER_SEC) << std::endl; + } + + { + std::vector b(a); + + std::clock_t start = std::clock(); + for (int i = 0; i < 100; ++i) { + std::copy(a.begin(), a.end(), b.begin()); + gfx::timsort(b.begin(), b.end()); + } + std::clock_t stop = std::clock(); + + std::cerr << "timsort " << (double(stop - start) / CLOCKS_PER_SEC) << std::endl; + } + } +}; + +int main(int argc, const char *argv[]) { + const int size = argc > 1 ? std::atoi(argv[1]) : 100 * 1000; + Benchmarker benchmarker(size); + benchmarker.run(); +} diff --git a/benchmarks/benchmarker.hpp b/benchmarks/benchmarker.hpp new file mode 100644 index 0000000..c93c7e6 --- /dev/null +++ b/benchmarks/benchmarker.hpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019 Morwenn. + * Copyright (c) 2021 Igor Kushnir . + * + * SPDX-License-Identifier: MIT + */ + +#ifndef GFX_TIMSORT_BENCHMARKER_HPP +#define GFX_TIMSORT_BENCHMARKER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace BenchmarkerHelpers { + template + struct convert_to + { + static T from(int value) { + return T(value); + } + }; + + template <> + struct convert_to + { + static std::string from(int value) { + std::ostringstream ss; + ss << value; + return ss.str(); + } + }; +} + +template class Bench> +class Benchmarker { +private: + enum state_t { sorted, randomized, reversed }; + + template + void bench(state_t const state) { + thread_local std::mt19937 random_engine(2581470); // fixed seed is enough + + std::vector a; + for (int i = 0; i < size_; ++i) { + a.push_back(BenchmarkerHelpers::convert_to::from((i + 1) * 10)); + } + + switch (state) { + case randomized: + std::shuffle(a.begin(), a.end(), random_engine); + break; + case reversed: + std::stable_sort(a.begin(), a.end()); + std::reverse(a.begin(), a.end()); + break; + case sorted: + std::stable_sort(a.begin(), a.end()); + break; + default: + std::abort(); // unreachable + } + + Bench()(a); + } + + void doit(state_t const state) { + std::cerr << "[int]" << std::endl; + bench(state); + + std::cerr << "[std::string]" << std::endl; + bench(state); + } + + const int size_; + +public: + explicit Benchmarker(int size) : size_(size) { + } + + void run() { + std::cerr << "size\t" << size_ << std::endl; + + std::cerr << std::setprecision(6) << std::setiosflags(std::ios::fixed); + + std::srand(0); + + std::cerr << "RANDOMIZED SEQUENCE" << std::endl; + doit(randomized); + + std::cerr << "REVERSED SEQUENCE" << std::endl; + doit(reversed); + + std::cerr << "SORTED SEQUENCE" << std::endl; + doit(sorted); + } +}; + +#endif // GFX_TIMSORT_BENCHMARKER_HPP diff --git a/cmake/gfx-timsort-config.cmake.in b/cmake/gfx-timsort-config.cmake.in new file mode 100644 index 0000000..c2c847f --- /dev/null +++ b/cmake/gfx-timsort-config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +if (NOT TARGET gfx::timsort) + include(${CMAKE_CURRENT_LIST_DIR}/gfx-timsort-targets.cmake) +endif() diff --git a/example/bench.cpp b/example/bench.cpp deleted file mode 100644 index 72cceea..0000000 --- a/example/bench.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "timsort.hpp" - -using namespace gfx; - -enum state_t { - sorted, randomized, reversed -}; - -template -static void bench(int const size, state_t const state) { - std::cerr << "size\t" << size << std::endl; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - switch(state) { - case randomized: - std::random_shuffle(a.begin(), a.end()); - break; - case reversed: - std::stable_sort(a.begin(), a.end()); - std::reverse(a.begin(), a.end()); - break; - case sorted: - std::stable_sort(a.begin(), a.end()); - break; - default: - assert(!"not reached"); - } - - { - std::vector b(a); - boost::timer t; - - for(int i = 0; i < 100; ++i) { - std::copy(a.begin(), a.end(), b.begin()); - std::sort(b.begin(), b.end()); - } - - std::cerr << "std::sort " << t.elapsed() << std::endl; - } - - { - std::vector b(a); - boost::timer t; - - for(int i = 0; i < 100; ++i) { - std::copy(a.begin(), a.end(), b.begin()); - std::stable_sort(b.begin(), b.end()); - } - - std::cerr << "std::stable_sort " << t.elapsed() << std::endl; - } - - { - std::vector b(a); - boost::timer t; - - for(int i = 0; i < 100; ++i) { - std::copy(a.begin(), a.end(), b.begin()); - timsort(b.begin(), b.end()); - } - - std::cerr << "timsort " << t.elapsed() << std::endl; - } -} - -static void doit(int const n, state_t const state) { - std::cerr << "[int]" << std::endl; - bench(n, state); - - std::cerr << "[boost::rational]" << std::endl; - bench< boost::rational >(n, state); -} - -int main(int argc, const char *argv[]) { - const int N = argc > 1 - ? boost::lexical_cast(argv[1]) - : 100 * 1000; - - std::cerr << std::setprecision(6) << std::setiosflags(std::ios::fixed); - - std::srand(0); - - std::cerr << "RANDOMIZED SEQUENCE" << std::endl; - doit(N, randomized); - - std::cerr << "REVERSED SEQUENCE" << std::endl; - doit(N, reversed); - - std::cerr << "SORTED SEQUENCE" << std::endl; - doit(N, sorted); -} diff --git a/include/gfx/timsort.hpp b/include/gfx/timsort.hpp new file mode 100644 index 0000000..f7803a3 --- /dev/null +++ b/include/gfx/timsort.hpp @@ -0,0 +1,770 @@ +/* + * C++ implementation of timsort + * + * ported from Python's and OpenJDK's: + * - http://svn.python.org/projects/python/trunk/Objects/listobject.c + * - http://cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java + * + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * Copyright (c) 2021 Igor Kushnir . + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef GFX_TIMSORT_HPP +#define GFX_TIMSORT_HPP + +#include +#include +#include +#include +#include +#include + +// Semantic versioning macros + +#define GFX_TIMSORT_VERSION_MAJOR 3 +#define GFX_TIMSORT_VERSION_MINOR 0 +#define GFX_TIMSORT_VERSION_PATCH 0 + +// Diagnostic selection macros + +#if defined(GFX_TIMSORT_ENABLE_ASSERT) || defined(GFX_TIMSORT_ENABLE_AUDIT) +# include +# define GFX_TIMSORT_ASSERT(expr) assert(expr) +#else +# define GFX_TIMSORT_ASSERT(expr) ((void)0) +#endif + +#ifdef GFX_TIMSORT_ENABLE_AUDIT +# define GFX_TIMSORT_AUDIT(expr) assert(expr) +#else +# define GFX_TIMSORT_AUDIT(expr) ((void)0) +#endif + +#ifdef GFX_TIMSORT_ENABLE_LOG +# include +# define GFX_TIMSORT_LOG(expr) (std::clog << "# " << __func__ << ": " << expr << std::endl) +#else +# define GFX_TIMSORT_LOG(expr) ((void)0) +#endif + + +namespace gfx { + +// --------------------------------------- +// Implementation details +// --------------------------------------- + +namespace detail { + +template +struct run { + using diff_t = typename std::iterator_traits::difference_type; + + Iterator base; + diff_t len; + + run(Iterator b, diff_t l) : base(b), len(l) { + } +}; + +template +class TimSort { + using iter_t = RandomAccessIterator; + using diff_t = std::iter_difference_t; + + static constexpr int MIN_MERGE = 32; + static constexpr int MIN_GALLOP = 7; + + int minGallop_ = MIN_GALLOP; + std::vector> tmp_; // temp storage for merges + std::vector> pending_; + + template + static void binarySort(iter_t const lo, iter_t const hi, iter_t start, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(lo <= start); + GFX_TIMSORT_ASSERT(start <= hi); + if (start == lo) { + ++start; + } + for (; start < hi; ++start) { + GFX_TIMSORT_ASSERT(lo <= start); + auto pos = std::ranges::upper_bound(lo, start, std::invoke(proj, *start), comp, proj); + rotateRight(pos, std::ranges::next(start)); + } + } + + template + static diff_t countRunAndMakeAscending(iter_t const lo, iter_t const hi, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(lo < hi); + + auto runHi = std::ranges::next(lo); + if (runHi == hi) { + return 1; + } + + if (std::invoke(comp, std::invoke(proj, *runHi), std::invoke(proj, *lo))) { // decreasing + do { + ++runHi; + } while (runHi < hi && std::invoke(comp, + std::invoke(proj, *runHi), + std::invoke(proj, *std::ranges::prev(runHi)))); + std::ranges::reverse(lo, runHi); + } else { // non-decreasing + do { + ++runHi; + } while (runHi < hi && !std::invoke(comp, + std::invoke(proj, *runHi), + std::invoke(proj, *std::ranges::prev(runHi)))); + } + + return runHi - lo; + } + + static diff_t minRunLength(diff_t n) { + GFX_TIMSORT_ASSERT(n >= 0); + + diff_t r = 0; + while (n >= 2 * MIN_MERGE) { + r |= (n & 1); + n >>= 1; + } + return n + r; + } + + void pushRun(iter_t const runBase, diff_t const runLen) { + pending_.emplace_back(runBase, runLen); + } + + template + void mergeCollapse(Compare comp, Projection proj) { + while (pending_.size() > 1) { + diff_t n = pending_.size() - 2; + + if ((n > 0 && pending_[n - 1].len <= pending_[n].len + pending_[n + 1].len) || + (n > 1 && pending_[n - 2].len <= pending_[n - 1].len + pending_[n].len)) { + if (pending_[n - 1].len < pending_[n + 1].len) { + --n; + } + mergeAt(n, comp, proj); + } else if (pending_[n].len <= pending_[n + 1].len) { + mergeAt(n, comp, proj); + } else { + break; + } + } + } + + template + void mergeForceCollapse(Compare comp, Projection proj) { + while (pending_.size() > 1) { + diff_t n = pending_.size() - 2; + + if (n > 0 && pending_[n - 1].len < pending_[n + 1].len) { + --n; + } + mergeAt(n, comp, proj); + } + } + + template + void mergeAt(diff_t const i, Compare comp, Projection proj) { + diff_t const stackSize = pending_.size(); + GFX_TIMSORT_ASSERT(stackSize >= 2); + GFX_TIMSORT_ASSERT(i >= 0); + GFX_TIMSORT_ASSERT(i == stackSize - 2 || i == stackSize - 3); + + auto base1 = pending_[i].base; + auto len1 = pending_[i].len; + auto base2 = pending_[i + 1].base; + auto len2 = pending_[i + 1].len; + + pending_[i].len = len1 + len2; + + if (i == stackSize - 3) { + pending_[i + 1] = pending_[i + 2]; + } + + pending_.pop_back(); + + mergeConsecutiveRuns(base1, len1, base2, len2, std::move(comp), std::move(proj)); + } + + template + void mergeConsecutiveRuns(iter_t base1, diff_t len1, iter_t base2, diff_t len2, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(len1 > 0); + GFX_TIMSORT_ASSERT(len2 > 0); + GFX_TIMSORT_ASSERT(base1 + len1 == base2); + + auto k = gallopRight(std::invoke(proj, *base2), base1, len1, 0, comp, proj); + GFX_TIMSORT_ASSERT(k >= 0); + + base1 += k; + len1 -= k; + + if (len1 == 0) { + return; + } + + len2 = gallopLeft(std::invoke(proj, base1[len1 - 1]), base2, len2, len2 - 1, comp, proj); + GFX_TIMSORT_ASSERT(len2 >= 0); + if (len2 == 0) { + return; + } + + if (len1 <= len2) { + mergeLo(base1, len1, base2, len2, comp, proj); + } else { + mergeHi(base1, len1, base2, len2, comp, proj); + } + } + + template + static diff_t gallopLeft(T const& key, Iter const base, diff_t const len, diff_t const hint, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(len > 0); + GFX_TIMSORT_ASSERT(hint >= 0); + GFX_TIMSORT_ASSERT(hint < len); + + diff_t lastOfs = 0; + diff_t ofs = 1; + + if (std::invoke(comp, std::invoke(proj, base[hint]), key)) { + auto maxOfs = len - hint; + while (ofs < maxOfs && std::invoke(comp, std::invoke(proj, base[hint + ofs]), key)) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + + if (ofs <= 0) { // int overflow + ofs = maxOfs; + } + } + if (ofs > maxOfs) { + ofs = maxOfs; + } + + lastOfs += hint; + ofs += hint; + } else { + diff_t const maxOfs = hint + 1; + while (ofs < maxOfs && !std::invoke(comp, std::invoke(proj, base[hint - ofs]), key)) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + + if (ofs <= 0) { + ofs = maxOfs; + } + } + if (ofs > maxOfs) { + ofs = maxOfs; + } + + diff_t const tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } + GFX_TIMSORT_ASSERT(-1 <= lastOfs); + GFX_TIMSORT_ASSERT(lastOfs < ofs); + GFX_TIMSORT_ASSERT(ofs <= len); + + return std::ranges::lower_bound(base + (lastOfs + 1), base + ofs, key, comp, proj) - base; + } + + template + static diff_t gallopRight(T const& key, Iter const base, diff_t const len, diff_t const hint, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(len > 0); + GFX_TIMSORT_ASSERT(hint >= 0); + GFX_TIMSORT_ASSERT(hint < len); + + diff_t ofs = 1; + diff_t lastOfs = 0; + + if (std::invoke(comp, key, std::invoke(proj, base[hint]))) { + diff_t const maxOfs = hint + 1; + while (ofs < maxOfs && std::invoke(comp, key, std::invoke(proj, base[hint - ofs]))) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + + if (ofs <= 0) { + ofs = maxOfs; + } + } + if (ofs > maxOfs) { + ofs = maxOfs; + } + + diff_t const tmp = lastOfs; + lastOfs = hint - ofs; + ofs = hint - tmp; + } else { + diff_t const maxOfs = len - hint; + while (ofs < maxOfs && !std::invoke(comp, key, std::invoke(proj, base[hint + ofs]))) { + lastOfs = ofs; + ofs = (ofs << 1) + 1; + + if (ofs <= 0) { // int overflow + ofs = maxOfs; + } + } + if (ofs > maxOfs) { + ofs = maxOfs; + } + + lastOfs += hint; + ofs += hint; + } + GFX_TIMSORT_ASSERT(-1 <= lastOfs); + GFX_TIMSORT_ASSERT(lastOfs < ofs); + GFX_TIMSORT_ASSERT(ofs <= len); + + return std::ranges::upper_bound(base + (lastOfs + 1), base + ofs, key, comp, proj) - base; + } + + static void rotateLeft(iter_t first, iter_t last) { + std::iter_value_t tmp = std::ranges::iter_move(first); + auto [_, last_1] = std::ranges::move(std::ranges::next(first), last, first); + *last_1 = std::move(tmp); + } + + static void rotateRight(iter_t first, iter_t last) { + auto last_1 = std::ranges::prev(last); + std::iter_value_t tmp = std::ranges::iter_move(last_1); + std::ranges::move_backward(first, last_1, last); + *first = std::move(tmp); + } + + template + void mergeLo(iter_t const base1, diff_t len1, iter_t const base2, diff_t len2, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(len1 > 0); + GFX_TIMSORT_ASSERT(len2 > 0); + GFX_TIMSORT_ASSERT(base1 + len1 == base2); + + if (len1 == 1) { + return rotateLeft(base1, base2 + len2); + } + if (len2 == 1) { + return rotateRight(base1, base2 + len2); + } + + move_to_tmp(base1, len1); + + auto cursor1 = tmp_.begin(); + auto cursor2 = base2; + auto dest = base1; + + *dest = std::ranges::iter_move(cursor2); + ++cursor2; + ++dest; + --len2; + + int minGallop(minGallop_); + + // outer: + while (true) { + diff_t count1 = 0; + diff_t count2 = 0; + + do { + GFX_TIMSORT_ASSERT(len1 > 1); + GFX_TIMSORT_ASSERT(len2 > 0); + + if (std::invoke(comp, std::invoke(proj, *cursor2), std::invoke(proj, *cursor1))) { + *dest = std::ranges::iter_move(cursor2); + ++cursor2; + ++dest; + ++count2; + count1 = 0; + if (--len2 == 0) { + goto epilogue; + } + } else { + *dest = std::ranges::iter_move(cursor1); + ++cursor1; + ++dest; + ++count1; + count2 = 0; + if (--len1 == 1) { + goto epilogue; + } + } + } while ((count1 | count2) < minGallop); + + do { + GFX_TIMSORT_ASSERT(len1 > 1); + GFX_TIMSORT_ASSERT(len2 > 0); + + count1 = gallopRight(std::invoke(proj, *cursor2), cursor1, len1, 0, comp, proj); + if (count1 != 0) { + std::ranges::move_backward(cursor1, cursor1 + count1, dest + count1); + dest += count1; + cursor1 += count1; + len1 -= count1; + + if (len1 <= 1) { + goto epilogue; + } + } + *dest = std::ranges::iter_move(cursor2); + ++cursor2; + ++dest; + if (--len2 == 0) { + goto epilogue; + } + + count2 = gallopLeft(std::invoke(proj, *cursor1), cursor2, len2, 0, comp, proj); + if (count2 != 0) { + std::ranges::move(cursor2, cursor2 + count2, dest); + dest += count2; + cursor2 += count2; + len2 -= count2; + if (len2 == 0) { + goto epilogue; + } + } + *dest = std::ranges::iter_move(cursor1); + ++cursor1; + ++dest; + if (--len1 == 1) { + goto epilogue; + } + + --minGallop; + } while ((count1 >= MIN_GALLOP) | (count2 >= MIN_GALLOP)); + + if (minGallop < 0) { + minGallop = 0; + } + minGallop += 2; + } // end of "outer" loop + + epilogue: // merge what is left from either cursor1 or cursor2 + + minGallop_ = (std::min)(minGallop, 1); + + if (len1 == 1) { + GFX_TIMSORT_ASSERT(len2 > 0); + std::ranges::move(cursor2, cursor2 + len2, dest); + *(dest + len2) = std::ranges::iter_move(cursor1); + } else { + GFX_TIMSORT_ASSERT(len1 != 0 && "Comparison function violates its general contract"); + GFX_TIMSORT_ASSERT(len2 == 0); + GFX_TIMSORT_ASSERT(len1 > 1); + std::ranges::move(cursor1, cursor1 + len1, dest); + } + } + + template + void mergeHi(iter_t const base1, diff_t len1, iter_t const base2, diff_t len2, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(len1 > 0); + GFX_TIMSORT_ASSERT(len2 > 0); + GFX_TIMSORT_ASSERT(base1 + len1 == base2); + + if (len1 == 1) { + return rotateLeft(base1, base2 + len2); + } + if (len2 == 1) { + return rotateRight(base1, base2 + len2); + } + + move_to_tmp(base2, len2); + + auto cursor1 = base1 + len1; + auto cursor2 = tmp_.begin() + (len2 - 1); + auto dest = base2 + (len2 - 1); + + *dest = std::ranges::iter_move(--cursor1); + --dest; + --len1; + + int minGallop(minGallop_); + + // outer: + while (true) { + diff_t count1 = 0; + diff_t count2 = 0; + + // The next loop is a hot path of the algorithm, so we decrement + // eagerly the cursor so that it always points directly to the value + // to compare, but we have to implement some trickier logic to make + // sure that it points to the next value again by the end of said loop + --cursor1; + + do { + GFX_TIMSORT_ASSERT(len1 > 0); + GFX_TIMSORT_ASSERT(len2 > 1); + + if (std::invoke(comp, std::invoke(proj, *cursor2), std::invoke(proj, *cursor1))) { + *dest = std::ranges::iter_move(cursor1); + --dest; + ++count1; + count2 = 0; + if (--len1 == 0) { + goto epilogue; + } + --cursor1; + } else { + *dest = std::ranges::iter_move(cursor2); + --cursor2; + --dest; + ++count2; + count1 = 0; + if (--len2 == 1) { + ++cursor1; // See comment before the loop + goto epilogue; + } + } + } while ((count1 | count2) < minGallop); + ++cursor1; // See comment before the loop + + do { + GFX_TIMSORT_ASSERT(len1 > 0); + GFX_TIMSORT_ASSERT(len2 > 1); + + count1 = len1 - gallopRight(std::invoke(proj, *cursor2), + base1, len1, len1 - 1, comp, proj); + if (count1 != 0) { + dest -= count1; + cursor1 -= count1; + len1 -= count1; + std::ranges::move_backward(cursor1, cursor1 + count1, dest + (1 + count1)); + + if (len1 == 0) { + goto epilogue; + } + } + *dest = std::ranges::iter_move(cursor2); + --cursor2; + --dest; + if (--len2 == 1) { + goto epilogue; + } + + count2 = len2 - gallopLeft(std::invoke(proj, *std::ranges::prev(cursor1)), + tmp_.begin(), len2, len2 - 1, comp, proj); + if (count2 != 0) { + dest -= count2; + cursor2 -= count2; + len2 -= count2; + std::ranges::move(std::ranges::next(cursor2), + cursor2 + (1 + count2), + std::ranges::next(dest)); + if (len2 <= 1) { + goto epilogue; + } + } + *dest = std::ranges::iter_move(--cursor1); + --dest; + if (--len1 == 0) { + goto epilogue; + } + + --minGallop; + } while ((count1 >= MIN_GALLOP) | (count2 >= MIN_GALLOP)); + + if (minGallop < 0) { + minGallop = 0; + } + minGallop += 2; + } // end of "outer" loop + + epilogue: // merge what is left from either cursor1 or cursor2 + + minGallop_ = (std::min)(minGallop, 1); + + if (len2 == 1) { + GFX_TIMSORT_ASSERT(len1 > 0); + dest -= len1; + std::ranges::move_backward(cursor1 - len1, cursor1, dest + (1 + len1)); + *dest = std::ranges::iter_move(cursor2); + } else { + GFX_TIMSORT_ASSERT(len2 != 0 && "Comparison function violates its general contract"); + GFX_TIMSORT_ASSERT(len1 == 0); + GFX_TIMSORT_ASSERT(len2 > 1); + std::ranges::move(tmp_.begin(), tmp_.begin() + len2, dest - (len2 - 1)); + } + } + + void move_to_tmp(iter_t const begin, diff_t len) { + tmp_.assign(std::make_move_iterator(begin), + std::make_move_iterator(begin + len)); + } + +public: + + template + static void merge(iter_t const lo, iter_t const mid, iter_t const hi, + Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(lo <= mid); + GFX_TIMSORT_ASSERT(mid <= hi); + + if (lo == mid || mid == hi) { + return; // nothing to do + } + + TimSort ts; + ts.mergeConsecutiveRuns(lo, mid - lo, mid, hi - mid, std::move(comp), std::move(proj)); + + GFX_TIMSORT_LOG("1st size: " << (mid - lo) << "; 2nd size: " << (hi - mid) + << "; tmp_.size(): " << ts.tmp_.size()); + } + + template + static void sort(iter_t const lo, iter_t const hi, Compare comp, Projection proj) { + GFX_TIMSORT_ASSERT(lo <= hi); + + auto nRemaining = hi - lo; + if (nRemaining < 2) { + return; // nothing to do + } + + if (nRemaining < MIN_MERGE) { + auto initRunLen = countRunAndMakeAscending(lo, hi, comp, proj); + GFX_TIMSORT_LOG("initRunLen: " << initRunLen); + binarySort(lo, hi, lo + initRunLen, comp, proj); + return; + } + + TimSort ts; + auto minRun = minRunLength(nRemaining); + auto cur = lo; + do { + auto runLen = countRunAndMakeAscending(cur, hi, comp, proj); + + if (runLen < minRun) { + auto force = (std::min)(nRemaining, minRun); + binarySort(cur, cur + force, cur + runLen, comp, proj); + runLen = force; + } + + ts.pushRun(cur, runLen); + ts.mergeCollapse(comp, proj); + + cur += runLen; + nRemaining -= runLen; + } while (nRemaining != 0); + + GFX_TIMSORT_ASSERT(cur == hi); + ts.mergeForceCollapse(comp, proj); + GFX_TIMSORT_ASSERT(ts.pending_.size() == 1); + + GFX_TIMSORT_LOG("size: " << (hi - lo) << " tmp_.size(): " << ts.tmp_.size() + << " pending_.size(): " << ts.pending_.size()); + } +}; + +} // namespace detail + + +// --------------------------------------- +// Public interface implementation +// --------------------------------------- + +/** + * Stably merges two consecutive sorted ranges [first, middle) and [middle, last) into one + * sorted range [first, last) with a comparison function and a projection function. + */ +template < + std::random_access_iterator Iterator, + std::sentinel_for Sentinel, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable +auto timmerge(Iterator first, Iterator middle, Sentinel last, + Compare comp={}, Projection proj={}) + -> Iterator +{ + auto last_it = std::ranges::next(first, last); + GFX_TIMSORT_AUDIT(std::ranges::is_sorted(first, middle, comp, proj) && "Precondition"); + GFX_TIMSORT_AUDIT(std::ranges::is_sorted(middle, last_it, comp, proj) && "Precondition"); + detail::TimSort::merge(first, middle, last_it, comp, proj); + GFX_TIMSORT_AUDIT(std::ranges::is_sorted(first, last_it, comp, proj) && "Postcondition"); + return last_it; +} + +/** + * Stably merges two sorted halves [first, middle) and [middle, last) of a range into one + * sorted range [first, last) with a comparison function and a projection function. + */ +template < + std::ranges::random_access_range Range, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable, Compare, Projection> +auto timmerge(Range &&range, std::ranges::iterator_t middle, + Compare comp={}, Projection proj={}) + -> std::ranges::borrowed_iterator_t +{ + return gfx::timmerge(std::begin(range), middle, std::end(range), comp, proj); +} + +/** + * Stably sorts a range with a comparison function and a projection function. + */ +template < + std::random_access_iterator Iterator, + std::sentinel_for Sentinel, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable +auto timsort(Iterator first, Sentinel last, + Compare comp={}, Projection proj={}) + -> Iterator +{ + auto last_it = std::ranges::next(first, last); + detail::TimSort::sort(first, last_it, comp, proj); + GFX_TIMSORT_AUDIT(std::ranges::is_sorted(first, last_it, comp, proj) && "Postcondition"); + return last_it; +} + +/** + * Stably sorts a range with a comparison function and a projection function. + */ +template < + std::ranges::random_access_range Range, + typename Compare = std::ranges::less, + typename Projection = std::identity +> + requires std::sortable, Compare, Projection> +auto timsort(Range &&range, Compare comp={}, Projection proj={}) + -> std::ranges::borrowed_iterator_t +{ + return gfx::timsort(std::begin(range), std::end(range), comp, proj); +} + +} // namespace gfx + +#undef GFX_TIMSORT_ENABLE_ASSERT +#undef GFX_TIMSORT_ASSERT +#undef GFX_TIMSORT_ENABLE_AUDIT +#undef GFX_TIMSORT_AUDIT +#undef GFX_TIMSORT_ENABLE_LOG +#undef GFX_TIMSORT_LOG + +#endif // GFX_TIMSORT_HPP diff --git a/test/test.cpp b/test/test.cpp deleted file mode 100644 index 4c6db3b..0000000 --- a/test/test.cpp +++ /dev/null @@ -1,541 +0,0 @@ -#include -#include -#include -#include - -#define BOOST_TEST_DYN_LINK -#define BOOST_TEST_MODULE TimSortTest -#include - -#include "timsort.hpp" - -using namespace gfx; - -BOOST_AUTO_TEST_CASE( simple0 ) { - std::vector a; - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a.size(), 0 ); -} - -BOOST_AUTO_TEST_CASE( simple1 ) { - std::vector a; - - a.push_back(42); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a.size(), 1 ); - BOOST_CHECK_EQUAL( a[0], 42 ); -} - -BOOST_AUTO_TEST_CASE( simple2 ) { - std::vector a; - - a.push_back(10); - a.push_back(20); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a.size(), 2 ); - BOOST_CHECK_EQUAL( a[0], 10); - BOOST_CHECK_EQUAL( a[1], 20 ); - - a.clear(); - a.push_back(20); - a.push_back(10); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a.size(), 2 ); - BOOST_CHECK_EQUAL( a[0], 10); - BOOST_CHECK_EQUAL( a[1], 20 ); - - a.clear(); - a.push_back(10); - a.push_back(10); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a.size(), 2 ); - BOOST_CHECK_EQUAL( a[0], 10); - BOOST_CHECK_EQUAL( a[1], 10 ); -} - -BOOST_AUTO_TEST_CASE( simple10 ) { - std::vector a; - a.push_back(60); - a.push_back(50); - a.push_back(10); - a.push_back(40); - a.push_back(80); - a.push_back(20); - a.push_back(30); - a.push_back(70); - a.push_back(10); - a.push_back(90); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a[0], 10 ); - BOOST_CHECK_EQUAL( a[1], 10 ); - BOOST_CHECK_EQUAL( a[2], 20 ); - BOOST_CHECK_EQUAL( a[3], 30 ); - BOOST_CHECK_EQUAL( a[4], 40 ); - BOOST_CHECK_EQUAL( a[5], 50 ); - BOOST_CHECK_EQUAL( a[6], 60 ); - BOOST_CHECK_EQUAL( a[7], 70 ); - BOOST_CHECK_EQUAL( a[8], 80 ); - BOOST_CHECK_EQUAL( a[9], 90 ); - - std::reverse(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL( a[0], 10 ); - BOOST_CHECK_EQUAL( a[1], 10 ); - BOOST_CHECK_EQUAL( a[2], 20 ); - BOOST_CHECK_EQUAL( a[3], 30 ); - BOOST_CHECK_EQUAL( a[4], 40 ); - BOOST_CHECK_EQUAL( a[5], 50 ); - BOOST_CHECK_EQUAL( a[6], 60 ); - BOOST_CHECK_EQUAL( a[7], 70 ); - BOOST_CHECK_EQUAL( a[8], 80 ); - BOOST_CHECK_EQUAL( a[9], 90 ); -} - - -BOOST_AUTO_TEST_CASE( shuffle30 ) { - const int size = 30; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL(a.size(), size); - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } -} - - -BOOST_AUTO_TEST_CASE( shuffle31 ) { - const int size = 31; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL(a.size(), size); - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } -} - -BOOST_AUTO_TEST_CASE( shuffle32 ) { - const int size = 32; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL(a.size(), size); - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } -} - -BOOST_AUTO_TEST_CASE( shuffle128 ) { - const int size = 128; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - BOOST_CHECK_EQUAL(a.size(), size); - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } -} - -BOOST_AUTO_TEST_CASE( shuffle1023 ) { - const int size = 1023; // odd number of elements - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( shuffle1024 ) { - const int size = 1024; // even number of elements - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( partial_shuffle1023 ) { - const int size = 1023; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - // sorted-shuffled-sorted pattern - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin() + (size/3 * 1), a.begin() + (size/3 * 2)); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } - - // shuffled-sorted-shuffled pattern - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin() , a.begin() + (size/3 * 1)); - std::random_shuffle(a.begin() + (size/3 * 2), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( partial_shuffle1024 ) { - const int size = 1024; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - // sorted-shuffled-sorted pattern - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin() + (size/3 * 1), a.begin() + (size/3 * 2)); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } - - // shuffled-sorted-shuffled pattern - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin() , a.begin() + (size/3 * 1)); - std::random_shuffle(a.begin() + (size/3 * 2), a.end()); - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( shuffle1024r ) { - const int size = 1024; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - for(int n = 0; n < 100; ++n) { - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end(), std::greater()); - - int j = size; - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (--j+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( partial_reversed1023 ) { - const int size = 1023; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - for(int n = 0; n < 100; ++n) { - std::reverse(a.begin(), a.begin() + (size / 2)); // partial reversed - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( partial_reversed1024 ) { - const int size = 1024; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - - for(int n = 0; n < 100; ++n) { - std::reverse(a.begin(), a.begin() + (size / 2)); // partial reversed - - timsort(a.begin(), a.end(), std::less()); - - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } - } -} - -BOOST_AUTO_TEST_CASE( c_array ) { - int a[] = { 7, 1, 5, 3, 9 }; - - timsort(a, a + sizeof(a) / sizeof(int), std::less()); - - BOOST_CHECK_EQUAL(a[0], 1); - BOOST_CHECK_EQUAL(a[1], 3); - BOOST_CHECK_EQUAL(a[2], 5); - BOOST_CHECK_EQUAL(a[3], 7); - BOOST_CHECK_EQUAL(a[4], 9); -} - -BOOST_AUTO_TEST_CASE( string_array ) { - std::string a[] = { "7", "1", "5", "3", "9" }; - - timsort(a, a + sizeof(a) / sizeof(std::string), std::less()); - - BOOST_CHECK_EQUAL(a[0], "1"); - BOOST_CHECK_EQUAL(a[1], "3"); - BOOST_CHECK_EQUAL(a[2], "5"); - BOOST_CHECK_EQUAL(a[3], "7"); - BOOST_CHECK_EQUAL(a[4], "9"); -} - -struct NonDefaultConstructible -{ - int i; - - NonDefaultConstructible( int i_ ) - : i( i_ ) {} - - friend bool operator<( NonDefaultConstructible const& x, NonDefaultConstructible const& y ) { - return x.i < y.i; - } - -}; - -BOOST_AUTO_TEST_CASE( non_default_constructible ) { - NonDefaultConstructible a[] = { 7, 1, 5, 3, 9 }; - - timsort(a, a + sizeof(a) / sizeof(a[0]), std::less()); - - BOOST_CHECK_EQUAL(a[0].i, 1); - BOOST_CHECK_EQUAL(a[1].i, 3); - BOOST_CHECK_EQUAL(a[2].i, 5); - BOOST_CHECK_EQUAL(a[3].i, 7); - BOOST_CHECK_EQUAL(a[4].i, 9); -} - -BOOST_AUTO_TEST_CASE( default_compare_function ) { - const int size = 128; - - std::vector a; - for(int i = 0; i < size; ++i) { - a.push_back((i+1) * 10); - } - std::random_shuffle(a.begin(), a.end()); - - timsort(a.begin(), a.end()); - - BOOST_CHECK_EQUAL(a.size(), size); - for(int i = 0; i < size; ++i) { - BOOST_CHECK_EQUAL( a[i], (i+1) * 10 ); - } -} - -enum id { foo, bar, baz }; -typedef std::pair pair_t; -bool less_in_first(pair_t x, pair_t y) { - return x.first < y.first; -} - -BOOST_AUTO_TEST_CASE( stability ) { - std::vector< pair_t > a; - - for(int i = 100; i >= 0; --i) { - a.push_back( std::make_pair(i, foo) ); - a.push_back( std::make_pair(i, bar) ); - a.push_back( std::make_pair(i, baz) ); - } - - timsort(a.begin(), a.end(), &less_in_first); - - BOOST_CHECK_EQUAL(a[0].first, 0); - BOOST_CHECK_EQUAL(a[0].second, foo); - BOOST_CHECK_EQUAL(a[1].first, 0); - BOOST_CHECK_EQUAL(a[1].second, bar); - BOOST_CHECK_EQUAL(a[2].first, 0); - BOOST_CHECK_EQUAL(a[2].second, baz); - - BOOST_CHECK_EQUAL(a[3].first, 1); - BOOST_CHECK_EQUAL(a[3].second, foo); - BOOST_CHECK_EQUAL(a[4].first, 1); - BOOST_CHECK_EQUAL(a[4].second, bar); - BOOST_CHECK_EQUAL(a[5].first, 1); - BOOST_CHECK_EQUAL(a[5].second, baz); - - BOOST_CHECK_EQUAL(a[6].first, 2); - BOOST_CHECK_EQUAL(a[6].second, foo); - BOOST_CHECK_EQUAL(a[7].first, 2); - BOOST_CHECK_EQUAL(a[7].second, bar); - BOOST_CHECK_EQUAL(a[8].first, 2); - BOOST_CHECK_EQUAL(a[8].second, baz); - - BOOST_CHECK_EQUAL(a[9].first, 3); - BOOST_CHECK_EQUAL(a[9].second, foo); - BOOST_CHECK_EQUAL(a[10].first, 3); - BOOST_CHECK_EQUAL(a[10].second, bar); - BOOST_CHECK_EQUAL(a[11].first, 3); - BOOST_CHECK_EQUAL(a[11].second, baz); -} - -bool less_in_pair(const std::pair& x, const std::pair& y) { - if (x.first == y.first) { - return x.second < y.second; - } - return x.first < y.first; -} - -BOOST_AUTO_TEST_CASE( issue2_compare ) { - typedef std::pair p; - gfx::Compare< p, bool(*)(const p&, const p&) > c(&less_in_pair); - - - BOOST_CHECK_EQUAL( c.lt(std::make_pair(10, 10), std::make_pair(10, 9)), false); - BOOST_CHECK_EQUAL( c.lt(std::make_pair(10, 10), std::make_pair(10, 10)), false); - BOOST_CHECK_EQUAL( c.lt(std::make_pair(10, 10), std::make_pair(10, 11)), true); - - BOOST_CHECK_EQUAL( c.le(std::make_pair(10, 10), std::make_pair(10, 9)), false); - BOOST_CHECK_EQUAL( c.le(std::make_pair(10, 10), std::make_pair(10, 10)), true); - BOOST_CHECK_EQUAL( c.le(std::make_pair(10, 10), std::make_pair(10, 11)), true); - - BOOST_CHECK_EQUAL( c.gt(std::make_pair(10, 10), std::make_pair(10, 9)), true); - BOOST_CHECK_EQUAL( c.gt(std::make_pair(10, 10), std::make_pair(10, 10)), false); - BOOST_CHECK_EQUAL( c.gt(std::make_pair(10, 10), std::make_pair(10, 11)), false); - - BOOST_CHECK_EQUAL( c.ge(std::make_pair(10, 10), std::make_pair(10, 9)), true); - BOOST_CHECK_EQUAL( c.ge(std::make_pair(10, 10), std::make_pair(10, 10)), true); - BOOST_CHECK_EQUAL( c.ge(std::make_pair(10, 10), std::make_pair(10, 11)), false); -} - -BOOST_AUTO_TEST_CASE( issue2_duplication ) { - std::vector< std::pair > a; - - for (int i = 0; i < 10000; ++i) { - int first = static_cast(rand()); - int second = static_cast(rand()); - - a.push_back( std::make_pair(first, second) ); - } - - std::vector< std::pair > expected(a); - - std::sort(expected.begin(), expected.end(), &less_in_pair); - timsort(a.begin(), a.end(), &less_in_pair); - - if (false) { - for (std::size_t i = 0; i < a.size(); ++i) { - std::clog << i << " "; - std::clog << "(" << a[i].first << ", " << a[i].second << ")"; - std::clog << " "; - std::clog << "(" << expected[i].first << ", " << expected[i].second << ") "; - std::clog << "\n"; - } - return; - } - - BOOST_CHECK_EQUAL(a.size(), expected.size()); - - // test some points - - BOOST_CHECK_EQUAL(a[0].first, expected[0].first); - BOOST_CHECK_EQUAL(a[0].second, expected[0].second); - - BOOST_CHECK_EQUAL(a[1].first, expected[1].first); - BOOST_CHECK_EQUAL(a[1].second, expected[1].second); - - BOOST_CHECK_EQUAL(a[10].first, expected[10].first); - BOOST_CHECK_EQUAL(a[10].second, expected[10].second); - - BOOST_CHECK_EQUAL(a[11].first, expected[11].first); - BOOST_CHECK_EQUAL(a[11].second, expected[11].second); - - BOOST_CHECK_EQUAL(a[100].first, expected[100].first); - BOOST_CHECK_EQUAL(a[100].second, expected[100].second); - - BOOST_CHECK_EQUAL(a[101].first, expected[101].first); - BOOST_CHECK_EQUAL(a[101].second, expected[101].second); - - BOOST_CHECK_EQUAL(a[111].first, expected[111].first); - BOOST_CHECK_EQUAL(a[111].second, expected[111].second); - - BOOST_CHECK_EQUAL(a[112].first, expected[112].first); - BOOST_CHECK_EQUAL(a[112].second, expected[112].second); - - BOOST_CHECK_EQUAL(a[999].first, expected[999].first); - BOOST_CHECK_EQUAL(a[999].second, expected[999].second); - - BOOST_CHECK_EQUAL(a[1000].first, expected[1000].first); - BOOST_CHECK_EQUAL(a[1000].second, expected[1000].second); - - BOOST_CHECK_EQUAL(a[1001].first, expected[1001].first); - BOOST_CHECK_EQUAL(a[1001].second, expected[1001].second); -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..bb9f237 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,153 @@ +# Copyright (c) 2019-2024 Morwenn +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.24.0) + +include(FetchContent) + +# Test suite options +option(GFX_TIMSORT_USE_VALGRIND "Whether to run the tests with Valgrind" OFF) +set(GFX_TIMSORT_SANITIZE "" CACHE STRING "Comma-separated list of options to pass to -fsanitize") + +# Find/download Catch2 +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2 + GIT_TAG fa43b77429ba76c462b1898d6cd2f2d7a9416b14 # v3.7.1 + SYSTEM + FIND_PACKAGE_ARGS 3.1.0 +) +FetchContent_MakeAvailable(Catch2) +list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +include(Catch) + +# Configure Valgrind +if (${GFX_TIMSORT_USE_VALGRIND}) + find_program(MEMORYCHECK_COMMAND valgrind) + set(MEMORYCHECK_COMMAND_OPTIONS "--leak-check=full --track-origins=yes --error-exitcode=1 --show-reachable=no") +endif() + +macro(configure_tests target) + # Add required dependencies to tests + target_link_libraries(${target} PRIVATE + Catch2::Catch2WithMain + gfx::timsort + ) + + target_compile_definitions(${target} PRIVATE + # Somewhat speed up Catch2 compile times + CATCH_CONFIG_FAST_COMPILE + # Fortify test suite for more thorough checks + _FORTIFY_SOURCE=3 + _GLIBCXX_ASSERTIONS + _LIBCPP_ENABLE_ASSERTIONS=1 + GFX_TIMSORT_ENABLE_ASSERT + ) + + # Add warnings + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") + target_compile_options(${target} PRIVATE + -Wall -Wextra -Wcast-align -Wmissing-declarations -Wmissing-include-dirs + -Wnon-virtual-dtor -Wodr -Wpedantic -Wredundant-decls -Wundef -Wunreachable-code + $<$:-Wlogical-op -Wuseless-cast> + ) + elseif (MSVC) + target_compile_options(${target} PRIVATE /W4) + endif() + + # Configure optimization options + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") + target_compile_options(${target} PRIVATE + $<$,$>:-O0> + $<$,$>:-Og> + ) + endif() + + # Use lld or the gold linker if possible + if (UNIX AND NOT APPLE) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " -fuse-ld=lld") + else() + set_property(TARGET ${target} APPEND_STRING PROPERTY LINK_FLAGS " -fuse-ld=gold") + endif() + endif() + + # Optionally enable sanitizers + if (UNIX AND GFX_TIMSORT_SANITIZE) + target_compile_options(${target} PRIVATE + -fsanitize=${GFX_TIMSORT_SANITIZE} + -fno-sanitize-recover=all + ) + set_property(TARGET ${target} + APPEND_STRING PROPERTY LINK_FLAGS + " -fsanitize=${GFX_TIMSORT_SANITIZE}" + ) + endif() +endmacro() + +# Tests that can run with C++98 +add_executable(cxx_98_tests + cxx_98_tests.cpp + verbose_abort.cpp +) +configure_tests(cxx_98_tests) +target_compile_features(cxx_98_tests PRIVATE cxx_std_98) + +# Tests requiring C++11 support +add_executable(cxx_11_tests + merge_cxx_11_tests.cpp + cxx_11_tests.cpp + verbose_abort.cpp +) +configure_tests(cxx_11_tests) +target_compile_features(cxx_11_tests PRIVATE cxx_std_11) + +# Tests requiring C++17 support +add_executable(cxx_17_tests + cxx_17_tests.cpp + verbose_abort.cpp +) +configure_tests(cxx_17_tests) +target_compile_features(cxx_17_tests PRIVATE cxx_std_17) + +# Tests requiring C++20 support +add_executable(cxx_20_tests + cxx_20_tests.cpp + verbose_abort.cpp +) +configure_tests(cxx_20_tests) +target_compile_features(cxx_20_tests PRIVATE cxx_std_20) + +# Tests requiring C++23 support +if ("cxx_std_23" IN_LIST CMAKE_CXX_COMPILE_FEATURES) + add_executable(cxx_23_tests + cxx_23_tests.cpp + verbose_abort.cpp + ) + configure_tests(cxx_23_tests) + target_compile_features(cxx_23_tests PRIVATE cxx_std_23) +endif() + +# Windows-specific tests +if (WIN32) + add_executable(windows_tests + windows.cpp + ) + configure_tests(windows_tests) + target_compile_features(windows_tests PRIVATE cxx_std_98) +endif() + +include(CTest) +include(Catch) + +string(RANDOM LENGTH 5 ALPHABET 123456789 RNG_SEED) +catch_discover_tests(cxx_98_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +catch_discover_tests(cxx_11_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +catch_discover_tests(cxx_17_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +catch_discover_tests(cxx_20_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +if ("cxx_std_23" IN_LIST CMAKE_CXX_COMPILE_FEATURES) + catch_discover_tests(cxx_23_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +endif() +if (WIN32) + catch_discover_tests(windows_tests EXTRA_ARGS --rng-seed ${RNG_SEED}) +endif() diff --git a/tests/cxx_11_tests.cpp b/tests/cxx_11_tests.cpp new file mode 100644 index 0000000..f84f73f --- /dev/null +++ b/tests/cxx_11_tests.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + +//////////////////////////////////////////////////////////// +// Move-only type for benchmarks +// +// std::sort and std::stable_sort are supposed to be able to +// sort collections of types that are move-only and that are +// not default-constructible. The class template move_only +// wraps such a type and can be fed to algorithms to check +// whether they still compile. +// +// Additionally, move_only detects attempts to read the value +// after a move has been performed and throws an exceptions +// when it happens. +// +// It also checks that no self-move is performed, since the +// standard algorithms can't rely on that to work either. +// + +namespace +{ + template struct move_only { + // Not default-constructible + move_only() = delete; + + // Move-only + move_only(const move_only &) = delete; + move_only& operator=(const move_only &) = delete; + + // Can be constructed from a T for convenience + move_only(const T &value) : can_read(true), value(value) { + } + + // Move operators + + move_only(move_only &&other) : can_read(true), value(std::move(other.value)) { + if (!exchange(other.can_read, false)) { + throw std::logic_error("illegal read from a moved-from value"); + } + } + + auto operator=(move_only &&other) -> move_only & { + // Self-move should be ok if the object is already in a moved-from + // state because it incurs no data loss, but should otherwise be + // frowned upon + if (&other == this && can_read) { + throw std::logic_error("illegal self-move was performed"); + } + + // Assign before overwriting other.can_read + can_read = other.can_read; + value = std::move(other.value); + + // If the two objects are not the same and we try to read from an + // object in a moved-from state, then it's a hard error because + // data might be lost + if (!exchange(other.can_read, false) && &other != this) { + throw std::logic_error("illegal read from a moved-from value"); + } + + return *this; + } + + // A C++11 backport of std::exchange() + template auto exchange(U &obj, U &&new_val) -> U { + U old_val = std::move(obj); + obj = std::forward(new_val); + return old_val; + } + + // Whether the value can be read + bool can_read = false; + // Actual value + T value; + }; +} + +template +bool operator<(const move_only &lhs, const move_only &rhs) +{ + return lhs.value < rhs.value; +} + +template +void swap(move_only &lhs, move_only &rhs) +{ + // This function matters because we want to prevent self-moves + // but we don't want to prevent self-swaps because it is the + // responsibility of class authors to make sure that self-swap + // does the right thing, and not the responsibility of algorithm + // authors to prevent them from happening + + // Both operands need to be readable + if (!(lhs.can_read || rhs.can_read)) { + throw std::logic_error("illegal read from a moved-from value"); + } + + // Swapping the values is enough to preserve the preconditions + using std::swap; + swap(lhs.value, rhs.value); +} + +TEST_CASE( "shuffle10k_for_move_only_types" ) { + const int size = 1024 * 10; // should be even number of elements + + std::vector > a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), [](const move_only &x, const move_only &y) { return x.value < y.value; }); + + for (int i = 0; i < size; ++i) { + CHECK(a[i].value == (i + 1) * 10); + } + } +} + +TEST_CASE( "merge_shuffle10k_for_move_only_types" ) { + const int size = 1024 * 10; // should be even number of elements + + std::vector > a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.end()); + + const auto compare = [](const move_only &x, const move_only &y) { return x.value < y.value; }; + const auto middle = a.begin() + rand() % size; + gfx::timsort(a.begin(), middle, compare); + gfx::timsort(middle, a.end(), compare); + gfx::timmerge(a.begin(), middle, a.end(), compare); + + for (int i = 0; i < size; ++i) { + CHECK(a[i].value == (i + 1) * 10); + } + } +} + +TEST_CASE( "issue14" ) { + const int a[] = {15, 7, 16, 20, 25, 28, 13, 27, 34, 24, 19, 1, 6, 30, 32, 29, 10, 9, + 3, 31, 21, 26, 8, 2, 22, 14, 4, 12, 5, 0, 23, 33, 11, 17, 18}; + std::deque c(std::begin(a), std::end(a)); + + SECTION( "timsort" ) { + gfx::timsort(std::begin(c), std::end(c)); + CHECK(std::is_sorted(std::begin(c), std::end(c))); + } + + SECTION( "timmerge" ) { + for (auto middle = c.begin(); ; ++middle) { + std::copy(std::begin(a), std::end(a), c.begin()); + + gfx::timsort(c.begin(), middle); + gfx::timsort(middle, c.end()); + gfx::timmerge(c.begin(), middle, c.end()); + CHECK(std::is_sorted(c.cbegin(), c.cend())); + + if (middle == c.end()) { + break; + } + } + } +} + +TEST_CASE( "range signatures" ) { + std::vector vec(50, 0); + std::iota(vec.begin(), vec.end(), -25); + test_helpers::shuffle(vec.begin(), vec.end()); + + SECTION( "range only" ) { + gfx::timsort(vec); + CHECK(std::is_sorted(vec.begin(), vec.end())); + } + + SECTION( "range with a comparison function" ) { + using value_type = std::vector::value_type; + gfx::timsort(vec, std::greater{}); + CHECK(std::is_sorted(vec.begin(), vec.end(), std::greater{})); + } + + SECTION( "range with comparison and projection functions" ) { + using value_type = std::vector::value_type; + gfx::timsort(vec, std::greater{}, std::negate{}); + CHECK(std::is_sorted(vec.begin(), vec.end())); + } +} diff --git a/tests/cxx_17_tests.cpp b/tests/cxx_17_tests.cpp new file mode 100644 index 0000000..679632f --- /dev/null +++ b/tests/cxx_17_tests.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct wrapper { + wrapper() = default; + wrapper(wrapper const&) = default; + wrapper(wrapper&&) = default; + + wrapper(int val) : value(val) { + } + + wrapper& operator=(wrapper const&) = default; + wrapper& operator=(wrapper&&) = default; + + wrapper& operator=(int val) { + value = val; + return *this; + } + + bool compare_to(wrapper const& other) const { + return value < other.value; + } + + int value = 0; + }; +} + +TEST_CASE( "generalized callables" ) { + std::vector vec(50); + std::iota(vec.begin(), vec.end(), -25); + std::mt19937 gen(123456); // fixed seed is enough + std::shuffle(vec.begin(), vec.end(), gen); + + const auto is_vec_sorted = [&vec] { + return std::is_sorted(vec.begin(), vec.end(), [](wrapper const& lhs, wrapper const& rhs) { + return lhs.value < rhs.value; + }); + }; + + SECTION( "timsort for comparisons" ) { + gfx::timsort(vec, &wrapper::compare_to); + CHECK(is_vec_sorted()); + } + + SECTION( "timsort for projections" ) { + gfx::timsort(vec, std::less<>{}, &wrapper::value); + CHECK(is_vec_sorted()); + } + + std::uniform_int_distribution random_middle(0, static_cast(vec.size())); + + SECTION( "timmerge for comparisons" ) { + const auto middle = vec.begin() + random_middle(gen); + gfx::timsort(vec.begin(), middle, &wrapper::compare_to); + gfx::timsort(middle, vec.end(), &wrapper::compare_to); + gfx::timmerge(vec.begin(), middle, vec.end(), &wrapper::compare_to); + CHECK(is_vec_sorted()); + } + + SECTION( "timmerge for projections" ) { + const auto middle = vec.begin() + random_middle(gen); + gfx::timsort(vec.begin(), middle, std::less<>{}, &wrapper::value); + gfx::timsort(middle, vec.end(), std::less<>{}, &wrapper::value); + gfx::timmerge(vec.begin(), middle, vec.end(), std::less<>{}, &wrapper::value); + CHECK(is_vec_sorted()); + } +} diff --git a/tests/cxx_20_tests.cpp b/tests/cxx_20_tests.cpp new file mode 100644 index 0000000..c7dea2a --- /dev/null +++ b/tests/cxx_20_tests.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Morwenn. + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + +TEST_CASE( "support for temporary types" ) { + SECTION( "timsort over std::span" ) { + std::vector vec(50); + std::iota(vec.begin(), vec.end(), -25); + test_helpers::shuffle(vec); + + auto last_it = gfx::timsort(std::span(vec)); + CHECK(std::ranges::is_sorted(vec)); + CHECK(last_it == std::span(vec).end()); + } + + SECTION( "timmerge over std::span" ) { + std::vector vec(100); + std::iota(vec.begin(), vec.end(), -25); + test_helpers::shuffle(vec); + + auto middle = vec.begin() + 38; + gfx::timsort(vec.begin(), middle); + gfx::timsort(middle, vec.end()); + + auto view = std::span(vec); + auto last_it = gfx::timmerge(std::span(vec), view.begin() + 38); + CHECK(std::ranges::is_sorted(vec)); + CHECK(last_it == view.end()); + } +} + +TEST_CASE( "dangling return type" ) { + SECTION( "timsort over temporary std::vector" ) { + std::vector vec(50, 8); + auto last_it = gfx::timsort(std::move(vec)); + STATIC_CHECK(std::is_same_v); + } + + SECTION( "timmerge over temporary std::vector" ) { + std::vector vec(30, 5); + auto last_it = gfx::timmerge(std::move(vec), vec.begin() + 14); + STATIC_CHECK(std::is_same_v); + } +} + +TEST_CASE( "support for sentinels" ) { + SECTION( "timsort with sentinel" ) { + std::vector vec(100); + std::iota(vec.begin(), vec.end(), -25); + test_helpers::shuffle(vec); + + auto last_it = gfx::timsort(std::counted_iterator(vec.begin(), 85), + std::default_sentinel); + CHECK(std::is_sorted(vec.begin(), vec.begin() + 85)); + CHECK(last_it == std::counted_iterator(vec.begin() + 85, 0)); + CHECK(last_it == std::default_sentinel); + } + + SECTION( "timmerge with sentinel" ) { + std::vector vec(100); + std::iota(vec.begin(), vec.end(), -25); + test_helpers::shuffle(vec); + + auto middle = vec.begin() + 38; + gfx::timsort(vec.begin(), middle); + gfx::timsort(middle, vec.end()); + + auto last_it = gfx::timmerge(std::counted_iterator(vec.begin(), 85), + std::counted_iterator(middle, 85 - 38), + std::default_sentinel); + CHECK(std::is_sorted(vec.begin(), vec.begin() + 85)); + CHECK(last_it == std::counted_iterator(vec.begin() + 85, 0)); + CHECK(last_it == std::default_sentinel); + } +} diff --git a/tests/cxx_23_tests.cpp b/tests/cxx_23_tests.cpp new file mode 100644 index 0000000..fe8fb3f --- /dev/null +++ b/tests/cxx_23_tests.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Morwenn. + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + + +#if defined(__cpp_lib_ranges_zip) + +TEST_CASE( "support for std::ranges::views::zip" ) +{ + SECTION( "zip two small collections" ) { + // issue #40 + std::vector vec = {4, 2, 3, 1}; + std::array arr = {'A', 'C', 'B', 'D'}; + auto zipped = std::views::zip(vec, arr); + + gfx::timsort( + zipped, {}, + [](std::tuple const& pair) { + return std::get<0>(pair); + } + ); + CHECK( std::ranges::is_sorted(vec) ); + CHECK( std::ranges::is_sorted(arr, std::ranges::greater{}) ); + } + + SECTION( "zip two big collections" ) { + std::vector vec(3000); + std::deque deq(3000); + std::iota(vec.begin(), vec.end(), -500); + std::ranges::reverse(vec); + std::iota(deq.begin(), deq.end(), -500); + + auto zipped = std::views::zip(vec, deq); + test_helpers::shuffle(zipped); + + gfx::timsort(zipped); + CHECK( std::ranges::is_sorted(vec) ); + CHECK( std::ranges::is_sorted(deq, std::ranges::greater{}) ); + } +} + +#endif // defined(__cpp_lib_ranges_zip) diff --git a/tests/cxx_98_tests.cpp b/tests/cxx_98_tests.cpp new file mode 100644 index 0000000..6699cac --- /dev/null +++ b/tests/cxx_98_tests.cpp @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + +using namespace test_helpers; + +TEST_CASE( "simple0" ) { + std::vector a; + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(0)); +} + +TEST_CASE( "simple1" ) { + std::vector a; + + a.push_back(42); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(1)); + CHECK(a[0] == 42); +} + +TEST_CASE( "simple2" ) { + std::vector a; + + a.push_back(10); + a.push_back(20); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 20); + + a.clear(); + a.push_back(20); + a.push_back(10); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 20); + + a.clear(); + a.push_back(10); + a.push_back(10); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 10); +} + +TEST_CASE( "simple10" ) { + std::vector a; + a.push_back(60); + a.push_back(50); + a.push_back(10); + a.push_back(40); + a.push_back(80); + a.push_back(20); + a.push_back(30); + a.push_back(70); + a.push_back(10); + a.push_back(90); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a[0] == 10); + CHECK(a[1] == 10); + CHECK(a[2] == 20); + CHECK(a[3] == 30); + CHECK(a[4] == 40); + CHECK(a[5] == 50); + CHECK(a[6] == 60); + CHECK(a[7] == 70); + CHECK(a[8] == 80); + CHECK(a[9] == 90); + + std::reverse(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a[0] == 10); + CHECK(a[1] == 10); + CHECK(a[2] == 20); + CHECK(a[3] == 30); + CHECK(a[4] == 40); + CHECK(a[5] == 50); + CHECK(a[6] == 60); + CHECK(a[7] == 70); + CHECK(a[8] == 80); + CHECK(a[9] == 90); +} + +TEST_CASE( "shuffle30" ) { + const int size = 30; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "shuffle31" ) { + const int size = 31; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "shuffle32" ) { + const int size = 32; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "shuffle128" ) { + const int size = 128; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "shuffle1023" ) { + const int size = 1023; // odd number of elements + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "shuffle1024" ) { + const int size = 1024; // should be even number of elements + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "partial_shuffle1023" ) { + const int size = 1023; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + // sorted-shuffled-sorted pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin() + (size / 3 * 1), a.begin() + (size / 3 * 2)); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + + // shuffled-sorted-shuffled pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.begin() + (size / 3 * 1)); + test_helpers::shuffle(a.begin() + (size / 3 * 2), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "partial_shuffle1024" ) { + const int size = 1024; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + // sorted-shuffled-sorted pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin() + (size / 3 * 1), a.begin() + (size / 3 * 2)); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + + // shuffled-sorted-shuffled pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.begin() + (size / 3 * 1)); + test_helpers::shuffle(a.begin() + (size / 3 * 2), a.end()); + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "shuffle1024r" ) { + const int size = 1024; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end(), std::greater()); + + int j = size; + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (--j + 1) * 10); + } + } +} + +TEST_CASE( "partial_reversed1023" ) { + const int size = 1023; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + std::reverse(a.begin(), a.begin() + (size / 2)); // partial reversed + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "partial_reversed1024" ) { + const int size = 1024; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + for (int n = 0; n < 100; ++n) { + std::reverse(a.begin(), a.begin() + (size / 2)); // partial reversed + + gfx::timsort(a.begin(), a.end(), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } +} + +TEST_CASE( "c_array" ) { + int a[] = {7, 1, 5, 3, 9}; + + gfx::timsort(a, a + sizeof(a) / sizeof(int), std::less()); + + CHECK(a[0] == 1); + CHECK(a[1] == 3); + CHECK(a[2] == 5); + CHECK(a[3] == 7); + CHECK(a[4] == 9); +} + +TEST_CASE( "string_array" ) { + std::string a[] = {"7", "1", "5", "3", "9"}; + + gfx::timsort(a, a + sizeof(a) / sizeof(std::string), std::less()); + + CHECK(a[0] == "1"); + CHECK(a[1] == "3"); + CHECK(a[2] == "5"); + CHECK(a[3] == "7"); + CHECK(a[4] == "9"); +} + +TEST_CASE( "non_default_constructible" ) { + NonDefaultConstructible a[] = {7, 1, 5, 3, 9}; + + gfx::timsort(a, a + sizeof(a) / sizeof(a[0]), std::less()); + + CHECK(a[0].i == 1); + CHECK(a[1].i == 3); + CHECK(a[2].i == 5); + CHECK(a[3].i == 7); + CHECK(a[4].i == 9); +} + +TEST_CASE( "default_compare_function" ) { + const int size = 128; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a.begin(), a.end()); + + gfx::timsort(a.begin(), a.end()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "stability" ) { + std::vector a; + + for (int i = 100; i >= 0; --i) { + a.push_back(std::make_pair(i, foo)); + a.push_back(std::make_pair(i, bar)); + a.push_back(std::make_pair(i, baz)); + } + + gfx::timsort(a.begin(), a.end(), &less_in_first); + + CHECK(a[0].first == 0); + CHECK(a[0].second == foo); + CHECK(a[1].first == 0); + CHECK(a[1].second == bar); + CHECK(a[2].first == 0); + CHECK(a[2].second == baz); + + CHECK(a[3].first == 1); + CHECK(a[3].second == foo); + CHECK(a[4].first == 1); + CHECK(a[4].second == bar); + CHECK(a[5].first == 1); + CHECK(a[5].second == baz); + + CHECK(a[6].first == 2); + CHECK(a[6].second == foo); + CHECK(a[7].first == 2); + CHECK(a[7].second == bar); + CHECK(a[8].first == 2); + CHECK(a[8].second == baz); + + CHECK(a[9].first == 3); + CHECK(a[9].second == foo); + CHECK(a[10].first == 3); + CHECK(a[10].second == bar); + CHECK(a[11].first == 3); + CHECK(a[11].second == baz); +} + +TEST_CASE( "issue2_duplication" ) { + std::vector > a; + + for (int i = 0; i < 10000; ++i) { + int first = static_cast(rand()); + int second = static_cast(rand()); + + a.push_back(std::make_pair(first, second)); + } + + std::vector > expected(a); + + std::sort(expected.begin(), expected.end()); + gfx::timsort(a.begin(), a.end()); + +#if 0 + for (std::size_t i = 0; i < a.size(); ++i) { + std::clog << i << " "; + std::clog << "(" << a[i].first << ", " << a[i].second << ")"; + std::clog << " "; + std::clog << "(" << expected[i].first << ", " << expected[i].second << ") "; + std::clog << "\n"; + } + return; +#endif + + CHECK(a.size() == expected.size()); + + // test some points + + CHECK(a[0].first == expected[0].first); + CHECK(a[0].second == expected[0].second); + + CHECK(a[1].first == expected[1].first); + CHECK(a[1].second == expected[1].second); + + CHECK(a[10].first == expected[10].first); + CHECK(a[10].second == expected[10].second); + + CHECK(a[11].first == expected[11].first); + CHECK(a[11].second == expected[11].second); + + CHECK(a[100].first == expected[100].first); + CHECK(a[100].second == expected[100].second); + + CHECK(a[101].first == expected[101].first); + CHECK(a[101].second == expected[101].second); + + CHECK(a[111].first == expected[111].first); + CHECK(a[111].second == expected[111].second); + + CHECK(a[112].first == expected[112].first); + CHECK(a[112].second == expected[112].second); + + CHECK(a[999].first == expected[999].first); + CHECK(a[999].second == expected[999].second); + + CHECK(a[1000].first == expected[1000].first); + CHECK(a[1000].second == expected[1000].second); + + CHECK(a[1001].first == expected[1001].first); + CHECK(a[1001].second == expected[1001].second); +} + +TEST_CASE( "projection" ) { + const int size = 128; + + std::vector vec; + for (int i = 0; i < size; ++i) { + vec.push_back(i - 40); + } + test_helpers::shuffle(vec.begin(), vec.end()); + + gfx::timsort(vec.begin(), vec.end(), std::greater(), std::negate()); + for (int i = 0; i < size; ++i) { + CHECK(vec[i] == i - 40); + } +} diff --git a/tests/merge_cxx_11_tests.cpp b/tests/merge_cxx_11_tests.cpp new file mode 100644 index 0000000..dab22d8 --- /dev/null +++ b/tests/merge_cxx_11_tests.cpp @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * Copyright (c) 2021 Igor Kushnir . + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + +using namespace test_helpers; + +namespace +{ + template + void sort_and_merge(RandomAccessRange &range, decltype(std::end(range)) middle, Compare compare) { + const auto first = std::begin(range); + const auto last = std::end(range); + gfx::timsort(first, middle, compare); + gfx::timsort(middle, last, compare); + gfx::timmerge(first, middle, last, compare); + } + + template + void sort_and_merge(RandomAccessRange &range, decltype(std::end(range)) middle) { + using value_type = typename std::iterator_traits::value_type; + sort_and_merge(range, middle, std::less()); + } + + inline std::mt19937& random_engine() + { + thread_local std::mt19937 res(Catch::rngSeed()); + return res; + } +} + +TEST_CASE( "merge_simple0" ) { + std::vector a; + + gfx::timmerge(a.begin(), a.end(), a.end()); + + CHECK(a.size() == std::size_t(0)); +} + +TEST_CASE( "merge_simple1" ) { + std::vector a; + + a.push_back(-54); + + gfx::timmerge(a.begin(), a.end(), a.end(), std::greater()); + + CHECK(a.size() == std::size_t(1)); + CHECK(a[0] == -54); +} + +TEST_CASE( "merge_simple2" ) { + std::vector a = { 10, 20 }; + + gfx::timmerge(a.begin(), a.begin() + 1, a.end()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 20); + + a = { 20, 10 }; + + gfx::timmerge(a.begin(), a.begin() + 1, a.end(), std::less()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 20); + + a = { 10, 10 }; + + gfx::timmerge(a.begin(), a.begin() + 1, a.end(), std::less()); + + CHECK(a.size() == std::size_t(2)); + CHECK(a[0] == 10); + CHECK(a[1] == 10); +} + +TEST_CASE( "merge_simple10" ) { + std::vector a = { 60, 50, 10, 40, 80, 20, 30, 70, 10, 90 }; + + sort_and_merge(a, a.begin() + 5, std::less()); + + CHECK(a[0] == 10); + CHECK(a[1] == 10); + CHECK(a[2] == 20); + CHECK(a[3] == 30); + CHECK(a[4] == 40); + CHECK(a[5] == 50); + CHECK(a[6] == 60); + CHECK(a[7] == 70); + CHECK(a[8] == 80); + CHECK(a[9] == 90); + + std::reverse(a.begin(), a.end()); + + sort_and_merge(a, a.begin() + 2, std::less()); + + CHECK(a[0] == 10); + CHECK(a[1] == 10); + CHECK(a[2] == 20); + CHECK(a[3] == 30); + CHECK(a[4] == 40); + CHECK(a[5] == 50); + CHECK(a[6] == 60); + CHECK(a[7] == 70); + CHECK(a[8] == 80); + CHECK(a[9] == 90); +} + +TEST_CASE( "merge_shuffle30" ) { + const int size = 30; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a); + + sort_and_merge(a, a.begin() + 24); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "merge_shuffle128" ) { + const int size = 128; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a); + + sort_and_merge(a, a.begin() + 51); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "merge_shuffle204x" ) { + for (int size : {2047, 2048}) { + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + std::uniform_int_distribution random_middle(0, size); + for (int n = 0; n < 30; ++n) { + test_helpers::shuffle(a); + + sort_and_merge(a, a.begin() + random_middle(random_engine())); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + } +} + +TEST_CASE( "merge_partial_shuffle102x" ) { + for (int size : {1023, 1024}) { + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + std::uniform_int_distribution random_middle(0, size); + + // sorted-shuffled-sorted pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin() + (size / 3 * 1), a.begin() + (size / 3 * 2)); + + sort_and_merge(a, a.begin() + random_middle(random_engine()), std::less()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + + // shuffled-sorted-shuffled pattern + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a.begin(), a.begin() + (size / 3 * 1)); + test_helpers::shuffle(a.begin() + (size / 3 * 2), a.end()); + + sort_and_merge(a, a.begin() + random_middle(random_engine())); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + } +} + +TEST_CASE( "merge_shuffle1025r" ) { + const int size = 1025; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + std::uniform_int_distribution random_middle(0, size); + for (int n = 0; n < 100; ++n) { + test_helpers::shuffle(a); + + sort_and_merge(a, a.begin() + random_middle(random_engine()), std::greater()); + + int j = size; + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (--j + 1) * 10); + } + } +} + +TEST_CASE( "merge_partial_reversed307x" ) { + for (int size : {3071, 3072}) { + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + + std::uniform_int_distribution random_middle(0, size); + for (int n = 0; n < 20; ++n) { + std::reverse(a.begin(), a.begin() + (size / 2)); // partial reversed + + sort_and_merge(a, a.begin() + random_middle(random_engine())); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } + } + } +} + +TEST_CASE( "merge_c_array" ) { + int a[] = {7, 1, 5, 3, 9}; + + sort_and_merge(a, a + 2); + + CHECK(a[0] == 1); + CHECK(a[1] == 3); + CHECK(a[2] == 5); + CHECK(a[3] == 7); + CHECK(a[4] == 9); +} + +TEST_CASE( "merge_string_array" ) { + std::string a[] = {"7", "1", "5", "3", "9"}; + + sort_and_merge(a, a + 3); + + CHECK(a[0] == "1"); + CHECK(a[1] == "3"); + CHECK(a[2] == "5"); + CHECK(a[3] == "7"); + CHECK(a[4] == "9"); +} + +TEST_CASE( "merge_non_default_constructible" ) { + NonDefaultConstructible a[] = {7, 1, 5, 3, 9}; + + sort_and_merge(a, a + 1); + + CHECK(a[0].i == 1); + CHECK(a[1].i == 3); + CHECK(a[2].i == 5); + CHECK(a[3].i == 7); + CHECK(a[4].i == 9); +} + +TEST_CASE( "merge_default_compare_function" ) { + const int size = 128; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back((i + 1) * 10); + } + test_helpers::shuffle(a); + + const auto middle = a.begin() + a.size() / 3; + gfx::timsort(a.begin(), middle); + gfx::timsort(middle, a.end()); + gfx::timmerge(a.begin(), middle, a.end()); + + CHECK(a.size() == std::size_t(size)); + for (int i = 0; i < size; ++i) { + CHECK(a[i] == (i + 1) * 10); + } +} + +TEST_CASE( "merge_stability" ) { + std::vector a; + + for (int i = 100; i >= 0; --i) { + a.push_back(std::make_pair(i, foo)); + a.push_back(std::make_pair(i, bar)); + a.push_back(std::make_pair(i, baz)); + } + + sort_and_merge(a, a.begin() + 117, &less_in_first); + + CHECK(a[0].first == 0); + CHECK(a[0].second == foo); + CHECK(a[1].first == 0); + CHECK(a[1].second == bar); + CHECK(a[2].first == 0); + CHECK(a[2].second == baz); + + CHECK(a[3].first == 1); + CHECK(a[3].second == foo); + CHECK(a[4].first == 1); + CHECK(a[4].second == bar); + CHECK(a[5].first == 1); + CHECK(a[5].second == baz); + + CHECK(a[6].first == 2); + CHECK(a[6].second == foo); + CHECK(a[7].first == 2); + CHECK(a[7].second == bar); + CHECK(a[8].first == 2); + CHECK(a[8].second == baz); + + CHECK(a[9].first == 3); + CHECK(a[9].second == foo); + CHECK(a[10].first == 3); + CHECK(a[10].second == bar); + CHECK(a[11].first == 3); + CHECK(a[11].second == baz); +} + +TEST_CASE( "merge_issue2_duplication" ) { + std::vector > a; + + for (int i = 0; i < 10000; ++i) { + int first = static_cast(rand()); + int second = static_cast(rand()); + + a.push_back(std::make_pair(first, second)); + } + + std::vector > expected(a); + + std::sort(expected.begin(), expected.end()); + sort_and_merge(a, a.begin() + 824); + + CHECK(a == expected); +} + +TEST_CASE( "merge_projection" ) { + const int size = 128; + + std::vector a; + for (int i = 0; i < size; ++i) { + a.push_back(i - 40); + } + test_helpers::shuffle(a); + + const auto middle = a.begin() + 43; + gfx::timsort(a.begin(), middle, std::greater(), std::negate()); + gfx::timsort(middle, a.end(), std::greater(), std::negate()); + gfx::timmerge(a.begin(), middle, a.end(), std::greater(), std::negate()); + + for (int i = 0; i < size; ++i) { + CHECK(a[i] == i - 40); + } +} diff --git a/tests/test_helpers.hpp b/tests/test_helpers.hpp new file mode 100644 index 0000000..c81b74b --- /dev/null +++ b/tests/test_helpers.hpp @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef GFX_TIMSORT_TEST_HELPERS_HPP +#define GFX_TIMSORT_TEST_HELPERS_HPP + +#include +#include +#include +#include + +namespace Catch +{ + // This functions is only available in an internal header that + // drags a lot of dependencies, it's cheaper to just declare + // it ourselves in this wrapper + unsigned int rngSeed(); +} + +namespace test_helpers { + // Helper types for the tests + + //////////////////////////////////////////////////////////// + // Timsort should work with types that are not + // default-constructible + + struct NonDefaultConstructible { + int i; + + NonDefaultConstructible(int i_) : i(i_) { + } + + friend bool operator<(NonDefaultConstructible const &x, NonDefaultConstructible const &y) { + return x.i < y.i; + } + }; + + //////////////////////////////////////////////////////////// + // Tools to test the stability of the sort + + enum id { foo, bar, baz }; + + typedef std::pair pair_t; + + inline bool less_in_first(pair_t x, pair_t y) { + return x.first < y.first; + } + + //////////////////////////////////////////////////////////// + // Timsort should work with iterators that don't have a + // post-increment or post-decrement operation + + template + class NoPostIterator + { + public: + + //////////////////////////////////////////////////////////// + // Public types + + typedef typename std::iterator_traits::iterator_category iterator_category; + typedef Iterator iterator_type; + typedef typename std::iterator_traits::value_type value_type; + typedef typename std::iterator_traits::difference_type difference_type; + typedef typename std::iterator_traits::pointer pointer; + typedef typename std::iterator_traits::reference reference; + + //////////////////////////////////////////////////////////// + // Constructors + + NoPostIterator() : _it() { + } + + explicit NoPostIterator(Iterator it) : _it(it) { + } + + //////////////////////////////////////////////////////////// + // Members access + + iterator_type base() const { + return _it; + } + + //////////////////////////////////////////////////////////// + // Element access + + reference operator*() const { + return *base(); + } + + pointer operator->() const { + return &(operator*()); + } + + //////////////////////////////////////////////////////////// + // Increment/decrement operators + + NoPostIterator& operator++() { + ++_it; + return *this; + } + + NoPostIterator& operator--() { + --_it; + return *this; + } + + NoPostIterator& operator+=(difference_type increment) { + _it += increment; + return *this; + } + + NoPostIterator& operator-=(difference_type increment) { + _it -= increment; + return *this; + } + + //////////////////////////////////////////////////////////// + // Comparison operators + + friend bool operator==(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() == rhs.base(); + } + + friend bool operator!=(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() != rhs.base(); + } + + //////////////////////////////////////////////////////////// + // Relational operators + + friend bool operator<(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() < rhs.base(); + } + + friend bool operator<=(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() <= rhs.base(); + } + + friend bool operator>(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() > rhs.base(); + } + + friend bool operator>=(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() >= rhs.base(); + } + + //////////////////////////////////////////////////////////// + // Arithmetic operators + + friend NoPostIterator operator+(NoPostIterator it, difference_type size) { + return it += size; + } + + friend NoPostIterator operator+(difference_type size, NoPostIterator it) { + return it += size; + } + + friend NoPostIterator operator-(NoPostIterator it, difference_type size) { + return it -= size; + } + + friend difference_type operator-(NoPostIterator const& lhs, NoPostIterator const& rhs) { + return lhs.base() - rhs.base(); + } + + private: + + Iterator _it; + }; + + template + NoPostIterator make_no_post_iterator(Iterator it) { + return NoPostIterator(it); + } + + //////////////////////////////////////////////////////////// + // Shuffle function + + template + void shuffle(RandomAccessIterator first, RandomAccessIterator last) + { + thread_local std::mt19937 random_engine(Catch::rngSeed()); + + std::shuffle(first, last, random_engine); + } + + template + void shuffle(RandomAccessRange &range) + { + test_helpers::shuffle(std::begin(range), std::end(range)); + } +} + +#endif // GFX_TIMSORT_TEST_HELPERS_HPP diff --git a/tests/verbose_abort.cpp b/tests/verbose_abort.cpp new file mode 100644 index 0000000..eec0271 --- /dev/null +++ b/tests/verbose_abort.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include + +#ifdef _LIBCPP_VERSION +#if defined(_LIBCPP_ENABLE_ASSERTIONS) && _LIBCPP_ENABLE_ASSERTIONS + +#include +#include +#include + +namespace std::inline __1 +{ + /* + * Required to avoid linking issues with AppleClang when + * compiling with _LIBCPP_ENABLE_ASSERTIONS. + * See https://releases.llvm.org/16.0.0/projects/libcxx/docs/UsingLibcxx.html#enabling-the-safe-libc-mode + */ + [[noreturn]] + void __libcpp_verbose_abort(char const* format, ...) { + std::va_list list; + va_start(list, format); + std::vfprintf(stderr, format, list); + va_end(list); + + std::abort(); + } + } + +#endif +#endif diff --git a/tests/windows.cpp b/tests/windows.cpp new file mode 100644 index 0000000..2f5b510 --- /dev/null +++ b/tests/windows.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2011 Fuji, Goro (gfx) . + * Copyright (c) 2019-2024 Morwenn. + * + * SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" + +TEST_CASE( "check inclusion of windows.h" ) { + const int size = 100; + + std::vector vec; + for (int i = 0; i < size; ++i) { + vec.push_back(i); + } + + test_helpers::shuffle(vec); + gfx::timsort(vec.begin(), vec.end()); + + for (int i = 0; i < size; ++i) { + CHECK(vec[i] == i); + } +} diff --git a/timsort.hpp b/timsort.hpp deleted file mode 100644 index ac9d143..0000000 --- a/timsort.hpp +++ /dev/null @@ -1,674 +0,0 @@ -/* - * C++ implementation of timsort - * - * ported from Python's and OpenJDK's: - * - http://svn.python.org/projects/python/trunk/Objects/listobject.c - * - http://cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java - * - * Copyright (c) 2011 Fuji, Goro (gfx) . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#ifndef GFX_TIMSORT_HPP -#define GFX_TIMSORT_HPP - -#include -#include -#include -#include -#include - -#ifdef ENABLE_TIMSORT_LOG -#include -#define GFX_TIMSORT_LOG(expr) (std::clog << "# " << __func__ << ": " << expr << std::endl) -#else -#define GFX_TIMSORT_LOG(expr) ((void)0) -#endif - -#if ENABLE_STD_MOVE && __cplusplus >= 201103L -#define GFX_TIMSORT_MOVE(x) std::move(x) -#else -#define GFX_TIMSORT_MOVE(x) (x) -#endif - -namespace gfx { - -// --------------------------------------- -// Declaration -// --------------------------------------- - -/** - * Same as std::stable_sort(first, last). - */ -template -inline void timsort(RandomAccessIterator const first, RandomAccessIterator const last); - -/** - * Same as std::stable_sort(first, last, c). - */ -template -inline void timsort(RandomAccessIterator const first, RandomAccessIterator const last, LessFunction compare); - - -// --------------------------------------- -// Implementation -// --------------------------------------- - -template class Compare { - public: - typedef Value value_type; - typedef LessFunction func_type; - - Compare(LessFunction f) - : less_(f) { } - Compare(const Compare& other) - : less_(other.less_) { } - - bool lt(value_type x, value_type y) { - return less_(x, y); - } - bool le(value_type x, value_type y) { - return less_(x, y) || !less_(y, x); - } - bool gt(value_type x, value_type y) { - return !less_(x, y) && less_(y, x); - } - bool ge(value_type x, value_type y) { - return !less_(x, y); - } - - func_type& less_function() { - return less_; - } - private: - func_type less_; -}; - -template -class TimSort { - typedef RandomAccessIterator iter_t; - typedef typename std::iterator_traits::value_type value_t; - typedef typename std::iterator_traits::reference ref_t; - typedef typename std::iterator_traits::difference_type diff_t; - typedef Compare compare_t; - - static const int MIN_MERGE = 32; - - compare_t comp_; - - static const int MIN_GALLOP = 7; - - int minGallop_; // default to MIN_GALLOP - - std::vector tmp_; // temp storage for merges - typedef typename std::vector::iterator tmp_iter_t; - - struct run { - iter_t base; - diff_t len; - - run(iter_t const b, diff_t const l) : base(b), len(l) { } - }; - std::vector pending_; - - static - void sort(iter_t const lo, iter_t const hi, compare_t c) { - assert( lo <= hi ); - - diff_t nRemaining = (hi - lo); - if(nRemaining < 2) { - return; // nothing to do - } - - if(nRemaining < MIN_MERGE) { - diff_t const initRunLen = countRunAndMakeAscending(lo, hi, c); - GFX_TIMSORT_LOG("initRunLen: " << initRunLen); - binarySort(lo, hi, lo + initRunLen, c); - return; - } - - TimSort ts(c); - diff_t const minRun = minRunLength(nRemaining); - iter_t cur = lo; - do { - diff_t runLen = countRunAndMakeAscending(cur, hi, c); - - if(runLen < minRun) { - diff_t const force = std::min(nRemaining, minRun); - binarySort(cur, cur + force, cur + runLen, c); - runLen = force; - } - - ts.pushRun(cur, runLen); - ts.mergeCollapse(); - - cur += runLen; - nRemaining -= runLen; - } while(nRemaining != 0); - - assert( cur == hi ); - ts.mergeForceCollapse(); - assert( ts.pending_.size() == 1 ); - - GFX_TIMSORT_LOG("size: " << (hi - lo) << " tmp_.size(): " << ts.tmp_.size() << " pending_.size(): " << ts.pending_.size()); - } // sort() - - static - void binarySort(iter_t const lo, iter_t const hi, iter_t start, compare_t compare) { - assert( lo <= start && start <= hi ); - if(start == lo) { - ++start; - } - for( ; start < hi; ++start ) { - assert(lo <= start); - /*const*/ value_t pivot = GFX_TIMSORT_MOVE(*start); - - iter_t const pos = std::upper_bound(lo, start, pivot, compare.less_function()); - for(iter_t p = start; p > pos; --p) { - *p = GFX_TIMSORT_MOVE(*(p - 1)); - } - *pos = GFX_TIMSORT_MOVE(pivot); - } - } - - static - diff_t countRunAndMakeAscending(iter_t const lo, iter_t const hi, compare_t compare) { - assert( lo < hi ); - - iter_t runHi = lo + 1; - if( runHi == hi ) { - return 1; - } - - if(compare.lt(*(runHi++), *lo)) { // descending - while(runHi < hi && compare.lt(*runHi, *(runHi - 1))) { - ++runHi; - } - std::reverse(lo, runHi); - } - else { // ascending - while(runHi < hi && compare.ge(*runHi, *(runHi - 1))) { - ++runHi; - } - } - - return runHi - lo; - } - - static - diff_t minRunLength(diff_t n) { - assert( n >= 0 ); - - diff_t r = 0; - while(n >= MIN_MERGE) { - r |= (n & 1); - n >>= 1; - } - return n + r; - } - - TimSort(compare_t c) - : comp_(c), minGallop_(MIN_GALLOP) { - } - - void pushRun(iter_t const runBase, diff_t const runLen) { - pending_.push_back(run(runBase, runLen)); - } - - void mergeCollapse() { - while( pending_.size() > 1 ) { - diff_t n = pending_.size() - 2; - - if(n > 0 && pending_[n - 1].len <= pending_[n].len + pending_[n + 1].len) { - if(pending_[n - 1].len < pending_[n + 1].len) { - --n; - } - mergeAt(n); - } - else if(pending_[n].len <= pending_[n + 1].len) { - mergeAt(n); - } - else { - break; - } - } - } - - void mergeForceCollapse() { - while( pending_.size() > 1 ) { - diff_t n = pending_.size() - 2; - - if(n > 0 && pending_[n - 1].len < pending_[n + 1].len) { - --n; - } - mergeAt(n); - } - } - - void mergeAt(diff_t const i) { - diff_t const stackSize = pending_.size(); - assert( stackSize >= 2 ); - assert( i >= 0 ); - assert( i == stackSize - 2 || i == stackSize - 3 ); - - iter_t base1 = pending_[i].base; - diff_t len1 = pending_[i].len; - iter_t base2 = pending_[i + 1].base; - diff_t len2 = pending_[i + 1].len; - - assert( len1 > 0 && len2 > 0 ); - assert( base1 + len1 == base2 ); - - pending_[i].len = len1 + len2; - - if(i == stackSize - 3) { - pending_[i + 1] = pending_[i + 2]; - } - - pending_.pop_back(); - - diff_t const k = gallopRight(*base2, base1, len1, 0); - assert( k >= 0 ); - - base1 += k; - len1 -= k; - - if(len1 == 0) { - return; - } - - len2 = gallopLeft(*(base1 + (len1 - 1)), base2, len2, len2 - 1); - assert( len2 >= 0 ); - if(len2 == 0) { - return; - } - - if(len1 <= len2) { - mergeLo(base1, len1, base2, len2); - } - else { - mergeHi(base1, len1, base2, len2); - } - } - - template - diff_t gallopLeft(ref_t key, Iter const base, diff_t const len, diff_t const hint) { - assert( len > 0 && hint >= 0 && hint < len ); - - diff_t lastOfs = 0; - diff_t ofs = 1; - - if(comp_.gt(key, *(base + hint))) { - diff_t const maxOfs = len - hint; - while(ofs < maxOfs && comp_.gt(key, *(base + (hint + ofs)))) { - lastOfs = ofs; - ofs = (ofs << 1) + 1; - - if(ofs <= 0) { // int overflow - ofs = maxOfs; - } - } - if(ofs > maxOfs) { - ofs = maxOfs; - } - - lastOfs += hint; - ofs += hint; - } - else { - diff_t const maxOfs = hint + 1; - while(ofs < maxOfs && comp_.le(key, *(base + (hint - ofs)))) { - lastOfs = ofs; - ofs = (ofs << 1) + 1; - - if(ofs <= 0) { - ofs = maxOfs; - } - } - if(ofs > maxOfs) { - ofs = maxOfs; - } - - diff_t const tmp = lastOfs; - lastOfs = hint - ofs; - ofs = hint - tmp; - } - assert( -1 <= lastOfs && lastOfs < ofs && ofs <= len ); - - return std::lower_bound(base+(lastOfs+1), base+ofs, key, comp_.less_function()) - base; - } - - template - diff_t gallopRight(ref_t key, Iter const base, diff_t const len, diff_t const hint) { - assert( len > 0 && hint >= 0 && hint < len ); - - diff_t ofs = 1; - diff_t lastOfs = 0; - - if(comp_.lt(key, *(base + hint))) { - diff_t const maxOfs = hint + 1; - while(ofs < maxOfs && comp_.lt(key, *(base + (hint - ofs)))) { - lastOfs = ofs; - ofs = (ofs << 1) + 1; - - if(ofs <= 0) { - ofs = maxOfs; - } - } - if(ofs > maxOfs) { - ofs = maxOfs; - } - - diff_t const tmp = lastOfs; - lastOfs = hint - ofs; - ofs = hint - tmp; - } - else { - diff_t const maxOfs = len - hint; - while(ofs < maxOfs && comp_.ge(key, *(base + (hint + ofs)))) { - lastOfs = ofs; - ofs = (ofs << 1) + 1; - - if(ofs <= 0) { // int overflow - ofs = maxOfs; - } - } - if(ofs > maxOfs) { - ofs = maxOfs; - } - - lastOfs += hint; - ofs += hint; - } - assert( -1 <= lastOfs && lastOfs < ofs && ofs <= len ); - - return std::upper_bound(base+(lastOfs+1), base+ofs, key, comp_.less_function()) - base; - } - - void mergeLo(iter_t const base1, diff_t len1, iter_t const base2, diff_t len2) { - assert( len1 > 0 && len2 > 0 && base1 + len1 == base2 ); - - copy_to_tmp(base1, len1); - - tmp_iter_t cursor1 = tmp_.begin(); - iter_t cursor2 = base2; - iter_t dest = base1; - - *(dest++) = *(cursor2++); - if(--len2 == 0) { - std::copy(cursor1, cursor1 + len1, dest); - return; - } - if(len1 == 1) { - std::copy(cursor2, cursor2 + len2, dest); - *(dest + len2) = *cursor1; - return; - } - - int minGallop(minGallop_); - - // outer: - while(true) { - int count1 = 0; - int count2 = 0; - - bool break_outer = false; - do { - assert( len1 > 1 && len2 > 0 ); - - if(comp_.lt(*cursor2, *cursor1)) { - *(dest++) = *(cursor2++); - ++count2; - count1 = 0; - if(--len2 == 0) { - break_outer = true; - break; - } - } - else { - *(dest++) = *(cursor1++); - ++count1; - count2 = 0; - if(--len1 == 1) { - break_outer = true; - break; - } - } - } while( (count1 | count2) < minGallop ); - if(break_outer) { - break; - } - - do { - assert( len1 > 1 && len2 > 0 ); - - count1 = gallopRight(*cursor2, cursor1, len1, 0); - if(count1 != 0) { - std::copy_backward(cursor1, cursor1 + count1, dest + count1); - dest += count1; - cursor1 += count1; - len1 -= count1; - - if(len1 <= 1) { - break_outer = true; - break; - } - } - *(dest++) = *(cursor2++); - if(--len2 == 0) { - break_outer = true; - break; - } - - count2 = gallopLeft(*cursor1, cursor2, len2, 0); - if(count2 != 0) { - std::copy(cursor2, cursor2 + count2, dest); - dest += count2; - cursor2 += count2; - len2 -= count2; - if(len2 == 0) { - break_outer = true; - break; - } - } - *(dest++) = *(cursor1++); - if(--len1 == 1) { - break_outer = true; - break; - } - - --minGallop; - } while( (count1 >= MIN_GALLOP) | (count2 >= MIN_GALLOP) ); - if(break_outer) { - break; - } - - if(minGallop < 0) { - minGallop = 0; - } - minGallop += 2; - } // end of "outer" loop - - minGallop_ = std::min(minGallop, 1); - - if(len1 == 1) { - assert( len2 > 0 ); - std::copy(cursor2, cursor2 + len2, dest); - *(dest + len2) = *cursor1; - } - else { - assert( len1 != 0 && "Comparision function violates its general contract"); - assert( len2 == 0 ); - assert( len1 > 1 ); - std::copy(cursor1, cursor1 + len1, dest); - } - } - - void mergeHi(iter_t const base1, diff_t len1, iter_t const base2, diff_t len2) { - assert( len1 > 0 && len2 > 0 && base1 + len1 == base2 ); - - copy_to_tmp(base2, len2); - - iter_t cursor1 = base1 + (len1 - 1); - tmp_iter_t cursor2 = tmp_.begin() + (len2 - 1); - iter_t dest = base2 + (len2 - 1); - - *(dest--) = *(cursor1--); - if(--len1 == 0) { - std::copy(tmp_.begin(), tmp_.begin() + len2, dest - (len2 - 1)); - return; - } - if(len2 == 1) { - dest -= len1; - cursor1 -= len1; - std::copy_backward(cursor1 + 1, cursor1 + (1 + len1), dest + (1 + len1)); - *dest = *cursor2; - return; - } - - int minGallop( minGallop_ ); - - // outer: - while(true) { - int count1 = 0; - int count2 = 0; - - bool break_outer = false; - do { - assert( len1 > 0 && len2 > 1 ); - - if(comp_.lt(*cursor2, *cursor1)) { - *(dest--) = *(cursor1--); - ++count1; - count2 = 0; - if(--len1 == 0) { - break_outer = true; - break; - } - } - else { - *(dest--) = *(cursor2--); - ++count2; - count1 = 0; - if(--len2 == 1) { - break_outer = true; - break; - } - } - } while( (count1 | count2) < minGallop ); - if(break_outer) { - break; - } - - do { - assert( len1 > 0 && len2 > 1 ); - - count1 = len1 - gallopRight(*cursor2, base1, len1, len1 - 1); - if(count1 != 0) { - dest -= count1; - cursor1 -= count1; - len1 -= count1; - std::copy_backward(cursor1 + 1, cursor1 + (1 + count1), dest + (1 + count1)); - - if(len1 == 0) { - break_outer = true; - break; - } - } - *(dest--) = *(cursor2--); - if(--len2 == 1) { - break_outer = true; - break; - } - - count2 = len2 - gallopLeft(*cursor1, tmp_.begin(), len2, len2 - 1); - if(count2 != 0) { - dest -= count2; - cursor2 -= count2; - len2 -= count2; - std::copy(cursor2 + 1, cursor2 + (1 + count2), dest + 1); - if(len2 <= 1) { - break_outer = true; - break; - } - } - *(dest--) = *(cursor1--); - if(--len1 == 0) { - break_outer = true; - break; - } - - minGallop--; - } while( (count1 >= MIN_GALLOP) | (count2 >= MIN_GALLOP) ); - if(break_outer) { - break; - } - - if(minGallop < 0) { - minGallop = 0; - } - minGallop += 2; - } // end of "outer" loop - - minGallop_ = std::min(minGallop, 1); - - if(len2 == 1) { - assert( len1 > 0 ); - dest -= len1; - cursor1 -= len1; - std::copy_backward(cursor1 + 1, cursor1 + (1 + len1), dest + (1 + len1)); - *dest = *cursor2; - } - else { - assert( len2 != 0 && "Comparision function violates its general contract"); - assert( len1 == 0 ); - assert( len2 > 1 ); - std::copy(tmp_.begin(), tmp_.begin() + len2, dest - (len2 - 1)); - } - } - - void copy_to_tmp(iter_t const begin, diff_t const len) { - tmp_.clear(); - tmp_.reserve(len); - std::copy(begin, begin + len, std::back_inserter(tmp_)); - } - - // the only interface is the friend timsort() function - template - friend void timsort(IterT first, IterT last, LessT c); -}; - -template -inline void timsort(RandomAccessIterator const first, RandomAccessIterator const last) { - typedef typename std::iterator_traits::value_type value_type; - timsort(first, last, std::less()); -} - -template -inline void timsort(RandomAccessIterator const first, RandomAccessIterator const last, LessFunction compare) { - TimSort::sort(first, last, compare); -} - -} // namespace gfx - -#undef GFX_TIMSORT_LOG -#undef GFX_TIMSORT_MOVE -#endif // GFX_TIMSORT_HPP -