diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d70f7172f..e2a4e69cc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -264,6 +264,74 @@ jobs: cmake --install "$GITHUB_WORKSPACE/build-ninja" --prefix "$GITHUB_WORKSPACE/install-ninja" bash .github/ci/flatzinc-tools-smoke.sh "$GITHUB_WORKSPACE/install-ninja" + build-cmake-allocator: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + + - name: Build and install allocator package + shell: bash + run: | + set -euxo pipefail + cmake -S . -B build-allocator \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ + -DGECODE_BUILD_SHARED=ON -DGECODE_BUILD_STATIC=OFF \ + -DGECODE_ENABLE_MIMALLOC=ON \ + -DGECODE_ENABLE_GCC_VISIBILITY=OFF \ + -DBUILD_TESTING=OFF -DGECODE_ENABLE_EXAMPLES=OFF \ + -DGECODE_ENABLE_SEARCH=OFF -DGECODE_ENABLE_INT_VARS=OFF \ + -DGECODE_ENABLE_SET_VARS=OFF -DGECODE_ENABLE_FLOAT_VARS=OFF \ + -DGECODE_ENABLE_MINIMODEL=OFF -DGECODE_ENABLE_DRIVER=OFF \ + -DGECODE_ENABLE_FLATZINC=OFF -DGECODE_ENABLE_QT=OFF -DGECODE_ENABLE_GIST=OFF + cmake --build build-allocator --config "$BUILD_TYPE" --parallel 2 + cmake --install build-allocator --config "$BUILD_TYPE" --prefix "$GITHUB_WORKSPACE/install-allocator" + + - name: Check private allocator exports + shell: bash + run: | + set -euxo pipefail + nm -D --defined-only install-allocator/lib/libgecodesupport.so > build-allocator/exports.txt + if grep -E '[[:space:]]_?(mi_[[:alnum:]_]+|malloc|calloc|realloc|free|aligned_alloc|posix_memalign)$' build-allocator/exports.txt; then + echo "Allocator implementation or malloc replacement exported by gecodesupport" + exit 1 + fi + + - name: Reject unsupported allocator combinations + shell: bash + run: | + set -euxo pipefail + if cmake -S . -B "$RUNNER_TEMP/mimalloc-custom" \ + -DGECODE_ENABLE_MIMALLOC=ON -DGECODE_ENABLE_ALLOCATOR=OFF > "$RUNNER_TEMP/mimalloc-custom.log" 2>&1; then + exit 1 + fi + grep -q 'requires GECODE_ENABLE_ALLOCATOR=ON' "$RUNNER_TEMP/mimalloc-custom.log" + if cmake -S . -B "$RUNNER_TEMP/mimalloc-static" \ + -DGECODE_ENABLE_MIMALLOC=ON -DGECODE_BUILD_STATIC=ON > "$RUNNER_TEMP/mimalloc-static.log" 2>&1; then + exit 1 + fi + grep -q 'requires GECODE_BUILD_STATIC=OFF' "$RUNNER_TEMP/mimalloc-static.log" + if cmake -S . -B "$RUNNER_TEMP/mimalloc-sanitizer" \ + -DGECODE_ENABLE_MIMALLOC=ON -DGECODE_SANITIZER=address > "$RUNNER_TEMP/mimalloc-sanitizer.log" 2>&1; then + exit 1 + fi + grep -q 'does not support GECODE_SANITIZER' "$RUNNER_TEMP/mimalloc-sanitizer.log" + + - name: Installed allocator coexistence and thread transfer + shell: bash + run: | + set -euxo pipefail + cmake -S test/package/allocator -B build-allocator-consumer \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ + -DCMAKE_PREFIX_PATH="$GITHUB_WORKSPACE/install-allocator" \ + -DGECODE_LINK_VARIANT=shared -DGECODE_EXPECT_MIMALLOC=ON \ + -DGECODE_HOST_MIMALLOC_OVERRIDE=ON \ + -DMIMALLOC_SOURCE_DIR="$GITHUB_WORKSPACE/build-allocator/_deps/gecode_mimalloc-src" + cmake --build build-allocator-consumer --config "$BUILD_TYPE" --parallel 2 + ctest --test-dir build-allocator-consumer --build-config "$BUILD_TYPE" --output-on-failure + test -f install-allocator/share/licenses/gecode/mimalloc-LICENSE + test ! -f install-allocator/include/mimalloc.h + build-32-bit-linkage: runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 01b980c7be..0767a92df6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,7 @@ option(GECODE_ENABLE_FLATZINC "Build FlatZinc module" ON) option(GECODE_ENABLE_MPFR "Enable MPFR support" ON) option(GECODE_ENABLE_ALLOCATOR "Enable default allocator" ON) +option(GECODE_ENABLE_MIMALLOC "Use a private mimalloc backend for shared libraries" OFF) option(GECODE_ENABLE_AUDIT "Enable audit code" OFF) # Test-only; FailPoint state is process-global and is supported only by the # dedicated isolated single-threaded fault-test executable below. @@ -774,6 +775,20 @@ if(HAVE_BUILTIN_POPCOUNTLL) set(GECODE_HAS_BUILTIN_POPCOUNTLL "/**/") endif() +if(GECODE_ENABLE_MIMALLOC) + if(NOT GECODE_ENABLE_ALLOCATOR) + message(FATAL_ERROR "GECODE_ENABLE_MIMALLOC requires GECODE_ENABLE_ALLOCATOR=ON") + endif() + if(GECODE_BUILD_STATIC) + message(FATAL_ERROR "GECODE_ENABLE_MIMALLOC requires GECODE_BUILD_STATIC=OFF; static builds cannot isolate the private mimalloc symbols") + endif() + if(GECODE_SANITIZER_NORMALIZED) + message(FATAL_ERROR "GECODE_ENABLE_MIMALLOC does not support GECODE_SANITIZER; use the system allocator for sanitizer builds") + endif() + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/GecodeMimalloc.cmake) + gecode_configure_mimalloc() +endif() + # Process config.hpp using autoconf-like undef handling. list(LENGTH CONFIG length) math(EXPR length "${length} - 1") @@ -802,6 +817,10 @@ foreach(i RANGE ${length}) set(CONFIG_OUT "${CONFIG_OUT}${line}") endforeach() +if(GECODE_ENABLE_MIMALLOC) + string(APPEND CONFIG_OUT "\n/* Use the private mimalloc backend (CMake only). */\n#define GECODE_MIMALLOC 1\n") +endif() + file(CONFIGURE OUTPUT "${GECODE_BINARY_DIR}/gecode/support/config.hpp" CONTENT "/* gecode/support/config.hpp. Generated from config.hpp.in by configure. */ /* gecode/support/config.hpp.in. Generated from configure.ac by autoheader. */ @@ -1235,6 +1254,12 @@ foreach(component ${GECODE_LIBRARY_COMPONENTS}) add_gecode_component_library(${component}) endforeach() +if(GECODE_ENABLE_MIMALLOC) + target_sources(gecodesupport_shared PRIVATE $) + target_include_directories(gecodesupport_shared PRIVATE "${GECODE_MIMALLOC_SOURCE_DIR}/include") + target_link_libraries(gecodesupport_shared PRIVATE ${GECODE_MIMALLOC_LIBRARIES}) +endif() + if(GECODE_ENABLE_THREAD) find_package(Threads REQUIRED) gecode_link_component(support Threads::Threads) @@ -1649,6 +1674,11 @@ endif() # --------------------------------------------------------------------------- if(GECODE_INSTALL) include(CMakePackageConfigHelpers) + if(GECODE_ENABLE_MIMALLOC) + install(FILES "${GECODE_MIMALLOC_SOURCE_DIR}/LICENSE" + DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/gecode + RENAME mimalloc-LICENSE) + endif() install(TARGETS ${GECODE_INSTALL_TARGETS} EXPORT GecodeTargets @@ -1787,7 +1817,7 @@ foreach(gecode_option IN ITEMS GECODE_ENABLE_EXAMPLES GECODE_INSTALL GECODE_ENABLE_SEARCH GECODE_ENABLE_INT_VARS GECODE_ENABLE_SET_VARS GECODE_ENABLE_FLOAT_VARS GECODE_ENABLE_MINIMODEL GECODE_ENABLE_DRIVER - GECODE_ENABLE_FLATZINC GECODE_ENABLE_ALLOCATOR GECODE_ENABLE_AUDIT + GECODE_ENABLE_FLATZINC GECODE_ENABLE_ALLOCATOR GECODE_ENABLE_MIMALLOC GECODE_ENABLE_AUDIT GECODE_ENABLE_FAULT_INJECTION GECODE_REGENERATE_VARIMP GECODE_DOC_SEARCH GECODE_DOC_TAGFILE) get_property(gecode_option_description CACHE ${gecode_option} PROPERTY HELPSTRING) diff --git a/changelog.in b/changelog.in index 08a4acf47c..89b00d12bb 100755 --- a/changelog.in +++ b/changelog.in @@ -73,6 +73,15 @@ Date: unreleased [DESCRIPTION] This is the development changelog for the next Gecode release. +[ENTRY] +Module: support +What: new +Rank: minor +[DESCRIPTION] +Add an optional CMake mimalloc backend for shared Gecode libraries. +Mimalloc can substantially improve parallel search performance on some workloads. +The system allocator is the default. + [ENTRY] Module: kernel What: new diff --git a/cmake/GecodeMimalloc.cmake b/cmake/GecodeMimalloc.cmake new file mode 100644 index 0000000000..5a72f3a93e --- /dev/null +++ b/cmake/GecodeMimalloc.cmake @@ -0,0 +1,46 @@ +# A private source build keeps malloc/new replacement outside Gecode. +function(gecode_configure_mimalloc) + foreach(target mimalloc mimalloc-static mimalloc-obj mimalloc-obj-target) + if(TARGET ${target}) + message(FATAL_ERROR "GECODE_ENABLE_MIMALLOC cannot share an existing ${target} target; Gecode requires its own non-overriding mimalloc build") + endif() + endforeach() + + # Normal variables keep these choices local, including when Gecode is a subproject. + # Upstream's minimum policy version predates normal-variable preservation for + # set(CACHE); keep our local settings when it declares its cache options. + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + set(MI_OVERRIDE OFF) + set(MI_XMALLOC OFF) + set(MI_EXTRA_CPPDEFS "") + set(MI_DEBUG_TSAN OFF) + set(MI_DEBUG_UBSAN OFF) + set(MI_OSX_INTERPOSE OFF) + set(MI_OSX_ZONE OFF) + set(MI_WIN_REDIRECT OFF) + set(MI_BUILD_SHARED OFF) + set(MI_BUILD_STATIC OFF) + set(MI_BUILD_OBJECT ON) + set(MI_BUILD_TESTS OFF) + set(MI_USE_CXX DEFAULT) + set(MI_OPT_ARCH OFF) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MI_TLS_MODEL LOCAL_DYNAMIC) + endif() + + include(FetchContent) + FetchContent_Declare(gecode_mimalloc + URL https://codeload.github.com/microsoft/mimalloc/tar.gz/refs/tags/v3.5.3 + URL_HASH SHA256=3b4a15153a59905995f7070296ed604bb5ccc00cabb8b93446931aff77224d47) + FetchContent_MakeAvailable(gecode_mimalloc) + # Exclude upstream convenience targets and install rules; embed its object directly. + set_property(DIRECTORY "${gecode_mimalloc_SOURCE_DIR}" PROPERTY EXCLUDE_FROM_ALL TRUE) + set_target_properties(mimalloc-obj PROPERTIES C_VISIBILITY_PRESET hidden) + get_directory_property(mi_libraries DIRECTORY "${gecode_mimalloc_SOURCE_DIR}" DEFINITION mi_libraries) + set(GECODE_MIMALLOC_SOURCE_DIR "${gecode_mimalloc_SOURCE_DIR}" PARENT_SCOPE) + set(GECODE_MIMALLOC_LIBRARIES "${mi_libraries}" PARENT_SCOPE) +endfunction() diff --git a/docs/cmake-build.md b/docs/cmake-build.md index 23f9677791..6136adc6e5 100644 --- a/docs/cmake-build.md +++ b/docs/cmake-build.md @@ -126,6 +126,11 @@ to inspect test assignments. usable Qt GUI stack is available. `ON` requires Gist dependencies and fails configuration if they are unavailable. Gist requires Qt Gui, Widgets, and PrintSupport in addition to Qt Core. +- `GECODE_ENABLE_MIMALLOC`: downloads and embeds mimalloc 3.5.3 for Gecode heap + and space storage. Default `OFF`. Requires `GECODE_BUILD_STATIC=OFF` and + `GECODE_ENABLE_ALLOCATOR=ON`; incompatible with `GECODE_SANITIZER` and + `GECODE_PEAKHEAP`. Application `malloc` and `new` are unaffected. + Rebuild consumers when changing backends. - `GECODE_ENABLE_FAULT_INJECTION`: builds deterministic allocation and copy failpoints together with the `Fault::*` regression suite. This facility is test-only, process-global, isolated to the dedicated single-threaded fault diff --git a/gecode/support/allocator.cpp b/gecode/support/allocator.cpp index fab763faa1..8f25a509e0 100644 --- a/gecode/support/allocator.cpp +++ b/gecode/support/allocator.cpp @@ -33,10 +33,29 @@ #include +#ifdef GECODE_MIMALLOC +#include +#endif + namespace Gecode { namespace Support { Allocator allocator; +#ifdef GECODE_MIMALLOC + void* + Allocator::alloc(size_t n) { + return mi_malloc(n); + } + void* + Allocator::realloc(void* p, size_t n) { + return mi_realloc(p,n); + } + void + Allocator::free(void* p) { + mi_free(p); + } +#endif + }} // STATISTICS: support-any diff --git a/gecode/support/allocator.hpp b/gecode/support/allocator.hpp index 5477521b87..301c654c59 100644 --- a/gecode/support/allocator.hpp +++ b/gecode/support/allocator.hpp @@ -42,6 +42,10 @@ #include #endif +#if defined(GECODE_MIMALLOC) && defined(GECODE_PEAKHEAP) +#error "The mimalloc backend does not support GECODE_PEAKHEAP" +#endif + #ifdef GECODE_ALLOCATOR namespace Gecode { namespace Support { @@ -62,10 +66,19 @@ namespace Gecode { namespace Support { /// Default constructor Allocator(void); /// Allocate memory block of size \a n +#ifdef GECODE_MIMALLOC + GECODE_SUPPORT_EXPORT +#endif void* alloc(size_t n); /// Return address of reallocated memory block \a p of size \a n +#ifdef GECODE_MIMALLOC + GECODE_SUPPORT_EXPORT +#endif void* realloc(void* p, size_t n); /// Free memory block \a p +#ifdef GECODE_MIMALLOC + GECODE_SUPPORT_EXPORT +#endif void free(void* p); /// Copy \a n bytes from source \a s directly to \a d and returns \a d void* memcpy(void *d, const void *s, size_t n); @@ -75,6 +88,7 @@ namespace Gecode { namespace Support { forceinline Allocator::Allocator(void) { } +#ifndef GECODE_MIMALLOC forceinline void* Allocator::alloc(size_t n) { return ::malloc(n); @@ -87,6 +101,7 @@ namespace Gecode { namespace Support { Allocator::free(void* p) { ::free(p); } +#endif forceinline void* Allocator::memcpy(void *d, const void *s, size_t n) { return ::memcpy(d,s,n); diff --git a/test/package/allocator/CMakeLists.txt b/test/package/allocator/CMakeLists.txt new file mode 100644 index 0000000000..caa1cb579a --- /dev/null +++ b/test/package/allocator/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.21) +project(gecode_allocator_consumer LANGUAGES C CXX) +enable_testing() +find_package(Gecode CONFIG REQUIRED COMPONENTS support) +find_package(Threads REQUIRED) + +set(GECODE_LINK_VARIANT shared CACHE STRING "Installed Gecode variant: shared or static") +option(GECODE_EXPECT_MIMALLOC "Require the mimalloc backend" OFF) +set(MIMALLOC_SOURCE_DIR "" CACHE PATH "Mimalloc sources for the host allocator coexistence check") +option(GECODE_HOST_MIMALLOC_OVERRIDE "Let the host mimalloc replace malloc" OFF) +if(GECODE_LINK_VARIANT STREQUAL "shared") + add_library(allocator-boundary SHARED boundary.cpp) +elseif(GECODE_LINK_VARIANT STREQUAL "static") + add_library(allocator-boundary STATIC boundary.cpp) + target_compile_definitions(allocator-boundary PUBLIC ALLOCATOR_BOUNDARY_STATIC) +else() + message(FATAL_ERROR "GECODE_LINK_VARIANT must be shared or static") +endif() +target_compile_definitions(allocator-boundary PRIVATE ALLOCATOR_BOUNDARY_BUILD) +target_link_libraries(allocator-boundary PUBLIC Gecode::gecodesupport_${GECODE_LINK_VARIANT}) +add_executable(allocator-consumer main.cpp) +target_link_libraries(allocator-consumer PRIVATE allocator-boundary Threads::Threads) +target_compile_definitions(allocator-consumer PRIVATE GECODE_EXPECT_MIMALLOC=$) +if(GECODE_EXPECT_MIMALLOC) + if(NOT EXISTS "${MIMALLOC_SOURCE_DIR}/include/mimalloc.h") + message(FATAL_ERROR "The host allocator check requires MIMALLOC_SOURCE_DIR") + endif() + # Embed the complete object so macOS includes malloc-zone registration. + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + set(MI_OVERRIDE ${GECODE_HOST_MIMALLOC_OVERRIDE}) + set(MI_BUILD_SHARED OFF) + set(MI_BUILD_STATIC OFF) + set(MI_BUILD_OBJECT ON) + set(MI_BUILD_TESTS OFF) + set(MI_OPT_ARCH OFF) + add_subdirectory("${MIMALLOC_SOURCE_DIR}" host-mimalloc EXCLUDE_FROM_ALL) + get_directory_property(host_mi_libraries DIRECTORY "${MIMALLOC_SOURCE_DIR}" DEFINITION mi_libraries) + target_link_libraries(allocator-consumer PRIVATE mimalloc-obj ${host_mi_libraries}) + target_compile_definitions(allocator-consumer PRIVATE CHECK_HOST_MIMALLOC + GECODE_HOST_MIMALLOC_OVERRIDE=$) +endif() +add_test(NAME allocator-consumer COMMAND allocator-consumer) diff --git a/test/package/allocator/boundary.cpp b/test/package/allocator/boundary.cpp new file mode 100644 index 0000000000..9a9f7b34eb --- /dev/null +++ b/test/package/allocator/boundary.cpp @@ -0,0 +1,11 @@ +#include "boundary.hh" + +void* boundary_alloc(size_t n) { + return Gecode::heap.ralloc(n); +} +void* boundary_realloc(void* p, size_t n) { + return Gecode::heap.rrealloc(p,n); +} +void boundary_free(void* p) { + Gecode::heap.rfree(p); +} diff --git a/test/package/allocator/boundary.hh b/test/package/allocator/boundary.hh new file mode 100644 index 0000000000..124b95c5cc --- /dev/null +++ b/test/package/allocator/boundary.hh @@ -0,0 +1,20 @@ +#ifndef GECODE_TEST_PACKAGE_ALLOCATOR_BOUNDARY_HH +#define GECODE_TEST_PACKAGE_ALLOCATOR_BOUNDARY_HH + +#include + +#if defined(_WIN32) && !defined(ALLOCATOR_BOUNDARY_STATIC) +#ifdef ALLOCATOR_BOUNDARY_BUILD +#define ALLOCATOR_BOUNDARY_EXPORT __declspec(dllexport) +#else +#define ALLOCATOR_BOUNDARY_EXPORT __declspec(dllimport) +#endif +#else +#define ALLOCATOR_BOUNDARY_EXPORT +#endif + +ALLOCATOR_BOUNDARY_EXPORT void* boundary_alloc(size_t n); +ALLOCATOR_BOUNDARY_EXPORT void* boundary_realloc(void* p, size_t n); +ALLOCATOR_BOUNDARY_EXPORT void boundary_free(void* p); + +#endif diff --git a/test/package/allocator/main.cpp b/test/package/allocator/main.cpp new file mode 100644 index 0000000000..3d565a779c --- /dev/null +++ b/test/package/allocator/main.cpp @@ -0,0 +1,49 @@ +#include "boundary.hh" +#include +#include +#include +#ifdef CHECK_HOST_MIMALLOC +#include +#endif + +#if GECODE_EXPECT_MIMALLOC && !defined(GECODE_MIMALLOC) +#error "Expected mimalloc-enabled installed headers" +#elif !GECODE_EXPECT_MIMALLOC && defined(GECODE_MIMALLOC) +#error "Expected system-allocator installed headers" +#endif + +int main(void) { + void* p = Gecode::heap.ralloc(256); +#ifdef CHECK_HOST_MIMALLOC + void* host = mi_malloc(256); + void* system = std::malloc(256); + bool isolated = !mi_is_in_heap_region(p) && mi_is_in_heap_region(host); +#if GECODE_HOST_MIMALLOC_OVERRIDE + isolated = isolated && mi_is_in_heap_region(system); +#else + isolated = isolated && !mi_is_in_heap_region(system); +#endif + mi_free(host); + std::free(system); + if (!isolated) + return 1; +#endif + std::memset(p,0x5a,256); + std::thread resize([&] { p = boundary_realloc(p,16384); }); + resize.join(); + for (int i=0; i<256; i++) + if (static_cast(p)[i] != 0x5a) + return 2; +#ifdef CHECK_HOST_MIMALLOC + if (mi_is_in_heap_region(p)) + return 3; +#endif + std::thread release([&] { boundary_free(p); }); + release.join(); + std::thread allocate([&] { p = boundary_alloc(1024); }); + allocate.join(); + p = Gecode::heap.rrealloc(p,4096); + Gecode::heap.rfree(p); + + return 0; +}