diff --git a/.gitignore b/.gitignore index 3158eb4a4b..ce2af17f09 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ cmake-build-debug/* build.ninja .ninja* a.out +*.json diff --git a/doc/html/indexes/s01.html b/doc/html/indexes/s01.html index 493ce79f1e..21bf57dc25 100644 --- a/doc/html/indexes/s01.html +++ b/doc/html/indexes/s01.html @@ -25,7 +25,7 @@

-Function Index

+Function Index

1 2 4 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _

@@ -550,6 +550,10 @@
  • chatterjee_correlation

  • +

    chatterjee_correlation_mnn

    + +
  • +
  • chebyshev_clenshaw_recurrence

  • @@ -984,6 +988,14 @@
  • +

    decltype

    + +
  • +
  • defined

  • @@ -2485,6 +2513,14 @@
  • +

    lgamma_p

    +
    +
  • +
  • +

    lgamma_q

    +
    +
  • +
  • ljung_box

  • @@ -2815,7 +2851,10 @@
  • min

    -
    +
  • mode

    @@ -2987,11 +3026,14 @@
  • norm

  • @@ -3023,10 +3065,12 @@
  • Automatic Differentiation

  • Bessel Functions of the First and Second Kinds

  • Chatterjee Correlation

  • +
  • Chatterjee Correlation (M nearest neighbours)

  • Empirical Cumulative Distribution Function

  • Fibonacci Numbers

  • Lanczos Smoothing Derivatives

  • Modified Bessel Functions of the First and Second Kinds

  • +
  • Prime Sieve

  • tanh_sinh

  • @@ -3147,6 +3191,7 @@
  • Roots of Cubic Polynomials

  • The Incomplete Beta Function Inverses

  • The Remez Method

  • +
  • Van den Bos Unit-Square Quadrature

  • @@ -3163,6 +3208,13 @@
  • +

    place

    +
    +
  • +
  • plasma

  • @@ -3490,6 +3542,10 @@
  • +

    run

    +
    +
  • +
  • runs_above_and_below_median

  • @@ -3597,6 +3653,13 @@
  • +

    segment

    +
    +
  • +
  • semipolar

  • @@ -3878,8 +3944,10 @@
  • test

  • @@ -3945,6 +4013,28 @@
  • +

    the

    +
    +
  • +
  • title

  • @@ -4007,6 +4097,18 @@

    typeid

    +
  • +

    types

    +
    +
  • U @@ -4040,6 +4142,10 @@
  • +

    undefined

    + +
  • +
  • unif01

    • Gradient Descent

    • diff --git a/doc/html/indexes/s02.html b/doc/html/indexes/s02.html index 81da5381ea..c9115314cc 100644 --- a/doc/html/indexes/s02.html +++ b/doc/html/indexes/s02.html @@ -25,8 +25,8 @@

    -Class Index

    -

    A B C D E F G H I J K L M N O P Q R S T U V W

    +Class Index
    +

    A B C D E F G H I K L M N O P Q R S T U V W

    A @@ -66,6 +66,10 @@
  • binomial_distribution

  • +

    block

    + +
  • +
  • box_constraints

  • @@ -124,10 +128,6 @@
  • chi_squared_distribution

  • -

    cma_es_parameters

    - -
  • -
  • combined_convergence_policy

  • @@ -168,14 +168,6 @@
  • -

    default_policy

    - -
  • -
  • -

    differential_evolution_parameters

    - -
  • -
  • discrete_lanczos_derivative

  • @@ -296,13 +288,6 @@
    -J -
    -
    -
    K
    @@ -487,10 +472,6 @@

    -Typedef Index

    +Typedef Index

    A B C D E F G H I K L M N P R S T U V W

    diff --git a/doc/html/indexes/s04.html b/doc/html/indexes/s04.html index 3323692dae..76f188c9e1 100644 --- a/doc/html/indexes/s04.html +++ b/doc/html/indexes/s04.html @@ -25,7 +25,7 @@

    -Macro Index

    +Macro Index

    B F

    @@ -136,6 +136,7 @@ @@ -145,6 +146,7 @@
  • Calling User Defined Error Handlers

  • Setting Polices at Namespace Scope

  • Setting Policies at Namespace or Translation Unit Scope

  • +
  • Support for C++20 Modules

  • Trading Accuracy for Performance

  • @@ -175,10 +177,18 @@
  • Additional Implementation Notes

  • Changing the Policy Defaults

  • Error Handling Example

  • +
  • Support for C++20 Modules

  • Using Macros to Change the Policy Defaults

  • +

    BOOST_MATH_ENABLE_CUDA

    + +
  • +
  • BOOST_MATH_EVALUATION_ERROR_POLICY

  • @@ -264,6 +274,13 @@
  • +

    BOOST_MATH_HAS_CUDA_PRIME_SIEVE

    + +
  • +
  • BOOST_MATH_INDETERMINATE_RESULT_ERROR_POLICY

  • @@ -424,7 +441,10 @@
  • BOOST_MATH_USE_FLOAT128

    - +
  • diff --git a/doc/html/indexes/s05.html b/doc/html/indexes/s05.html index db583578ad..f6cd19400f 100644 --- a/doc/html/indexes/s05.html +++ b/doc/html/indexes/s05.html @@ -24,7 +24,7 @@

    -Index

    +Index

    1 2 4 5 7 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _

    @@ -487,10 +487,7 @@
  • Algorithm jSO

    - +
  • Algorithm TOMS 748: Alefeld, Potra and Shi: Enclosing zeros of continuous functions

    @@ -515,6 +512,7 @@
  • dis

  • performance

  • quadrature

  • +
  • the

  • v

  • @@ -547,6 +545,7 @@
  • expression

  • pdf

  • policy_type

  • +
  • the

  • value_type

  • @@ -702,6 +701,7 @@
  • Nesterov Accelerated Gradient Descent

  • Reverse Mode autodiff policies

  • Reverse Mode Automatic Differentiation

  • +
  • Support for C++20 Modules

  • @@ -790,6 +790,7 @@
  • message

  • multiprecision

  • performance

  • +
  • the

  • unchecked_bernoulli_b2n

  • zero

  • @@ -811,6 +812,7 @@
  • cyl_bessel_j

  • cyl_neumann

  • data

  • +
  • fail

  • GSL

  • n

  • O

  • @@ -998,6 +1000,10 @@
  • +

    block

    + +
  • +
  • bool

  • @@ -1156,6 +1162,7 @@
    @@ -1165,6 +1172,7 @@
  • Calling User Defined Error Handlers

  • Setting Polices at Namespace Scope

  • Setting Policies at Namespace or Translation Unit Scope

  • +
  • Support for C++20 Modules

  • Trading Accuracy for Performance

  • @@ -1195,10 +1203,18 @@
  • Additional Implementation Notes

  • Changing the Policy Defaults

  • Error Handling Example

  • +
  • Support for C++20 Modules

  • Using Macros to Change the Policy Defaults

  • +

    BOOST_MATH_ENABLE_CUDA

    + +
  • +
  • BOOST_MATH_EVALUATION_ERROR_POLICY

  • @@ -1284,6 +1300,13 @@
  • +

    BOOST_MATH_HAS_CUDA_PRIME_SIEVE

    + +
  • +
  • BOOST_MATH_INDETERMINATE_RESULT_ERROR_POLICY

  • @@ -1444,7 +1467,10 @@
  • BOOST_MATH_USE_FLOAT128

    - +
  • box_constraints

    @@ -2104,6 +2130,7 @@
  • performance

  • prime

  • resize

  • +
  • segment

  • size

  • value_type

  • @@ -2248,8 +2275,22 @@
  • O

  • +
  • +

    Chatterjee Correlation (M nearest neighbours)

    + +
  • chatterjee_correlation

  • +

    chatterjee_correlation_mnn

    + +
  • +
  • Chebyshev Polynomials

  • @@ -2661,6 +2704,7 @@
  • Caveats

  • Centered Continued Fractions

  • Chatterjee Correlation

  • +
  • Chatterjee Correlation (M nearest neighbours)

  • Chebyshev Polynomials

  • Complements are supported too - and when to use them

  • Conceptual Requirements for Real Number Types

  • @@ -2711,6 +2755,7 @@
  • Signal Statistics

  • Simple Continued Fractions

  • sinh_sinh

  • +
  • Support for C++20 Modules

  • tanh_sinh

  • Testing

  • The Lanczos Approximation

  • @@ -2725,6 +2770,7 @@
  • Using Boost.Math with High-Precision Floating-Point Libraries

  • Using Boost.Multiprecision

  • Using with GCC's __float128 datatype

  • +
  • Van den Bos Unit-Square Quadrature

  • Weibull Distribution

  • @@ -2792,6 +2838,7 @@
  • complex

  • continued_fraction_a

  • continued_fraction_b

  • +
  • eps

  • expint_as_fraction

  • expint_fraction

  • expression

  • @@ -3199,6 +3246,7 @@
  • performance

  • prime

  • support

  • +
  • the

  • @@ -3231,8 +3279,12 @@
  • -

    default_policy

    - +

    decltype

    +
  • defined

    @@ -3300,17 +3352,13 @@

    Differential Evolution

  • differential_evolution

  • -

    differential_evolution_parameters

    - -
  • -
  • differentiation

  • @@ -3745,6 +3794,10 @@
  • +

    eps

    + +
  • +
  • epsilon

  • @@ -4335,10 +4388,7 @@
  • Evolution Strategy with Covariance Matrix Adaptation

    - +
  • Exact-Width Floating-Point typedef s

    @@ -4350,6 +4400,8 @@
  • BOOST_FLOAT80_C

  • constants

  • defined

  • +
  • run

  • +
  • types

  • @@ -4562,6 +4614,7 @@
  • Use in template code

  • Using With MPFR or GMP - High-Precision Floating-Point Library

  • Using without expression templates for Boost.Test and others

  • +
  • Vector- and matrix-valued integrands

  • @@ -4655,6 +4708,13 @@
  • +

    fail

    + +
  • +
  • Falling Factorial

  • @@ -4848,7 +4909,10 @@
  • find_non_centrality

    - +
  • find_scale

    @@ -4926,6 +4990,7 @@
  • cpp_bin_float

  • cpp_dec_float

  • multiprecision

  • +
  • place

  • @@ -5162,6 +5227,7 @@
  • @@ -5248,14 +5314,17 @@
  • @@ -5265,11 +5334,13 @@
  • abscissa

  • constants

  • cpp_bin_float

  • +
  • decltype

  • expression

  • gauss

  • integrate

  • integration

  • multiprecision

  • +
  • norm

  • positive

  • quadrature

  • weights

  • @@ -5613,7 +5684,10 @@
  • Hints on using float128 (and __float128)

    - +
  • History

    @@ -5700,6 +5774,7 @@
  • of

  • performance

  • policy_type

  • +
  • the

  • value_type

  • @@ -6049,9 +6124,12 @@
  • gamma_q

  • GSL

  • Lanczos approximation

  • +
  • lgamma_p

  • +
  • lgamma_q

  • series

  • tgamma

  • tgamma_lower

  • +
  • the

  • @@ -6137,6 +6215,7 @@
  • Modified Akima interpolation

  • PCHIP interpolation

  • Quintic Hermite interpolation

  • +
  • Support for C++20 Modules

  • The Remez Method

  • Vector-valued Barycentric Rational Interpolation

  • Whittaker-Shannon interpolation

  • @@ -6583,10 +6662,6 @@

    jso

    -
  • -

    jso_parameters

    - -
  • K @@ -6607,6 +6682,7 @@
  • Jacobi Elliptic SN, CN and DN

  • Known Issues, and TODO List

  • Quintic Hermite interpolation

  • +
  • Van den Bos Unit-Square Quadrature

  • @@ -6793,6 +6869,7 @@
  • lambert_wm1_prime

  • ln

  • multiprecision

  • +
  • place

  • quadrature

  • range

  • refinement

  • @@ -6839,7 +6916,9 @@
  • Numerical Differentiation

  • Performance Tuning Macros

  • References

  • +
  • Support for C++20 Modules

  • term

  • +
  • the

  • The Lanczos Approximation

  • Using NTL Library

  • @@ -6854,6 +6933,7 @@
  • O

  • operator

  • performance

  • +
  • the

  • v

  • @@ -7009,6 +7089,14 @@
  • +

    lgamma_p

    + +
  • +
  • +

    lgamma_q

    + +
  • +
  • Library Comparison with GNU C++ version 9.2.1 20191008 on linux

  • @@ -8354,6 +8454,7 @@
  • Roots of Cubic Polynomials

  • The Incomplete Beta Function Inverses

  • The Remez Method

  • +
  • Van den Bos Unit-Square Quadrature

  • @@ -8454,6 +8555,7 @@
  • Performance Tuning Macros

  • Polynomial and Rational Function Evaluation

  • Polynomials

  • +
  • Prime Sieve

  • Quintic Hermite interpolation

  • Random Search

  • Reciprocal square root

  • @@ -8505,6 +8607,13 @@
  • +

    place

    + +
  • +
  • plasma

  • @@ -8541,7 +8650,6 @@
  • Policy Class Reference

  • +

    Prime Sieve

    + +
  • +
  • promote_args

  • @@ -8839,6 +8964,7 @@
  • norm

  • of

  • sup

  • +
  • the

  • unreal

  • @@ -8892,7 +9018,6 @@
  • @@ -8904,10 +9029,6 @@
  • random_search

  • -

    random_search_parameters

    - -
  • -
  • random_uniform_initializer_rvar

  • @@ -9119,6 +9240,7 @@
  • adjoint

  • autodiff

  • backward

  • +
  • block

  • BOOST_MATH_BUFFER_SIZE

  • BOOST_MATH_REVERSE_MODE_ET_ON

  • constants

  • @@ -9142,6 +9264,7 @@
  • rewind_to_checkpoint_at

  • rewind_to_last_checkpoint

  • rvar

  • +
  • types

  • x

  • zero_grad

  • @@ -9330,6 +9453,7 @@
  • cpp_bin_float

  • cpp_dec_float

  • multiprecision

  • +
  • types

  • @@ -9391,6 +9515,10 @@
  • +

    run

    + +
  • +
  • Runs tests

    U @@ -10534,6 +10737,10 @@
  • +

    undefined

    + +
  • +
  • unif01

  • +

    Van den Bos Unit-Square Quadrature

    + +
  • +
  • variance

  • +

    Vector- and matrix-valued integrands

    + +
  • +
  • Vector-valued Barycentric Rational Interpolation

    diff --git a/doc/html/math_toolkit/chatterjee_correlation.html b/doc/html/math_toolkit/chatterjee_correlation.html index 135a4f47de..46bb47e86b 100644 --- a/doc/html/math_toolkit/chatterjee_correlation.html +++ b/doc/html/math_toolkit/chatterjee_correlation.html @@ -7,7 +7,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -126,7 +126,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/chatterjee_correlation_mnn.html b/doc/html/math_toolkit/chatterjee_correlation_mnn.html new file mode 100644 index 0000000000..ce2fb35e9a --- /dev/null +++ b/doc/html/math_toolkit/chatterjee_correlation_mnn.html @@ -0,0 +1,149 @@ + + + +Chatterjee Correlation (M nearest neighbours) + + + + + + + + + + + + + + + + +
    Boost C++ LibrariesHomeLibrariesPeopleFAQMore
    +
    +
    +PrevUpHomeNext +
    +
    + +

    + + Synopsis +

    +
    #include <boost/math/statistics/chatterjee_correlation.hpp>
    +
    +namespace boost::math::statistics {
    +
    +    C++17:
    +    template <typename ExecutionPolicy, typename Container>
    +    auto chatterjee_correlation_mnn(ExecutionPolicy&& exec, const Container& u, const Container& v, std::size_t M);
    +
    +    C++11:
    +    template <typename Container>
    +    auto chatterjee_correlation_mnn(const Container& u, const Container& v, std::size_t M);
    +}
    +
    +

    + + Description +

    +

    + chatterjee_correlation_mnn + computes the revised Chatterjee rank correlation of Lin and Han (2021), which + generalises Chatterjee's coefficient by incorporating the M + right nearest neighbours of each point rather than only the single right neighbour. + The statistic still consistently estimates the same measure of dependence (between + 0 and 1; zero if and only if X and Y are independent, unity if and only if + Y is a measurable function of X), but its use of additional neighbours boosts + the power of the associated independence test. +

    +

    + The original coefficient chatterjee_correlation + has a statistical detection boundary of n-1/4 for testing independence, which + is substantially weaker than the parametric n-1/2 rate. By letting M grow with the sample size (with M/n -> + 0), the revised statistic can approach near-parametric efficiency. +

    +

    + Let X and Y be random variables, where Y is not constant, and let (X_i, Y_i) + be samples sorted so that X_(0) < X_(1) < ... < X_(n-1). Writing R_i + for the rank of Y_i and j_m(i) for the index of the m-th right nearest neighbour + of X_i, the statistic is +

    +
    xi_{n,M} = -2 + 6 * sum_i sum_{m=1}^{M} min(R_i, R_{j_m(i)}) / ((n + 1) * (n*M + M*(M + 1) / 4))
    +
    +

    + The complexity is O(n log n + n M). For M + of order O(1) or O(poly-log n) this is nearly linear; as M + approaches n it tends to O(n2). +

    +

    + An example is given below: +

    +
    std::vector<double> X{1,2,3,4,5};
    +std::vector<double> Y{1,2,3,4,5};
    +using boost::math::statistics::chatterjee_correlation_mnn;
    +std::size_t M = 2;
    +double coeff = chatterjee_correlation_mnn(X, Y, M);
    +
    +

    + Nota bene: If the input is an integer type the output + will be a double precision type. +

    +

    + + Choice + of M +

    +

    + The asymptotic null variance of the statistic is minimised when M is of order sqrt(n), which is a reasonable + default for users who want improved power without the quadratic cost of large + M. Pushing M + closer to n increases the power + of the independence test against smooth alternatives at the cost of additional + computation. The choice is left to the caller; M + must satisfy 1 <= M <= n. +

    +

    + Nota bene: Even at M + = 1 this statistic is not identical to chatterjee_correlation: + it uses min(R_i, R_j) in place of |R_i - R_j| and a different normalisation, + so the two agree only up to a term of order 1/n. Use chatterjee_correlation + when the original coefficient is required. +

    +

    + + Invariants +

    +

    + The function expects at least two samples, a non-constant vector Y, the same + number of X's as Y's, and 1 <= M <= n. If Y is constant, the result is + a quiet NaN. The data set must be sorted by X values. If there are ties in + the values of X, then the statistic is random due to the random breaking of + ties. +

    +

    + + References +

    +
    • + Lin, Zhexiao, and Fang Han. "On boosting the power of Chatterjee's + rank correlation." Biometrika 110.2 (2023): 283-299. arXiv:2108.06828. +
    +
    + +
    +
    +PrevUpHomeNext +
    + + diff --git a/doc/html/math_toolkit/compilers_overview.html b/doc/html/math_toolkit/compilers_overview.html index 25dc38af02..ba43d4f359 100644 --- a/doc/html/math_toolkit/compilers_overview.html +++ b/doc/html/math_toolkit/compilers_overview.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -676,7 +676,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/conventions.html b/doc/html/math_toolkit/conventions.html index 13d9465baa..b63bf9484d 100644 --- a/doc/html/math_toolkit/conventions.html +++ b/doc/html/math_toolkit/conventions.html @@ -28,7 +28,7 @@ Document Conventions

    - +

    This documentation aims to use of the following naming and formatting conventions. diff --git a/doc/html/math_toolkit/cxx20_modules.html b/doc/html/math_toolkit/cxx20_modules.html new file mode 100644 index 0000000000..01d1c3aba2 --- /dev/null +++ b/doc/html/math_toolkit/cxx20_modules.html @@ -0,0 +1,154 @@ + + + +Support for C++20 Modules + + + + + + + + + + + + + + + + +
    Boost C++ LibrariesHomeLibrariesPeopleFAQMore
    +


    +
    +PrevUpHomeNext +
    +
    + +
    + + C++20 + Modules Support +
    +

    + Boost.Math can be consumed as the named module boost.math. The + module covers the public interface of the library: special functions, statistical + distributions, constants, policies, ccmath, the public tools (root finding, + minima, polynomials, rational and series evaluation, norms, condition numbers + and friends), quadrature, the modern interpolators, statistics, optimization, + differentiation (autodiff, finite differences and Lanczos smoothing), the complex + inverse trigonometric functions, and the quaternion and octonion types. +

    +
    import boost.math;
    +
    +int main()
    +{
    +    return boost::math::cdf(boost::math::normal_distribution<>(), 0.0) == 0.5 ? 0 : 1;
    +}
    +
    +

    + Module support is completely inert unless the module is built and consumed + explicitly: nothing changes for existing header users. +

    +
    + + How + to build +
    +

    + The module is consumable from C++20 onward. The interface unit lives in libs/math/module/math.cppm + and builds with any toolchain that provides the standard library module (import std); + the standard library maintainers agreed to make import + std available in C++20 mode as well + as C++23, so a C++23 compilation mode is not required. CI exercises two configurations, + both with CMake 4.4, Ninja and the matching CMAKE_EXPERIMENTAL_CXX_IMPORT_STD + UUID: clang 20 with libc++ runs the full module test suite, and GCC 15 with + libstdc++ builds the module and runs the quick test (libstdc++ cannot yet mix + the textual standard library includes used by the wider test sources with + import std + in the same translation unit). From a Boost superproject checkout: +

    +
    cmake -G Ninja -DBOOST_INCLUDE_LIBRARIES=math -DBUILD_TESTING=ON \
    +  -DBOOST_MATH_BUILD_MODULE=ON \
    +  -DCMAKE_CXX_COMPILER=clang++-20 \
    +  -DCMAKE_CXX_FLAGS="-stdlib=libc++ -Wno-reserved-module-identifier" \
    +  -DCMAKE_EXPERIMENTAL_CXX_IMPORT_STD=<uuid-for-your-cmake-version> \
    +  -DCMAKE_CXX_STDLIB_MODULES_JSON=/usr/lib/llvm-20/lib/libc++.modules.json ..
    +cmake --build . --target tests
    +ctest
    +
    +

    + The module test suite reuses a curated subset of the regular test files, which + switch from textual inclusion to import + boost.math; when + BOOST_MATH_BUILD_MODULE is + defined. The cmake-module-test + job in the library's CI runs this configuration on every commit. +

    +
    + + Limitations +
    +
      +
    • + The module is always built in standalone mode, so it has no dependencies + on other Boost libraries. +
    • +
    • + Macros do not cross module boundaries. The policy configuration macros + (BOOST_MATH_DOMAIN_ERROR_POLICY + and friends) and the convenience macros BOOST_MATH_DECLARE_DISTRIBUTIONS + and BOOST_MATH_DECLARE_SPECIAL_FUNCTIONS + therefore have no effect on an importing translation unit. Configure policies + at the call site with boost::math::policies::make_policy + or a policy<...> + type instead; those interfaces are fully available through the import. +
    • +
    • + The user_error policy is + not supported through the module, because a user supplied boost::math::policies::user_..._error definition cannot attach to the + module. +
    • +
    • + A program should consume Boost.Math either through the module or through + textual includes, not both. The only headers that are safe to include textually + alongside the import are boost/math/tools/config.hpp + and boost/math/tools/assert.hpp, which the test support headers rely + on. +
    • +
    • + Excluded from the module: the TR1/C99 interfaces and common_factor + headers, boost/math/bindings, boost/math/concepts, + boost/math/cstdfloat, all deprecated headers, and + the headers that require external non-Boost dependencies (chebyshev_transform.hpp and interpolators/cardinal_trigonometric.hpp + need FFTW, optimization/cma_es.hpp + needs Eigen). These remain available as ordinary includes. +
    • +
    • + __float128 support (BOOST_MATH_USE_FLOAT128) is not enabled + in the module build. +
    • +
    • + On libc++ the parallel execution policy overloads in statistics compile + out (no <execution> support), matching the ordinary header + behavior on that standard library. +
    • +
    +
    + +
    +
    +PrevUpHomeNext +
    + + diff --git a/doc/html/math_toolkit/dist_ref/dists/chi_squared_dist.html b/doc/html/math_toolkit/dist_ref/dists/chi_squared_dist.html index bd5a433ef4..2a689fc14a 100644 --- a/doc/html/math_toolkit/dist_ref/dists/chi_squared_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/chi_squared_dist.html @@ -52,10 +52,10 @@ // Parameter estimation: BOOST_MATH_GPU_ENABLED static RealType find_degrees_of_freedom( - RealType difference_from_mean, + RealType difference_from_variance, RealType alpha, RealType beta, - RealType sd, + RealType variance, RealType hint = 100); }; diff --git a/doc/html/math_toolkit/dist_ref/dists/geometric_dist.html b/doc/html/math_toolkit/dist_ref/dists/geometric_dist.html index b561b87dd8..39a259678e 100644 --- a/doc/html/math_toolkit/dist_ref/dists/geometric_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/geometric_dist.html @@ -53,11 +53,9 @@ // Bounds on success fraction: BOOST_MATH_GPU_ENABLED static RealType find_lower_bound_on_p( RealType trials, - RealType successes, RealType probability); // alpha BOOST_MATH_GPU_ENABLED static RealType find_upper_bound_on_p( RealType trials, - RealType successes, RealType probability); // alpha // Estimate min/max number of trials: @@ -255,8 +253,8 @@
    Bound on success_fraction Parameter p
    static RealType find_lower_bound_on_p(
    -  RealType failures,
    -  RealType probability) // (0 <= alpha <= 1), 0.05 equivalent to 95% confidence.
    +  RealType trials,
    +  RealType alpha) // (0 <= alpha <= 1), 0.05 equivalent to 95% confidence.
     

    Returns a lower bound on the success fraction: diff --git a/doc/html/math_toolkit/dist_ref/dists/kolmogorov_smirnov_dist.html b/doc/html/math_toolkit/dist_ref/dists/kolmogorov_smirnov_dist.html index b583c90761..06eed5cf7a 100644 --- a/doc/html/math_toolkit/dist_ref/dists/kolmogorov_smirnov_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/kolmogorov_smirnov_dist.html @@ -320,7 +320,7 @@



    [2] diff --git a/doc/html/math_toolkit/dist_ref/dists/nc_f_dist.html b/doc/html/math_toolkit/dist_ref/dists/nc_f_dist.html index ef4c833847..1b0b1c4458 100644 --- a/doc/html/math_toolkit/dist_ref/dists/nc_f_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/nc_f_dist.html @@ -53,6 +53,11 @@ // Accessor to non-centrality parameter lambda: BOOST_MATH_GPU_ENABLED RealType non_centrality()const; + + // Parameter finders: + BOOST_MATH_GPU_ENABLED static RealType find_non_centrality(const RealType x, const RealType v1, const RealType v2, const RealType p); + template <class A, class B, class C, class D> + BOOST_MATH_GPU_ENABLED static RealType find_non_centrality(const complemented3_type<A,B,C,D>& c); }; }} // namespaces @@ -94,6 +99,30 @@

    +
    BOOST_MATH_GPU_ENABLED static RealType find_non_centrality(const RealType x, const RealType v1, const RealType v2, const RealType p);
    +
    +

    + This function returns the non centrality parameter lambda + such that: +

    +

    + cdf(non_central_chi_squared<RealType, Policy>(v1, v2, lambda), x) == + p +

    +
    template <class A, class B, class C, class D>
    +BOOST_MATH_GPU_ENABLED static RealType find_non_centrality(const complemented4_type<A,B,C,D>& c);
    +
    +

    + When called with argument boost::math::complement(x, + v1, + v2, + q) + this function returns the non centrality parameter lambda + such that: +

    +

    + cdf(complement(non_central_chi_squared<RealType, Policy>(v1, v2, lambda), x)) == q. +

    Member diff --git a/doc/html/math_toolkit/dist_ref/dists/negative_binomial_dist.html b/doc/html/math_toolkit/dist_ref/dists/negative_binomial_dist.html index 5fbc8302bd..895fa8248f 100644 --- a/doc/html/math_toolkit/dist_ref/dists/negative_binomial_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/negative_binomial_dist.html @@ -241,7 +241,7 @@
    Bound on Parameter p
    BOOST_MATH_GPU_ENABLED static RealType find_lower_bound_on_p(
    -  RealType failures,
    +  RealType trials,
       RealType successes,
       RealType probability) // (0 <= alpha <= 1), 0.05 equivalent to 95% confidence.
     
    @@ -251,10 +251,9 @@

    -
    failures
    +
    trials

    - The total number of failures before the rth - success. + The total number of trials conducted.

    successes

    diff --git a/doc/html/math_toolkit/dist_ref/dists/students_t_dist.html b/doc/html/math_toolkit/dist_ref/dists/students_t_dist.html index 60557b726a..91e8a332d9 100644 --- a/doc/html/math_toolkit/dist_ref/dists/students_t_dist.html +++ b/doc/html/math_toolkit/dist_ref/dists/students_t_dist.html @@ -56,6 +56,11 @@ RealType beta, RealType sd, RealType hint = 100); + + // degrees of freedom inversion from a quantile and probability: + BOOST_MATH_GPU_ENABLED static RealType find_degrees_of_freedom( + RealType t, + RealType p); }; }} // namespaces @@ -177,6 +182,15 @@

    For more information on this function see the NIST Engineering Statistics Handbook.

    +
    BOOST_MATH_GPU_ENABLED static RealType find_degrees_of_freedom(
    +   RealType t,
    +   RealType p);
    +
    +

    + Returns the degrees of freedom ν such that CDF(x; ν) + = p. Requires 0 < p < 1 + and x ≠ 0, otherwise calls domain_error. +

    Non-member @@ -410,6 +424,19 @@

    + + +

    + find_degrees_of_freedom(t, p) +

    + + +

    + Uses a 2nd-order Edgeworth expansion as initial guess for a numerical + root finder +

    + +

    diff --git a/doc/html/math_toolkit/double_exponential.html b/doc/html/math_toolkit/double_exponential.html index 9a23fcc2dd..2b95c00836 100644 --- a/doc/html/math_toolkit/double_exponential.html +++ b/doc/html/math_toolkit/double_exponential.html @@ -34,6 +34,8 @@ functions with large features near an endpoint with tanh-sinh quadrature

  • sinh_sinh
    exp_sinh
    +
    Vector- and + matrix-valued integrands
    Setting the Termination Condition for Integration
    Setting the diff --git a/doc/html/math_toolkit/double_exponential/de_exp_sinh.html b/doc/html/math_toolkit/double_exponential/de_exp_sinh.html index 94998119b3..2c96433398 100644 --- a/doc/html/math_toolkit/double_exponential/de_exp_sinh.html +++ b/doc/html/math_toolkit/double_exponential/de_exp_sinh.html @@ -7,7 +7,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -111,7 +111,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/double_exponential/de_matrix.html b/doc/html/math_toolkit/double_exponential/de_matrix.html new file mode 100644 index 0000000000..31904a296b --- /dev/null +++ b/doc/html/math_toolkit/double_exponential/de_matrix.html @@ -0,0 +1,119 @@ + + + +Vector- and matrix-valued integrands + + + + + + + + + + + + + + + + +
    Boost C++ LibrariesHomeLibrariesPeopleFAQMore
    +
    +
    +PrevUpHomeNext +
    +
    + +

    + The CPU exp_sinh, sinh_sinh, and tanh_sinh + classes also accept an explicit additive identity and scalar norm. Insert + zero, + norm after the bounds, or after + f when using the default + domain: +

    +
    integrator.integrate(f, zero, norm, tolerance, &error, &L1, &levels);
    +integrator.integrate(f, a, b, zero, norm, tolerance, &error, &L1, &levels);
    +
    +

    + The bounded form is available for exp_sinh + and tanh_sinh; sinh_sinh always integrates over the whole + real line. Arguments after norm + retain their existing defaults. The new overloads participate only if applying + norm to the integrand's result + can be explicitly converted to Real. + Existing overloads remain unchanged. +

    +

    + For example, using Eigen with exp-sinh: +

    +
    using Matrix = Eigen::Matrix<std::complex<double>, 2, 2>;
    +Matrix zero = Matrix::Zero();
    +auto f = [](double x) -> Matrix {
    +    Matrix result;
    +    result << std::exp(-x), 0., 0., std::exp(-2*x);
    +    return result;
    +};
    +auto norm = [](const Matrix& m) { return m.stableNorm(); };
    +boost::math::quadrature::exp_sinh<double> integrator;
    +Matrix result = integrator.integrate(f, zero, norm);
    +
    +

    + Return concrete owning values, with dimensions matching zero, + rather than expression templates referencing temporary values. The value + type must support addition, addition assignment, unary negation, scalar multiplication + on either side, and multiplication assignment by a scalar. The norm must + be nonnegative, vanish only at zero, and return a scalar convertible to + Real. It should also report + non-finite values for non-finite inputs; endpoint checks use this property. + A norm implementation that avoids intermediate overflow and underflow is + useful for the large dynamic ranges encountered near endpoints. +

    +

    + The selected norm is used for successive-estimate errors, the L1 integral, + tail truncation, and checks for non-finite values. Convergence compares the + error with tolerance * + L1; it does not impose a separate + relative tolerance on every entry. Error estimates are not rigorous bounds, + and exhausting refinement levels does not guarantee that the requested tolerance + was reached. +

    +

    + Both forms of the two-argument tanh-sinh integrand are supported: f(x, xc) + may return a matrix, while x + and the signed endpoint distance xc + remain scalars. For the new bounded tanh-sinh overloads, equal bounds return + zero and set requested error, + L1, and level outputs to zero without calling f. + Reversed finite bounds negate the integral; the two-argument functor receives + distances for the endpoints in increasing order. Exp-sinh retains its requirement + for a half-infinite domain. +

    +

    + Domain errors use the scalar policy. If it returns, the result is zero times its scalar error value; requested + error and L1 outputs receive that value, and levels is set to zero. Evaluation-error + policies receive the scalar norm of the problematic value; a non-throwing + evaluation policy returns that concrete value or current estimate, as appropriate. + Diagnostic outputs are not specified after an evaluation error. GPU free-function + interfaces are unchanged. +

    +
    + +
    +
    +PrevUpHomeNext +
    + + diff --git a/doc/html/math_toolkit/double_exponential/de_tol.html b/doc/html/math_toolkit/double_exponential/de_tol.html index 2cdf33c6dd..e83489f61a 100644 --- a/doc/html/math_toolkit/double_exponential/de_tol.html +++ b/doc/html/math_toolkit/double_exponential/de_tol.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -69,7 +69,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/gauss.html b/doc/html/math_toolkit/gauss.html index 81e111932e..544ee51ccb 100644 --- a/doc/html/math_toolkit/gauss.html +++ b/doc/html/math_toolkit/gauss.html @@ -131,6 +131,66 @@

    + Vector- + and matrix-valued integrands +

    +

    + Additional overloads accept an explicit additive identity and a scalar-valued + norm: +

    +
    template <class F, class Norm>
    +static auto integrate(F f, const decltype(f(Real(0)))& zero, Norm norm,
    +                      Real* pL1 = nullptr) -> decltype(f(Real(0)));
    +
    +template <class F, class Norm>
    +static auto integrate(F f, Real a, Real b,
    +                      const decltype(f(Real(0)))& zero, Norm norm,
    +                      Real* pL1 = nullptr) -> decltype(f(Real(0)));
    +
    +

    + These overloads participate in overload resolution only when norm(f(Real(0))) is convertible + to Real by an explicit cast. + Existing overloads are unchanged. +

    +

    + The integrand must return a concrete value type supporting addition, addition + assignment, unary negation, and multiplication by Real + on either side. All returned values and zero + must have compatible dimensions. In particular, return an owning matrix rather + than an unevaluated expression referencing temporaries. norm + must return a nonnegative scalar norm convertible to Real. + If requested, pL1 receives + the quadrature approximation to the integral of norm(f(x)) over + the interval in increasing order. This is not an error estimate. +

    +

    + For example, with Eigen available: +

    +
    using Matrix = Eigen::Matrix<std::complex<double>, 2, 2>;
    +Matrix zero = Matrix::Zero();
    +auto f = [](double x) -> Matrix {
    +    Matrix result;
    +    result << x, 0., 0., x*x;
    +    return result;
    +};
    +auto norm = [](const Matrix& m) { return m.norm(); };
    +Matrix result = boost::math::quadrature::gauss<double, 10>::integrate(
    +    f, 0., 1., zero, norm);
    +
    +

    + The explicit zero also allows dynamically sized matrices: initialize it with + the required dimensions. For equal bounds the supplied zero is returned, *pL1 is set + to zero if requested, and the integrand is not evaluated. +

    +

    + Invalid bounds invoke the existing scalar domain-error policy. If that policy + returns a scalar error value instead of throwing, the result is zero multiplied by that value, and *pL1 receives + that value if requested. For floating-point Eigen matrices and the ignore-error + policy, this produces a matrix of NaNs with the supplied dimensions. Custom + value types must support this multiplication too. +

    +

    + Choosing the number of points

    @@ -163,7 +223,7 @@

    - + Examples

    diff --git a/doc/html/math_toolkit/gauss_kronrod.html b/doc/html/math_toolkit/gauss_kronrod.html index 9fc69aae2a..f774090387 100644 --- a/doc/html/math_toolkit/gauss_kronrod.html +++ b/doc/html/math_toolkit/gauss_kronrod.html @@ -84,6 +84,59 @@

    + Vector- + and matrix-valued integrands +

    +

    + An additional overload accepts an explicit additive identity and a scalar norm: +

    +
    template <class F, class Norm>
    +static auto integrate(F f, Real a, Real b,
    +                      const decltype(f(a))& zero, Norm norm,
    +                      unsigned max_depth = 15,
    +                      Real tol = tools::root_epsilon<Real>(),
    +                      Real* error = nullptr, Real* pL1 = nullptr)
    +    -> decltype(f(a));
    +
    +

    + This overload participates only when norm(f(a)) can be + explicitly converted to Real. + Existing overloads and their behavior are unchanged. +

    +

    + As with the Gauss overloads, f must return a concrete value type supporting + addition, addition assignment, unary negation, and scalar multiplication on + either side. The supplied zero and all integrand values must have compatible + dimensions. The norm must be nonnegative and convertible to Real. + For example, with Eigen: +

    +
    Eigen::MatrixXcd zero = Eigen::MatrixXcd::Zero(rows, cols);
    +auto norm = [](const Eigen::MatrixXcd& m) { return m.norm(); };
    +Eigen::MatrixXcd result = boost::math::quadrature::gauss_kronrod<double, 15>::integrate(
    +    f, a, b, zero, norm, 15, 1e-10);
    +
    +

    + The supplied norm is used for the Gauss-Kronrod difference, the roundoff floor, + the relative convergence test, and the L1 integral. Tolerance therefore applies + to that norm, not separately to each matrix entry. error + is an estimate, not a rigorous bound, and reaching max_depth + does not guarantee the requested tolerance. In this overload, local error estimates + are scaled by interval width, and by the additional factor in the half-infinite + substitutions, to match the units of the returned integral. Reversing finite + bounds negates the integral but leaves error and L1 nonnegative. +

    +

    + Equal bounds return the supplied zero and set requested error and L1 outputs + to zero without evaluating f. + Invalid bounds invoke the scalar domain-error policy; if it returns instead + of throwing, the result is zero + multiplied by the policy's scalar error value, and requested error and L1 outputs + receive that value. For floating-point Eigen matrices the standard ignore-error + policy thus produces a matrix of NaNs with the supplied dimensions. No Eigen + dependency is added to Boost.Math headers. +

    +

    + Description

    static const RandomAccessContainer& abscissa();
    @@ -130,7 +183,7 @@ 

    the result is likely to be ill-conditioned.

    - + Choosing the number of points

    @@ -169,7 +222,7 @@

    - + Complex Quadrature

    @@ -179,7 +232,7 @@

    and the complex type is deduced via the return type of the function.

    - + Examples

    @@ -230,7 +283,7 @@

    We still achieve 1e-15 precision, with an error estimate of 1e-10.

    - + Caveats

    @@ -251,7 +304,7 @@

    integration schemes which are generally much more robust.

    - + References

      diff --git a/doc/html/math_toolkit/gpu.html b/doc/html/math_toolkit/gpu.html index 4416b66463..c75087f19a 100644 --- a/doc/html/math_toolkit/gpu.html +++ b/doc/html/math_toolkit/gpu.html @@ -7,7 +7,7 @@ - + @@ -21,7 +21,7 @@
      -PrevUpHomeNext +PrevUpHomeNext

      @@ -43,6 +43,22 @@

      + Library + owned kernels +
      +

      + One facility launches its own kernels instead of being called from a user's + kernel: the prime sieve (boost::math::prime_sieve, prime_range, + prime_count) accepts boost::math::execution::cuda as + its execution policy. When the translation unit is compiled by nvcc with BOOST_MATH_ENABLE_CUDA defined, the header + <boost/math/special_functions/prime_sieve.hpp> defines + BOOST_MATH_HAS_CUDA_PRIME_SIEVE + and sieves the requested range on the device, managing the device memory and + the kernel launches itself. Without nvcc the same call runs on the CPU. See + the prime sieve documentation for details and measurements. +

      +
      + Policies

      @@ -51,7 +67,7 @@

      is run on device it will be ignored.

      - + How to build with device support
      @@ -99,6 +115,10 @@
      }); } +

      + When using SYCL you must define BOOST_MATH_ENABLE_SYCL, since device support + is opt-in on that platform. For NVCC and NVRTC device support is enabled automatically. +

      Once your kernel function has been written then use the framework mechanism for launching the kernel. @@ -115,7 +135,7 @@


      -PrevUpHomeNext +PrevUpHomeNext
      diff --git a/doc/html/math_toolkit/history1.html b/doc/html/math_toolkit/history1.html index 5eba9e040e..d34c1ea5a8 100644 --- a/doc/html/math_toolkit/history1.html +++ b/doc/html/math_toolkit/history1.html @@ -64,6 +64,10 @@
    • Lots of minor fixes and improved code coverage.
    • +
    • + Add support for the M nearest-neighbour Chatterjee correlation of Lin and + Han (2021), see 990. +
    diff --git a/doc/html/math_toolkit/history2.html b/doc/html/math_toolkit/history2.html index d29d142e0b..2333caf972 100644 --- a/doc/html/math_toolkit/history2.html +++ b/doc/html/math_toolkit/history2.html @@ -64,6 +64,10 @@
  • Lots of minor fixes and improved code coverage.
  • +
  • + Add support for the M nearest-neighbour Chatterjee correlation of Lin and + Han (2021), see 990. +
  • diff --git a/doc/html/math_toolkit/internals/simple_continued_fraction.html b/doc/html/math_toolkit/internals/simple_continued_fraction.html index 82cbbe9e07..527ee2f98d 100644 --- a/doc/html/math_toolkit/internals/simple_continued_fraction.html +++ b/doc/html/math_toolkit/internals/simple_continued_fraction.html @@ -40,6 +40,8 @@ Real khinchin_harmonic_mean() const; + const std::vector<Z>& partial_denominators() const; + template<typename T, typename Z_> friend std::ostream& operator<<(std::ostream& out, simple_continued_fraction<T, Z>& scf); }; @@ -66,6 +68,50 @@ with the modified Lentz's algorithm. Once a convergent is within a few ulps of the input value, the computation stops.

    +

    + Finite simple continued fractions are not unique: [a0; a1, ..., an, 1] + and [a0; a1, + ..., an + + 1] represent the same value. The class uses + the shorter, canonical representation, absorbing a trailing 1 into the preceding coefficient. For example, + 3.75 is represented as [3; + 1, 3], rather + than [3; 1, + 2, 1]. +

    +

    + The partial_denominators() member function returns a read-only reference + to the stored coefficients, including the integer part at index zero: +

    +
    const auto& a = cfrac.partial_denominators();
    +std::cout << a.size() << " coefficients\n";
    +std::cout << a[0] << "\n";    // Integer part: 3
    +std::cout << a.at(1) << "\n"; // First partial denominator: 7
    +
    +

    + The reference remains valid for the lifetime of cfrac. +

    +

    + A prefix of these coefficients defines a convergent, which can be used as + a rational approximation. For example, the first four coefficients of the + expansion of π give 355/113. With #include + <boost/rational.hpp>, + this can be computed by evaluating the prefix backwards: +

    +
    using rational = boost::rational<std::int64_t>;
    +std::size_t n = 4; // Use the first four coefficients of cfrac above.
    +rational approximation(a.at(n - 1));
    +for (std::size_t i = n - 1; i > 0; --i) {
    +    approximation = rational(a[i - 1]) + rational(1) / approximation;
    +}
    +std::cout << approximation << "\n"; // Prints: 355/113
    +
    +

    + In general, choose n between + 1 and a.size() + and an integer type wide enough for the rational arithmetic, including intermediate + results. Longer prefixes can overflow a fixed-width integer type. +

    Note that every floating point number is a rational number, and this exact rational can be exactly converted to a finite continued fraction. This is diff --git a/doc/html/math_toolkit/navigation.html b/doc/html/math_toolkit/navigation.html index 7ae0278709..0fa7e651d4 100644 --- a/doc/html/math_toolkit/navigation.html +++ b/doc/html/math_toolkit/navigation.html @@ -28,7 +28,7 @@ Navigation

    - +

    Boost.Math documentation is provided in both HTML and PDF formats. diff --git a/doc/html/math_toolkit/number_series.html b/doc/html/math_toolkit/number_series.html index 11e894ad39..7212af631c 100644 --- a/doc/html/math_toolkit/number_series.html +++ b/doc/html/math_toolkit/number_series.html @@ -32,6 +32,7 @@ Numbers

    Tangent Numbers
    Prime Numbers
    +
    Prime Sieve
    Fibonacci Numbers
    diff --git a/doc/html/math_toolkit/number_series/fibonacci_numbers.html b/doc/html/math_toolkit/number_series/fibonacci_numbers.html index 27117b9d0d..43d40bea1d 100644 --- a/doc/html/math_toolkit/number_series/fibonacci_numbers.html +++ b/doc/html/math_toolkit/number_series/fibonacci_numbers.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -190,7 +190,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/number_series/prime_sieve.html b/doc/html/math_toolkit/number_series/prime_sieve.html new file mode 100644 index 0000000000..3b36853779 --- /dev/null +++ b/doc/html/math_toolkit/number_series/prime_sieve.html @@ -0,0 +1,964 @@ + + + +Prime Sieve + + + + + + + + + + + + + + + + +
    Boost C++ LibrariesHomeLibrariesPeopleFAQMore
    +
    +
    +PrevUpHomeNext +
    +
    + +
    + + Synopsis +
    +
    #include <boost/math/special_functions/prime_sieve.hpp>
    +
    +
    namespace boost { namespace math {
    +
    +namespace execution {
    +   struct cuda_policy {};
    +   inline constexpr cuda_policy cuda {};
    +}
    +
    +enum class prime_range_strategy { automatic, full_sieve, test_survivors };
    +
    +struct prime_sieve_options
    +{
    +   std::size_t l1d_bytes {0};          // 0 = detect
    +   std::size_t l2_bytes {0};           // 0 = detect (per core)
    +   std::size_t sieve_bytes {0};        // 0 = derive from sqrt(upper) and the caches; clamped to [16 KiB, 8 MiB]
    +   unsigned max_threads {0};           // 0 = std::thread::hardware_concurrency()
    +   std::size_t chunk_primes {1u << 19};  // primes per parallel chunk when storing output
    +   prime_range_strategy range_strategy {prime_range_strategy::automatic};
    +   bool probable_prime_only {false};   // beyond 2^64: Baillie-PSW only, skip the pseudosquares test
    +};
    +
    +// primes in [2, upper_bound)
    +template <class ExecutionPolicy, class Integer, class OutputIterator>
    +OutputIterator prime_sieve(ExecutionPolicy&& policy, Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {});
    +template <class Integer, class OutputIterator>
    +OutputIterator prime_sieve(Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {});
    +template <class ExecutionPolicy, class Integer, class T, class Alloc>
    +void prime_sieve(ExecutionPolicy&& policy, Integer upper_bound, std::vector<T, Alloc>& out, const prime_sieve_options& options = {});
    +template <class Integer, class T, class Alloc>
    +void prime_sieve(Integer upper_bound, std::vector<T, Alloc>& out, const prime_sieve_options& options = {});
    +
    +// primes in [lower_bound, upper_bound); the bounds may have different integer types
    +template <class ExecutionPolicy, class Lower, class Upper, class OutputIterator>
    +OutputIterator prime_range(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {});
    +template <class Lower, class Upper, class OutputIterator>
    +OutputIterator prime_range(Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {});
    +template <class ExecutionPolicy, class Lower, class Upper, class T, class Alloc>
    +void prime_range(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, std::vector<T, Alloc>& out, const prime_sieve_options& options = {});
    +template <class Lower, class Upper, class T, class Alloc>
    +void prime_range(Lower lower_bound, Upper upper_bound, std::vector<T, Alloc>& out, const prime_sieve_options& options = {});
    +
    +// number of primes in [2, upper_bound) and in [lower_bound, upper_bound)
    +template <class ExecutionPolicy, class Integer>
    +std::uint64_t prime_count(ExecutionPolicy&& policy, Integer upper_bound, const prime_sieve_options& options = {});
    +template <class ExecutionPolicy, class Lower, class Upper>
    +std::uint64_t prime_count(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {});
    +template <class Integer>
    +std::uint64_t prime_count(Integer upper_bound, const prime_sieve_options& options = {});
    +template <class Lower, class Upper>
    +std::uint64_t prime_count(Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {});
    +
    +// reserves room for all primes below upper_bound
    +template <class Integer, class T, class Alloc>
    +void prime_reserve(Integer upper_bound, std::vector<T, Alloc>& primes);
    +
    +}} // namespaces
    +
    +
    + + Description +
    +

    + These functions require C++17. +

    +

    + prime_sieve produces the + primes in the half-open range [2, upper_bound), + prime_range the primes in + [lower_bound, upper_bound), and prime_count + returns how many primes such a range contains without storing them. Output + is always in ascending order. The overloads taking a std::vector + append to it and are the fastest way to store the primes; the output iterator + overloads return the advanced iterator. A lower_bound + below 2 is treated as 2, negative bounds as 0, and an empty range produces + nothing. A negative bound is clamped in its own type, so mixing a signed and + an unsigned bound is safe. +

    +

    + Integer may be any builtin + integer type or a Boost.Multiprecision integer such as cpp_int + or mpz_int. Bounds that fit + in 64 bits are sieved with a segmented sieve of Eratosthenes in the style + of primesieve: + one byte represents 30 integers (the eight residues coprime to 30), multiples + of the primes up to 163 are removed by pre-computed periodic patterns, and + the sieving primes are split into three classes (many multiples per segment, + a few, or at most one) that use a mod 30 wheel with unrolled loops, lists + sorted by wheel position, and Oliveira e Silva's bucket sieve (fixed 8 KiB + buckets from a memory pool) respectively. Segments are sized to the L1 and + L2 data caches, which are detected on Linux and macOS and can be overridden + through prime_sieve_options; + when big primes are present the largest segment (8 MiB) is used. The wheel + tables and the 16 pre-sieve patterns (123 KB) are computed at compile time + with constexpr on GCC and Clang; + MSVC and nvcc, whose constexpr step limits are lower, generate the patterns + on first use (define BOOST_MATH_PRIME_SIEVE_RUNTIME_PRESIEVE + to force that everywhere). +

    +

    + Short intervals at large magnitudes (roughly when the interval is narrower + than sqrt(upper_bound) + / ln(upper_bound)) switch automatically to a window method: + the interval is sieved by the primes up to a modest depth and the survivors + are tested with a Baillie-PSW test in Montgomery arithmetic, which is deterministic + below 264. prime_range_strategy + forces either method. +

    +

    + Bounds beyond 264 use the same window method with the Lukes-Patterson-Williams + pseudosquares test (Sorenson, The pseudosquares prime sieve, + 2006), which is deterministic while upper_bound + divided by the sieving depth stays below the largest tabulated pseudosquare + (about 2.95 * 1023), and Baillie-PSW beyond that, for which no counterexample + is known. Setting probable_prime_only + uses Baillie-PSW everywhere and is roughly ten times faster. This path is + intended for windows of up to about 108 integers; the head of a range that + straddles 264 is still processed by the 64-bit engine. +

    +
    + + Execution + policies +
    +

    + The policy overloads accept the std::execution + policies and boost::math::execution::cuda: +

    +
    ++++ + + + + + + + + + + + + + + + + + + +
    +

    + Policy +

    +
    +

    + Effect +

    +
    +

    + std::execution::seq, std::execution::unseq, + or no policy +

    +
    +

    + Single threaded. +

    +
    +

    + std::execution::par, std::execution::par_unseq +

    +
    +

    + The range is cut into chunks of about 200 * sqrt(upper_bound) integers that are + handed to max_threads + worker threads through an atomic counter; each worker owns its + sieve and shares the read-only sieving primes. Output is committed + in order, so the result is identical to the sequential one. +

    +
    +

    + boost::math::execution::cuda +

    +
    +

    + When the translation unit is compiled by nvcc with BOOST_MATH_ENABLE_CUDA defined, + the range is sieved on the GPU (BOOST_MATH_HAS_CUDA_PRIME_SIEVE + is defined in that case). Otherwise the call runs on the CPU with + the parallel policy. +

    +
    +

    + The std::execution overloads are available when + the standard library provides the policy types (libstdc++ 9 and later, MSVC, + and libc++ with -fexperimental-library); + no parallel algorithms and therefore no TBB are required. Threads are never + used when Boost.Math is configured without thread support. +

    +
    + + Examples +
    +
    #include <boost/math/special_functions/prime_sieve.hpp>
    +#include <boost/multiprecision/cpp_int.hpp>
    +#include <execution>
    +#include <vector>
    +#include <list>
    +#include <iterator>
    +#include <cstdint>
    +
    +// All primes below one billion, using every core
    +std::vector<std::uint64_t> primes;
    +boost::math::prime_reserve(1'000'000'000, primes);
    +boost::math::prime_sieve(std::execution::par, 1'000'000'000, primes);
    +
    +// Count the primes in [10^18, 10^18 + 10^9) on one thread
    +std::uint64_t n = boost::math::prime_count(1'000'000'000'000'000'000ull, 1'000'000'001'000'000'000ull);
    +
    +// Primes just above 2^64 with an arbitrary precision type
    +using boost::multiprecision::cpp_int;
    +std::vector<cpp_int> big;
    +boost::math::prime_range(cpp_int(1) << 64, (cpp_int(1) << 64) + 1'000'000, big);
    +
    +// A range through any output iterator
    +std::list<int> small;
    +boost::math::prime_range(100, 1000, std::back_inserter(small));
    +
    +
    + + Performance +
    +

    + Measured with the Google Benchmark harness in reporting/performance/prime_sieve_performance.cpp on + an Apple M4 Max (12 performance and 4 efficiency cores, 128 KiB L1D), Apple + clang 17, -O3 + -march=native, + against primesieve 12.16 built from source on the same machine. Times are + wall clock in milliseconds. +

    +
    +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    + Task +

    +
    +

    + boost::math, 1 thread +

    +
    +

    + primesieve, 1 thread +

    +
    +

    + boost::math, 16 threads +

    +
    +

    + primesieve, 16 threads +

    +
    +

    + count primes below 108 +

    +
    +

    + 2.6 +

    +
    +

    + 3.2 +

    +
    +

    + 0.8 +

    +
    +
    +

    + count primes below 109 +

    +
    +

    + 37 +

    +
    +

    + 44 +

    +
    +

    + 5.4 +

    +
    +

    + 5 +

    +
    +

    + count primes below 1010 +

    +
    +

    + 495 +

    +
    +

    + 566 +

    +
    +

    + 63 +

    +
    +

    + 56 +

    +
    +

    + count primes below 1011 +

    +
    +

    + 6660 +

    +
    +

    + 7620 +

    +
    +

    + 744 +

    +
    +

    + 654 +

    +
    +

    + store primes below 109 +

    +
    +

    + 89 +

    +
    +

    + 84 +

    +
    +

    + 23 +

    +
    +
    +

    + store primes below 1010 +

    +
    +

    + 1051 +

    +
    +

    + 1027 +

    +
    +

    + 286 +

    +
    +
    +

    + count a 109 window at 1012 +

    +
    +

    + 91 +

    +
    +

    + 108 +

    +
    + +
    +

    + count a 109 window at 1015 +

    +
    +

    + 167 +

    +
    +

    + 267 +

    +
    + +
    +

    + count a 109 window at 1018 +

    +
    +

    + 619 +

    +
    +

    + 732 +

    +
    + +
    +

    + The same comparison on an Intel Core i9-11900K (8 cores, 48 KiB L1D, 512 + KiB L2 per core) with GCC 13, -O3 -march=native, + primesieve 12.16 built from source there: +

    +
    +++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    + Task +

    +
    +

    + boost::math, 1 thread +

    +
    +

    + primesieve, 1 thread +

    +
    +

    + boost::math, 16 threads +

    +
    +

    + primesieve, 16 threads +

    +
    +

    + count primes below 109 +

    +
    +

    + 53 +

    +
    +

    + 51 +

    +
    + +
    +

    + count primes below 1010 +

    +
    +

    + 655 +

    +
    +

    + 640 +

    +
    +

    + 124 +

    +
    +

    + 146 +

    +
    +

    + count primes below 1011 +

    +
    + +

    + 8392 +

    +
    +

    + 1767 +

    +
    +

    + 1865 +

    +
    +

    + store primes below 109 +

    +
    +

    + 79 +

    +
    +

    + 70 +

    +
    + +
    +

    + count a 109 window at 1012 +

    +
    +

    + 128 +

    +
    +

    + 124 +

    +
    + +
    +

    + count a 109 window at 1015 +

    +
    +

    + 273 +

    +
    +

    + 224 +

    +
    + +
    +

    + count a 109 window at 1018 +

    +
    +

    + 1037 +

    +
    +

    + 796 +

    +
    + +
    +

    + Single threaded this library is 10 to 15 percent ahead on Apple silicon and + within 2 to 3 percent of primesieve on x86 for whole ranges, while primesieve + keeps a lead of 20 to 30 percent on x86 for windows at 1015 and beyond (the + bucket sieve is memory bound there and the Intel machine has a third of the + memory bandwidth). With all cores this library is ahead on both machines. + When compiled with AVX-512 VBMI2 support (-march=native on Ice Lake or Zen 4 and later) + primes are extracted from the sieve with byte compression instructions; define + BOOST_MATH_PRIME_SIEVE_NO_SIMD + to keep the portable loop. +

    +

    + Both sieves are O(n log log n) in time and use O(sqrt(n)) memory for the + sieving primes (about four bytes per prime here) plus the segment; the multithreaded + path additionally buffers at most 2 * max_threads + * chunk_primes output values. +

    +

    + The CUDA backend was measured on an NVIDIA GeForce RTX 3060 (CUDA 13.1, + nvcc -O3 -arch=sm_86) + against the same code running with std::execution::par on + that machine's Intel Core i9-11900K (8 cores, 16 threads). It stores 16 bytes + per sieving prime on the device, so ranges up to 264 +need about 3.3 GB of + device memory. +

    +
    +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    + Task +

    +
    +

    + execution::cuda, RTX 3060 +

    +
    +

    + std::execution::par, i9-11900K +

    +
    +

    + count primes below 109 +

    +
    +

    + 8 +

    +
    +

    + 11 +

    +
    +

    + count primes below 1010 +

    +
    +

    + 72 +

    +
    +

    + 136 +

    +
    +

    + count primes below 1011 +

    +
    +

    + 1077 +

    +
    +

    + 1786 +

    +
    +

    + count a 109 window at 1012 +

    +
    +

    + 13 +

    +
    +

    + 29 +

    +
    +

    + count a 109 window at 1015 +

    +
    +

    + 75 +

    +
    +

    + 136 +

    +
    +

    + count a 109 window at 1018 +

    +
    +

    + 656 +

    +
    +

    + 691 +

    +
    +

    + count a 230 window below 264 +

    +
    +

    + 2371 +

    +
    +

    + 2995 +

    +
    +

    + store primes below 1010 +

    +
    +

    + 1485 +

    +
    +

    + 430 +

    +
    +

    + Counting is where the GPU pays off; storing primes is dominated by the transfer + and compaction of the output (3.6 GB for the primes below 1010) and is faster + on the CPU. Windows near 264 are dominated by generating the 203 million sieving + primes on the host. +

    +
    + + References +
    +
    +
    + +
    +
    +PrevUpHomeNext +
    + + diff --git a/doc/html/math_toolkit/number_series/primes.html b/doc/html/math_toolkit/number_series/primes.html index 99392e41c0..bc7d304f94 100644 --- a/doc/html/math_toolkit/number_series/primes.html +++ b/doc/html/math_toolkit/number_series/primes.html @@ -7,7 +7,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -81,7 +81,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/sf_gamma/igamma.html b/doc/html/math_toolkit/sf_gamma/igamma.html index 1fe87286b1..93e467594e 100644 --- a/doc/html/math_toolkit/sf_gamma/igamma.html +++ b/doc/html/math_toolkit/sf_gamma/igamma.html @@ -47,6 +47,18 @@
    template <class T1, class T2, class Policy> BOOST_MATH_GPU_ENABLED calculated-result-type gamma_q(T1 a, T2 z, const Policy&); +template <class T1, class T2> +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_q(T1 a, T2 z); + +template <class T1, class T2, class Policy> +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_q(T1 a, T2 z, const Policy&); + +template <class T1, class T2> +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_p(T1 a, T2 z); + +template <class T1, class T2, class Policy> +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_p(T1 a, T2 z, const Policy&); + template <class T1, class T2> BOOST_MATH_GPU_ENABLED calculated-result-type tgamma_lower(T1 a, T2 z); @@ -109,6 +121,16 @@

    template <class T1, class T2>
    +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_p(T1 a, T2 z);
    +
    +template <class T1, class T2, class Policy>
    +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_p(T1 a, T2 z, const Policy&);
    +
    +

    + Returns the natural log of the normalized lower incomplete gamma function + of a and z. +

    +
    template <class T1, class T2>
     BOOST_MATH_GPU_ENABLED calculated-result-type gamma_q(T1 a, T2 z);
     
     template <class T1, class T2, class Policy>
    @@ -129,6 +151,16 @@ 

    template <class T1, class T2>
    +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_q(T1 a, T2 z);
    +
    +template <class T1, class T2, class Policy>
    +BOOST_MATH_GPU_ENABLED calculated-result-type lgamma_q(T1 a, T2 z, const Policy&);
    +
    +

    + Returns the natural log of the normalized upper incomplete gamma function + of a and z. +

    +
    template <class T1, class T2>
     BOOST_MATH_GPU_ENABLED calculated-result-type tgamma_lower(T1 a, T2 z);
     
     template <class T1, class T2, class Policy>
    @@ -973,6 +1005,18 @@ 
    other methods. Use of log(1+x)-x here is inspired by Temme (see references below).

    +

    + The natural log of the normalized upper incomplete gamma function is computed + as expected except when the normalized upper incomplete gamma function begins + to underflow. This approximately occurs at +

    +
    ((x > 1000) && ((a < x) || (fabs(a - 50) / x < 1))) || ((x > log_max_value<T>() - 10) && (x > a))
    +
    +

    + in which case an expansion, for large x, of the (non-normalised) upper incomplete + gamma function is used. The return is then normalised by subtracting the + log of the gamma function and adding a log(x)-x-log(x). +

    References diff --git a/doc/html/math_toolkit/sf_gamma/polygamma.html b/doc/html/math_toolkit/sf_gamma/polygamma.html index e2eed2bf06..913a653f83 100644 --- a/doc/html/math_toolkit/sf_gamma/polygamma.html +++ b/doc/html/math_toolkit/sf_gamma/polygamma.html @@ -347,7 +347,7 @@

    The coefficients of the cosine terms can be calculated iteratively starting - from C1,0 = -1 and then using + from C0,1 = -1 and then using

    diff --git a/doc/html/math_toolkit/trapezoidal.html b/doc/html/math_toolkit/trapezoidal.html index f49eac7f2b..c0471b74e9 100644 --- a/doc/html/math_toolkit/trapezoidal.html +++ b/doc/html/math_toolkit/trapezoidal.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@


    -PrevUpHomeNext +PrevUpHomeNext

    @@ -49,6 +49,47 @@

    + Vector- + and matrix-valued integrands +

    +

    + An additional overload accepts an explicit zero value and scalar norm: +

    +
    template<class F, class Real, class Norm>
    +auto trapezoidal(F f, Real a, Real b, const decltype(f(a))& zero, Norm norm,
    +    Real tol = boost::math::tools::root_epsilon<Real>(),
    +    std::size_t max_refinements = 12,
    +    Real* error_estimate = nullptr, Real* L1 = nullptr) -> decltype(f(a));
    +
    +

    + A policy overload accepts the same arguments without defaults, followed by + const Policy& pol. + These overloads participate only if norm(f(a)) can be + explicitly converted to Real. + Existing overloads remain unchanged. +

    +

    + Return a concrete owning vector or matrix, with dimensions matching zero. The value type needs addition, addition + assignment, unary negation, and scalar multiplication. The supplied norm controls + the successive-estimate error and L1 integral, so tolerance is measured against + tol * + L1, not independently for each entry. + For an Eigen matrix, a typical norm is [](const Matrix& m) + { return m.norm(); }. +

    +
    Matrix result = boost::math::quadrature::trapezoidal(
    +    f, a, b, zero, norm, 1e-10);
    +
    +

    + Equal bounds return zero and + set requested error and L1 outputs to zero without calling f. + Reversed bounds negate the integral and preserve nonnegative error and L1 estimates. + Invalid bounds invoke the scalar domain-error policy; if it returns, the result + is zero multiplied by its scalar + error value, and requested error and L1 outputs receive that value. +

    +

    + Description

    @@ -238,7 +279,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/math_toolkit/univariate_statistics.html b/doc/html/math_toolkit/univariate_statistics.html index 06c5738fd7..5e9515f89e 100644 --- a/doc/html/math_toolkit/univariate_statistics.html +++ b/doc/html/math_toolkit/univariate_statistics.html @@ -353,7 +353,7 @@

    four moments

    - Simultaneously computes the first four central + Simultaneously computes the mean and the second, third, and fourth central moments in a single pass through the data:

    std::vector<double> v{1,2,3,4,5};
    diff --git a/doc/html/math_toolkit/van_den_bos_unit_square.html b/doc/html/math_toolkit/van_den_bos_unit_square.html
    new file mode 100644
    index 0000000000..56ca61f325
    --- /dev/null
    +++ b/doc/html/math_toolkit/van_den_bos_unit_square.html
    @@ -0,0 +1,178 @@
    +
    +
    +
    +Van den Bos Unit-Square Quadrature
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Boost C++ LibrariesHomeLibrariesPeopleFAQMore
    +
    +
    +PrevUpHomeNext +
    +
    + +

    + + Synopsis +

    +
    #include <boost/math/quadrature/van_den_bos_unit_square.hpp>
    +namespace boost { namespace math { namespace quadrature {
    +
    +template<class F>
    +auto van_den_bos_unit_square(
    +    F const& f,
    +    double tolerance,
    +    double* error_estimate = nullptr,
    +    double* L1 = nullptr,
    +    std::size_t* evaluations = nullptr);
    +
    +}}} // namespaces
    +
    +

    + + Description +

    +

    + van_den_bos_unit_square computes + an adaptive positive cubature of a real- or complex-valued function over the + reference square [0,1]^2. The integrand receives the two coordinates as separate + double arguments. +

    +

    + For a nondegenerate parallelogram with one vertex p0 and + edge vectors a and b, use the affine + map p(u,v) = p0 + + u a + v + b, where (u,v) lies in [0,1]^2. The integral + over the parallelogram is the integral of f(p0 + + u a + + v b) over the + unit square, multiplied by abs(det(a,b)). The caller must perform this mapping and + include the constant Jacobian abs(det(a,b)) in the integrand. +

    +

    + The precomputed rules contain no points on the boundary of the square, so the + routine does not evaluate the integrand at an endpoint singularity. Interior + nodes approach the boundary as the levels increase, however, so excluding the + endpoints does not by itself make an improper integral well conditioned. Singular + integrals may still require a suitable change of variables. +

    +

    + The precomputed rules form a nested hierarchy. After the first level, only + points newly introduced by the next level are evaluated. The routine requires + two consecutive adjacent-level differences to satisfy tolerance + before stopping, which avoids accepting an isolated cancellation. If the maximum + level is reached first, the last rule is returned and its difference is used + as the final error_estimate + (subject to the roundoff floor). +

    +

    + The optional L1 output is the + weighted sum of the absolute integrand values. evaluations + reports the number of function calls, including calls reused by all subsequent + levels. The supported result types are float + and double, including complex + values with float or double components. Higher-precision return types + are not supported. +

    +

    + Nodes and weights are computed offline. If the requested tolerance is not reached + by the final precomputed level, the routine returns that level's result and + error estimate; it does not compute additional levels. +

    +

    + Requires C++17. +

    +

    + Positive cubature rules satisfying the same moment conditions are not unique. + The nodes and weights of this routine are chosen on a Fejer type-II grid. The + nodes are strictly contained in (0,1)^2, and the + weighted rule is invariant under D4. +

    +

    + + Dilogarithm + example +

    +

    + For a smooth special-function example, the integral of 1/(1-xy/2) over the unit square is pi^2/6 - log^2(2). +

    +
    using boost::math::quadrature::van_den_bos_unit_square;
    +
    +double error_estimate = 0;
    +double value = van_den_bos_unit_square(
    +    [](double x, double y)
    +    {
    +        return 1 / (1 - x * y / 2);
    +    },
    +    1e-10,
    +    &error_estimate);
    +
    +

    + + Square-lattice + Green function +

    +

    + The resolvent of the nearest-neighbor square lattice gives the genuinely two-dimensional + periodic integral of 1/(z - cos(2*pi*x) - cos(2*pi*y)) over the unit square. For z > + 2, its closed form is G(z) + = 2*K(2/z)/(pi*z), where K is the complete + elliptic integral of the first kind. For example, z = 3 + can be computed with +

    +
    double const z = 3;
    +double const two_pi = 2 * boost::math::constants::pi<double>();
    +double value = van_den_bos_unit_square(
    +    [=](double x, double y)
    +    {
    +        return 1 / (z - std::cos(two_pi * x)
    +                      - std::cos(two_pi * y));
    +    },
    +    1e-6);
    +
    +

    + The lattice value z = 2, and the parameter value z + = 1 in the dilogarithm family (1-zxy)^-1, give improper + integrals. Although the rules do not evaluate the singular boundary points + themselves, singular integrands may require a suitable change of variables + and should not be expected to have reliable adjacent-level error estimates + without such treatment. +

    +

    + The construction follows the positive, nested reduction approach of van den Bos et al., adapted + here to the uniform measure on the unit square. +

    +
    + +
    +
    +PrevUpHomeNext +
    + + diff --git a/doc/html/overview.html b/doc/html/overview.html index 7e7e6439ba..522edcf89d 100644 --- a/doc/html/overview.html +++ b/doc/html/overview.html @@ -40,6 +40,7 @@ Result
    Error Handling
    Support for GPU programming in Boost.Math
    +
    Support for C++20 Modules
    Compilers
    Configuration Macros
    Policies
    diff --git a/doc/html/quadrature.html b/doc/html/quadrature.html index 9041b488ae..1268345e76 100644 --- a/doc/html/quadrature.html +++ b/doc/html/quadrature.html @@ -7,7 +7,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -29,6 +29,8 @@
    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/html/special.html b/doc/html/special.html index 543c511377..651ec551df 100644 --- a/doc/html/special.html +++ b/doc/html/special.html @@ -35,6 +35,7 @@ Numbers
    Tangent Numbers
    Prime Numbers
    +
    Prime Sieve
    Fibonacci Numbers
    diff --git a/doc/html/standalone_HTML.manifest b/doc/html/standalone_HTML.manifest index 57ce0d9d84..2b1ef508bc 100644 --- a/doc/html/standalone_HTML.manifest +++ b/doc/html/standalone_HTML.manifest @@ -10,6 +10,7 @@ math_toolkit/standalone.html math_toolkit/result_type.html math_toolkit/error_handling.html math_toolkit/gpu.html +math_toolkit/cxx20_modules.html math_toolkit/compilers_overview.html math_toolkit/config_macros.html math_toolkit/intro_pol_overview.html @@ -168,6 +169,7 @@ math_toolkit/runs_test.html math_toolkit/ljung_box.html math_toolkit/linear_regression.html math_toolkit/chatterjee_correlation.html +math_toolkit/chatterjee_correlation_mnn.html vector_functionals.html math_toolkit/norms.html special.html @@ -175,6 +177,7 @@ math_toolkit/number_series.html math_toolkit/number_series/bernoulli_numbers.html math_toolkit/number_series/tangent_numbers.html math_toolkit/number_series/primes.html +math_toolkit/number_series/prime_sieve.html math_toolkit/number_series/fibonacci_numbers.html math_toolkit/sf_gamma.html math_toolkit/sf_gamma/tgamma.html @@ -357,6 +360,7 @@ math_toolkit/pchip.html math_toolkit/quintic_hermite.html math_toolkit/bilinear_uniform.html quadrature.html +math_toolkit/van_den_bos_unit_square.html math_toolkit/trapezoidal.html math_toolkit/gauss.html math_toolkit/gauss_kronrod.html @@ -366,6 +370,7 @@ math_toolkit/double_exponential/de_tanh_sinh.html math_toolkit/double_exponential/de_tanh_sinh_2_arg.html math_toolkit/double_exponential/de_sinh_sinh.html math_toolkit/double_exponential/de_exp_sinh.html +math_toolkit/double_exponential/de_matrix.html math_toolkit/double_exponential/de_tol.html math_toolkit/double_exponential/de_levels.html math_toolkit/double_exponential/de_thread.html diff --git a/doc/html/statistics.html b/doc/html/statistics.html index f341a83212..c9b2ac3846 100644 --- a/doc/html/statistics.html +++ b/doc/html/statistics.html @@ -39,6 +39,8 @@
    The Ljung-Box Test
    Linear Regression
    Chatterjee Correlation
    +
    Chatterjee Correlation + (M nearest neighbours)

    diff --git a/doc/html/vector_functionals.html b/doc/html/vector_functionals.html index 264af3effe..9e80721ddf 100644 --- a/doc/html/vector_functionals.html +++ b/doc/html/vector_functionals.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@
    -PrevUpHomeNext +PrevUpHomeNext

    @@ -42,7 +42,7 @@


    -PrevUpHomeNext +PrevUpHomeNext
    diff --git a/doc/overview/gpu.qbk b/doc/overview/gpu.qbk index 58df868a63..d08b34ae56 100644 --- a/doc/overview/gpu.qbk +++ b/doc/overview/gpu.qbk @@ -7,6 +7,16 @@ These functions will have the annotation `BOOST_MATH_GPU_ENABLED` or `BOOST_MATH Functions marked with `BOOST_MATH_GPU_ENABLED` are tested using CUDA (both NVCC and NVRTC) as well as SYCL to provide a wide range of support. Functions marked with `BOOST_MATH_CUDA_ENABLED` are few, but due to its restrictions SYCL is unsupported. +[h4 Library owned kernels] + +One facility launches its own kernels instead of being called from a user's kernel: the prime sieve +(`boost::math::prime_sieve`, `prime_range`, `prime_count`) accepts `boost::math::execution::cuda` as +its execution policy. When the translation unit is compiled by nvcc with `BOOST_MATH_ENABLE_CUDA` +defined, the header `` defines +`BOOST_MATH_HAS_CUDA_PRIME_SIEVE` and sieves the requested range on the device, managing the device +memory and the kernel launches itself. Without nvcc the same call runs on the CPU. See the prime sieve +documentation for details and measurements. + [h4 Policies] The default policy on all devices is ignore error due to the lack of throwing ability. diff --git a/doc/sf/number_series.qbk b/doc/sf/number_series.qbk index c47cc877f6..bdd2931411 100644 --- a/doc/sf/number_series.qbk +++ b/doc/sf/number_series.qbk @@ -259,7 +259,225 @@ Passing a value greater than `max_prime` results in a __domain_error being raise This function is `constexpr` only if the compiler supports C++14 constexpr functions. -[endsect] [/section:primes] +[endsect] [/section:primes Prime Numbers] + +[section:prime_sieve Prime Sieve] + +[h4 Synopsis] + +`` +#include +`` + + namespace boost { namespace math { + + namespace execution { + struct cuda_policy {}; + inline constexpr cuda_policy cuda {}; + } + + enum class prime_range_strategy { automatic, full_sieve, test_survivors }; + + struct prime_sieve_options + { + std::size_t l1d_bytes {0}; // 0 = detect + std::size_t l2_bytes {0}; // 0 = detect (per core) + std::size_t sieve_bytes {0}; // 0 = derive from sqrt(upper) and the caches; clamped to [16 KiB, 8 MiB] + unsigned max_threads {0}; // 0 = std::thread::hardware_concurrency() + std::size_t chunk_primes {1u << 19}; // primes per parallel chunk when storing output + prime_range_strategy range_strategy {prime_range_strategy::automatic}; + bool probable_prime_only {false}; // beyond 2^64: Baillie-PSW only, skip the pseudosquares test + }; + + // primes in [2, upper_bound) + template + OutputIterator prime_sieve(ExecutionPolicy&& policy, Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {}); + template + OutputIterator prime_sieve(Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {}); + template + void prime_sieve(ExecutionPolicy&& policy, Integer upper_bound, std::vector& out, const prime_sieve_options& options = {}); + template + void prime_sieve(Integer upper_bound, std::vector& out, const prime_sieve_options& options = {}); + + // primes in [lower_bound, upper_bound); the bounds may have different integer types + template + OutputIterator prime_range(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {}); + template + OutputIterator prime_range(Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {}); + template + void prime_range(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, std::vector& out, const prime_sieve_options& options = {}); + template + void prime_range(Lower lower_bound, Upper upper_bound, std::vector& out, const prime_sieve_options& options = {}); + + // number of primes in [2, upper_bound) and in [lower_bound, upper_bound) + template + std::uint64_t prime_count(ExecutionPolicy&& policy, Integer upper_bound, const prime_sieve_options& options = {}); + template + std::uint64_t prime_count(ExecutionPolicy&& policy, Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {}); + template + std::uint64_t prime_count(Integer upper_bound, const prime_sieve_options& options = {}); + template + std::uint64_t prime_count(Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {}); + + // reserves room for all primes below upper_bound + template + void prime_reserve(Integer upper_bound, std::vector& primes); + + }} // namespaces + +[h4 Description] + +These functions require C++17. + +`prime_sieve` produces the primes in the half-open range \[2, `upper_bound`), `prime_range` the primes in +\[`lower_bound`, `upper_bound`), and `prime_count` returns how many primes such a range contains without +storing them. Output is always in ascending order. The overloads taking a `std::vector` append to it +and are the fastest way to store the primes; the output iterator overloads return the advanced iterator. +A `lower_bound` below 2 is treated as 2, negative bounds as 0, and an empty range produces nothing. +A negative bound is clamped in its own type, so mixing a signed and an unsigned bound is safe. + +`Integer` may be any builtin integer type or a Boost.Multiprecision integer such as `cpp_int` or `mpz_int`. +Bounds that fit in 64 bits are sieved with a segmented sieve of Eratosthenes in the style of +[@https://github.com/kimwalisch/primesieve primesieve]: one byte represents 30 integers (the eight residues +coprime to 30), multiples of the primes up to 163 are removed by pre-computed periodic patterns, and the +sieving primes are split into three classes (many multiples per segment, a few, or at most one) that use a +mod 30 wheel with unrolled loops, lists sorted by wheel position, and Oliveira e Silva's bucket sieve +(fixed 8 KiB buckets from a memory pool) respectively. Segments are sized to the L1 and L2 data caches, +which are detected on Linux and macOS and can be overridden through `prime_sieve_options`; when big +primes are present the largest segment (8 MiB) is used. The wheel tables and the 16 pre-sieve +patterns (123 KB) are computed at compile time with `constexpr` on GCC and Clang; MSVC and nvcc, whose +constexpr step limits are lower, generate the patterns on first use (define +`BOOST_MATH_PRIME_SIEVE_RUNTIME_PRESIEVE` to force that everywhere). + +Short intervals at large magnitudes (roughly when the interval is narrower than `sqrt(upper_bound) / ln(upper_bound)`) +switch automatically to a window method: the interval is sieved by the primes up to a modest depth and the +survivors are tested with a Baillie-PSW test in Montgomery arithmetic, which is deterministic below 2[super 64]. +`prime_range_strategy` forces either method. + +Bounds beyond 2[super 64] use the same window method with the Lukes-Patterson-Williams pseudosquares test +(Sorenson, ['The pseudosquares prime sieve], 2006), which is deterministic while `upper_bound` divided by the +sieving depth stays below the largest tabulated pseudosquare (about 2.95 * 10[super 23]), and Baillie-PSW beyond +that, for which no counterexample is known. Setting `probable_prime_only` uses Baillie-PSW everywhere and is +roughly ten times faster. This path is intended for windows of up to about 10[super 8] integers; the +head of a range that straddles 2[super 64] is still processed by the 64-bit engine. + +[h4 Execution policies] + +The policy overloads accept the `std::execution` policies and `boost::math::execution::cuda`: + +[table +[[Policy][Effect]] +[[`std::execution::seq`, `std::execution::unseq`, or no policy][Single threaded.]] +[[`std::execution::par`, `std::execution::par_unseq`][The range is cut into chunks of about 200 * sqrt(`upper_bound`) integers that are handed to `max_threads` worker threads through an atomic counter; each worker owns its sieve and shares the read-only sieving primes. Output is committed in order, so the result is identical to the sequential one.]] +[[`boost::math::execution::cuda`][When the translation unit is compiled by nvcc with `BOOST_MATH_ENABLE_CUDA` defined, the range is sieved on the GPU (`BOOST_MATH_HAS_CUDA_PRIME_SIEVE` is defined in that case). Otherwise the call runs on the CPU with the parallel policy.]] +] + +The `std::execution` overloads are available when the standard library provides the policy types +(libstdc++ 9 and later, MSVC, and libc++ with `-fexperimental-library`); no parallel algorithms and +therefore no TBB are required. Threads are never used when Boost.Math is configured without thread support. + +[h4 Examples] + + #include + #include + #include + #include + #include + #include + #include + + // All primes below one billion, using every core + std::vector primes; + boost::math::prime_reserve(1'000'000'000, primes); + boost::math::prime_sieve(std::execution::par, 1'000'000'000, primes); + + // Count the primes in [10^18, 10^18 + 10^9) on one thread + std::uint64_t n = boost::math::prime_count(1'000'000'000'000'000'000ull, 1'000'000'001'000'000'000ull); + + // Primes just above 2^64 with an arbitrary precision type + using boost::multiprecision::cpp_int; + std::vector big; + boost::math::prime_range(cpp_int(1) << 64, (cpp_int(1) << 64) + 1'000'000, big); + + // A range through any output iterator + std::list small; + boost::math::prime_range(100, 1000, std::back_inserter(small)); + +[h4 Performance] + +Measured with the Google Benchmark harness in `reporting/performance/prime_sieve_performance.cpp` on an +Apple M4 Max (12 performance and 4 efficiency cores, 128 KiB L1D), Apple clang 17, `-O3 -march=native`, +against primesieve 12.16 built from source on the same machine. Times are wall clock in milliseconds. + +[table +[[Task][`boost::math`, 1 thread][primesieve, 1 thread][`boost::math`, 16 threads][primesieve, 16 threads]] +[[count primes below 10[super 8]][2.6][3.2][0.8][]] +[[count primes below 10[super 9]][37][44][5.4][5]] +[[count primes below 10[super 10]][495][566][63][56]] +[[count primes below 10[super 11]][6660][7620][744][654]] +[[store primes below 10[super 9]][89][84][23][]] +[[store primes below 10[super 10]][1051][1027][286][]] +[[count a 10[super 9] window at 10[super 12]][91][108][][]] +[[count a 10[super 9] window at 10[super 15]][167][267][][]] +[[count a 10[super 9] window at 10[super 18]][619][732][][]] +] + +The same comparison on an Intel Core i9-11900K (8 cores, 48 KiB L1D, 512 KiB L2 per core) with GCC 13, +`-O3 -march=native`, primesieve 12.16 built from source there: + +[table +[[Task][`boost::math`, 1 thread][primesieve, 1 thread][`boost::math`, 16 threads][primesieve, 16 threads]] +[[count primes below 10[super 9]][53][51][][]] +[[count primes below 10[super 10]][655][640][124][146]] +[[count primes below 10[super 11]][][8392][1767][1865]] +[[store primes below 10[super 9]][79][70][][]] +[[count a 10[super 9] window at 10[super 12]][128][124][][]] +[[count a 10[super 9] window at 10[super 15]][273][224][][]] +[[count a 10[super 9] window at 10[super 18]][1037][796][][]] +] + +Single threaded this library is 10 to 15 percent ahead on Apple silicon and within 2 to 3 percent of +primesieve on x86 for whole ranges, while primesieve keeps a lead of 20 to 30 percent on x86 for +windows at 10[super 15] and beyond (the bucket sieve is memory bound there and the Intel machine has +a third of the memory bandwidth). With all cores this library is ahead on both machines. When compiled with +AVX-512 VBMI2 support (`-march=native` on Ice Lake or Zen 4 and later) primes are extracted from the +sieve with byte compression instructions; define `BOOST_MATH_PRIME_SIEVE_NO_SIMD` to keep the portable loop. + +Both sieves are O(n log log n) in time and use O(sqrt(n)) memory for the sieving primes (about +four bytes per prime here) plus the segment; the multithreaded path additionally buffers at most +2 * `max_threads` * `chunk_primes` output values. + +The CUDA backend was measured on an NVIDIA GeForce RTX 3060 (CUDA 13.1, `nvcc -O3 -arch=sm_86`) +against the same code running with `std::execution::par` on that machine's Intel Core i9-11900K +(8 cores, 16 threads). It stores 16 bytes per sieving prime on the device, so ranges up to 2[super 64] +need about 3.3 GB of device memory. + +[table +[[Task][`execution::cuda`, RTX 3060][`std::execution::par`, i9-11900K]] +[[count primes below 10[super 9]][8][11]] +[[count primes below 10[super 10]][72][136]] +[[count primes below 10[super 11]][1077][1786]] +[[count a 10[super 9] window at 10[super 12]][13][29]] +[[count a 10[super 9] window at 10[super 15]][75][136]] +[[count a 10[super 9] window at 10[super 18]][656][691]] +[[count a 2[super 30] window below 2[super 64]][2371][2995]] +[[store primes below 10[super 10]][1485][430]] +] + +Counting is where the GPU pays off; storing primes is dominated by the transfer and compaction +of the output (3.6 GB for the primes below 10[super 10]) and is faster on the CPU. Windows near +2[super 64] are dominated by generating the 203 million sieving primes on the host. + +[h4 References] + +* Kim Walisch, [@https://github.com/kimwalisch/primesieve primesieve] and its [@https://github.com/kimwalisch/primesieve/blob/master/doc/ALGORITHMS.md algorithm notes]. +* Tomas Oliveira e Silva, [@https://sweet.ua.pt/tos/software/prime_sieve.html Fast implementation of the segmented sieve of Eratosthenes]. +* Jonathan Sorenson, [@https://link.springer.com/chapter/10.1007/11792086_15 The pseudosquares prime sieve], ANTS VII, 2006. +* Pierre Dusart, [@https://arxiv.org/abs/1002.0442 Estimates of some functions over primes without R.H.], 2010 (the bound used by `prime_reserve`). +* Robert Baillie and Samuel S. Wagstaff Jr., Lucas pseudoprimes, Mathematics of Computation 35 (1980). + +[endsect] [/section:prime_sieve Prime Sieve] + [section:fibonacci_numbers Fibonacci Numbers] diff --git a/include/boost/math/special_functions/detail/prime_sieve/big_range.hpp b/include/boost/math/special_functions/detail/prime_sieve/big_range.hpp new file mode 100644 index 0000000000..6da0321882 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/big_range.hpp @@ -0,0 +1,324 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Ranges beyond 2^64 and short intervals at large magnitudes: sieve a window of odd +// numbers by the primes up to a modest depth, then test each survivor individually +// (deterministic Miller-Rabin below 2^64, the pseudosquares test while the table +// covers n / depth, Baillie-PSW beyond). Sorenson, "The pseudosquares prime sieve" (2006). + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_BIG_RANGE_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_BIG_RANGE_HPP + +#include + +#ifndef BOOST_MATH_HAS_NVRTC + +#include +#include +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +inline constexpr std::uint64_t window_min_depth {100000}; +inline constexpr std::uint64_t window_max_depth {100000000}; +inline constexpr std::size_t window_max_bits {std::size_t(1) << 24}; // odd candidates per window (2 MiB) + +// The short-interval heuristic for 64-bit ranges: when (width * ln(stop))^2 < stop the +// per-candidate tests are cheaper than generating pi(sqrt(stop)) sieving primes. +inline bool prefer_test_path(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options) noexcept +{ + if (options.range_strategy == prime_range_strategy::full_sieve) + { + return false; + } + if (options.range_strategy == prime_range_strategy::test_survivors) + { + return true; + } + const double width {static_cast(stop - start) + 1.0}; + const double t {width * std::log(static_cast(stop))}; + return t * t < static_cast(stop); +} + +// Sieving depth s balancing pi(s) modular reductions against the tests on the survivors, +// whose density after sieving to s is about 0.56 / ln(s) (Mertens' theorem). +// test_cost is the cost of one primality test relative to one modular reduction. +inline std::uint64_t choose_window_depth(double window_width, double test_cost, std::uint64_t sqrt_upper) noexcept +{ + double s {window_min_depth}; + for (int i {0}; i < 8; ++i) + { + const double ln_s {std::log(s)}; + s = 0.5615 * window_width * test_cost / ln_s; + s = (std::max)(s, static_cast(window_min_depth)); + s = (std::min)(s, static_cast(window_max_depth)); + } + std::uint64_t depth {static_cast(s)}; + depth = (std::min)(depth, sqrt_upper); + depth = (std::max)(depth, std::uint64_t(7)); + return depth; +} + +// All odd primes up to depth. +inline std::vector window_sieving_primes(std::uint64_t depth) +{ + std::vector primes; + for (std::size_t i {1}; i < 71 && small_primes_to_353[i] <= presieve_max_prime && small_primes_to_353[i] <= depth; ++i) + { + primes.push_back(small_primes_to_353[i]); + } + const std::vector rest {sieving_primes_upto(depth)}; + primes.insert(primes.end(), rest.begin(), rest.end()); + return primes; +} + +// Residue of the (odd) window base modulo small primes, specialized per base representation. +struct base_mod_u64 +{ + std::uint64_t base; + + std::uint32_t operator()(std::uint32_t p) const noexcept + { + return static_cast(base % p); + } +}; + +// base = high * 2^64 + low, reduced with 64-bit arithmetic only. +struct base_mod_u128 +{ + std::uint64_t high; + std::uint64_t low; + + std::uint32_t operator()(std::uint32_t p) const noexcept + { + const std::uint64_t two64_mod_p {((std::numeric_limits::max)() % p + 1) % p}; + const std::uint64_t r {((high % p) * two64_mod_p + low % p) % p}; + return static_cast(r); + } +}; + +template +struct base_mod_generic +{ + const Integer& base; + + std::uint32_t operator()(std::uint32_t p) const + { + return static_cast(base % Integer(p)); + } +}; + +// Marks composites among the odd numbers base + 2 i, i < bits, using the given primes. +// A prime equal to a candidate is left standing. +template +void sieve_window(std::vector& composite, std::size_t bits, const BaseMod& base_mod, + const std::vector& primes, std::uint64_t small_base) +{ + composite.assign(bits, 0); + for (const std::uint32_t p : primes) + { + const std::uint32_t r {base_mod(p)}; + // first multiple >= base, made odd + std::uint64_t offset {r == 0 ? 0 : static_cast(p - r)}; + if (offset & 1u) + { + offset += p; + } + const std::uint64_t square {static_cast(p) * p}; + if (small_base != 0 && small_base < square) + { + // p * p is an odd multiple of p at or above the base, so this only moves the start up + offset = square - small_base; + } + for (std::uint64_t i {offset / 2}; i < bits; i += p) + { + composite[static_cast(i)] = 1; + } + } +} + +// Sieves [start, stop] (start >= 7, both < 2^64) by the window method and passes batches +// of primes to consume(const std::uint64_t*, std::size_t). +template +void test_range_u64(std::uint64_t start, std::uint64_t stop, Consumer& consume) +{ + if (stop < start) + { + return; + } + const std::uint64_t root {boost::math::tools::isqrt(stop)}; + const double width {static_cast(stop - start) + 1.0}; + const std::uint64_t depth {choose_window_depth((std::min)(width, 2.0 * static_cast(window_max_bits)), 100.0, root)}; + const std::vector primes {window_sieving_primes(depth)}; + std::vector composite; + std::vector batch; + batch.reserve(4096); + + std::uint64_t base {start | 1u}; + while (base <= stop) + { + const std::uint64_t remaining {(stop - base) / 2 + 1}; + const std::size_t bits {static_cast((std::min)(remaining, static_cast(window_max_bits)))}; + sieve_window(composite, bits, base_mod_u64 {base}, primes, base); + for (std::size_t i {0}; i < bits; ++i) + { + if (!composite[i]) + { + const std::uint64_t n {base + 2u * static_cast(i)}; + if (is_prime_u64(n)) + { + batch.push_back(n); + if (batch.size() == batch.capacity()) + { + consume(batch.data(), batch.size()); + batch.clear(); + } + } + } + } + if (bits == window_max_bits && base <= stop - 2u * static_cast(window_max_bits)) + { + base += 2u * static_cast(window_max_bits); + } + else + { + break; + } + } + if (!batch.empty()) + { + consume(batch.data(), batch.size()); + } +} + +// Classifies a survivor above 2^64. +template +bool survivor_is_prime(const Integer& n, std::size_t pss_index, std::uint64_t depth, bool probable_only) +{ + if (!probable_only && pss_index < 49) + { + return pseudosquares_prime_test(n, pss_index, depth); + } + return is_probable_prime_bpsw(n); +} + +// Primes in [lower, upper] (inclusive, lower >= 7, upper >= 2^64) passed one at a time to +// emit(const Integer&). +template +void big_range_impl(const Integer& lower, const Integer& upper, const prime_sieve_options& options, Emit&& emit) +{ + if (upper < lower) + { + return; + } + const Integer two64 {Integer(std::numeric_limits::max()) + Integer(1)}; + const bool fits_128 {upper < two64 * two64}; + + const Integer total_width {upper - lower}; + const double width {total_width > Integer(2u * window_max_bits) ? 2.0 * static_cast(window_max_bits) : static_cast(total_width) + 1.0}; + const double test_cost {options.probable_prime_only ? 2000.0 : 40000.0}; + const std::uint64_t depth {choose_window_depth(width, test_cost, window_max_depth)}; + const std::vector primes {window_sieving_primes(depth)}; + std::vector composite; + + Integer base {lower % 2 == 0 ? lower + Integer(1) : lower}; + while (base <= upper) + { + const Integer remaining {(upper - base) / 2 + 1}; + const std::size_t bits {remaining > Integer(window_max_bits) ? window_max_bits : static_cast(static_cast(remaining))}; + const Integer window_high {base + Integer(2u) * Integer(bits - 1)}; + + if (fits_128) + { + const std::uint64_t high {static_cast((base / two64) % two64)}; + const std::uint64_t low {static_cast(base % two64)}; + sieve_window(composite, bits, base_mod_u128 {high, low}, primes, 0); + } + else + { + sieve_window(composite, bits, base_mod_generic {base}, primes, 0); + } + + const Integer n_over_s {window_high / Integer(depth)}; + const std::size_t pss_index {pseudosquare_index_for(n_over_s)}; + for (std::size_t i {0}; i < bits; ++i) + { + if (!composite[i]) + { + const Integer n {base + Integer(2u) * Integer(i)}; + if (fits_u64(n)) + { + if (is_prime_u64(to_u64(n))) + { + emit(n); + } + } + else if (survivor_is_prime(n, pss_index, depth, options.probable_prime_only)) + { + emit(n); + } + } + } + if (bits < window_max_bits) + { + break; + } + base += Integer(2u) * Integer(window_max_bits); + } +} + +// Primes in [lower, upper] (inclusive) for bounds that do not fit 64 bits. Builtin types +// can never reach this point, so they get an empty instantiation. +template +OutputIterator big_range(const Integer& lower, const Integer& upper, OutputIterator out, const prime_sieve_options& options) +{ + if constexpr (std::is_integral::value) + { + (void)lower; + (void)upper; + (void)options; + return out; + } + else + { + big_range_impl(lower, upper, options, [&](const Integer& p) { *out = p; ++out; }); + return out; + } +} + +template +std::uint64_t big_count(const Integer& lower, const Integer& upper, const prime_sieve_options& options) +{ + if constexpr (std::is_integral::value) + { + (void)lower; + (void)upper; + (void)options; + return 0; + } + else + { + std::uint64_t count {0}; + big_range_impl(lower, upper, options, [&](const Integer&) { ++count; }); + return count; + } +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_HAS_NVRTC +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_BIG_RANGE_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/cpu_cache.hpp b/include/boost/math/special_functions/detail/prime_sieve/cpu_cache.hpp new file mode 100644 index 0000000000..6f56575272 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/cpu_cache.hpp @@ -0,0 +1,115 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Best-effort detection of the data cache sizes that drive the segment geometry. +// Falls back to 32 KiB L1 and 1 MiB L2 per core where nothing can be queried. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CPU_CACHE_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CPU_CACHE_HPP + +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#endif + +#if defined(__APPLE__) +# include +# include +#elif defined(__linux__) +# include +#endif + +namespace boost::math::detail::prime_sieve { + +struct cache_info +{ + std::size_t l1d_bytes {32768}; + std::size_t l2_bytes {1048576}; // per core (L2 size divided by the cores sharing it) +}; + +inline cache_info detect_cache_info() noexcept +{ + cache_info info {}; + +#if defined(__APPLE__) + std::int64_t value {0}; + std::size_t size {sizeof(value)}; + if (sysctlbyname("hw.perflevel0.l1dcachesize", &value, &size, nullptr, 0) == 0 && value > 0) + { + info.l1d_bytes = static_cast(value); + } + else + { + value = 0; + size = sizeof(value); + if (sysctlbyname("hw.l1dcachesize", &value, &size, nullptr, 0) == 0 && value > 0) + { + info.l1d_bytes = static_cast(value); + } + } + value = 0; + size = sizeof(value); + std::int64_t l2 {0}; + if (sysctlbyname("hw.perflevel0.l2cachesize", &value, &size, nullptr, 0) == 0 && value > 0) + { + l2 = value; + std::int64_t sharing {0}; + size = sizeof(sharing); + if (sysctlbyname("hw.perflevel0.cpusperl2", &sharing, &size, nullptr, 0) == 0 && sharing > 1) + { + l2 /= sharing; + } + } + else + { + value = 0; + size = sizeof(value); + if (sysctlbyname("hw.l2cachesize", &value, &size, nullptr, 0) == 0 && value > 0) + { + l2 = value; + } + } + if (l2 > 0) + { + info.l2_bytes = static_cast(l2); + } +#elif defined(__linux__) && defined(_SC_LEVEL1_DCACHE_SIZE) + const long l1 {sysconf(_SC_LEVEL1_DCACHE_SIZE)}; + if (l1 > 0) + { + info.l1d_bytes = static_cast(l1); + } +# if defined(_SC_LEVEL2_CACHE_SIZE) + const long l2 {sysconf(_SC_LEVEL2_CACHE_SIZE)}; + if (l2 > 0) + { + info.l2_bytes = static_cast(l2); + } +# endif +#endif + + // Guard against nonsense from virtual machines + if (info.l1d_bytes < 4096 || info.l1d_bytes > (1u << 20)) + { + info.l1d_bytes = 32768; + } + if (info.l2_bytes < info.l1d_bytes || info.l2_bytes > (std::size_t(1) << 30)) + { + info.l2_bytes = 1048576; + } + return info; +} + +inline const cache_info& cached_cache_info() noexcept +{ + static const cache_info info {detect_cache_info()}; + return info; +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CPU_CACHE_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/cuda.hpp b/include/boost/math/special_functions/detail/prime_sieve/cuda.hpp new file mode 100644 index 0000000000..bf8e7e32be --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/cuda.hpp @@ -0,0 +1,889 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// CUDA backend of the prime sieve. Only compiled by nvcc with BOOST_MATH_ENABLE_CUDA. +// +// The range is processed in chunks of up to 2^32 integers. Each chunk is sieved by a grid of +// thread blocks, one block per 32 KiB shared-memory segment (983040 integers) in the same +// mod-30 byte layout as the CPU engine: primes 7..37 are removed by ANDing periodic tables, +// primes 41..1024 are handled one warp per prime, and primes up to the segment span one +// thread per prime with atomicAnd on shared memory. Primes above the segment span (at most +// one multiple per segment) are applied in a second kernel directly on the chunk's bitset in +// global memory. Counting uses __popc with a block reduction; generation compacts the set +// bits into a sorted 64-bit array through a two-level exclusive scan. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CUDA_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CUDA_HPP + +#include + +#if defined(__CUDACC__) && defined(BOOST_MATH_ENABLE_CUDA) && !defined(BOOST_MATH_HAS_NVRTC) + +#define BOOST_MATH_HAS_CUDA_PRIME_SIEVE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::math::detail::prime_sieve { + +namespace cuda_detail { + +inline constexpr unsigned segment_bytes {32768}; // shared memory per block +inline constexpr unsigned segment_span {segment_bytes * 30u}; // integers per segment +inline constexpr unsigned block_threads {256}; +inline constexpr unsigned warp_prime_limit {1024}; // primes up to this: one warp per prime +inline constexpr unsigned presieve_limit {37}; // primes up to this: periodic tables +inline constexpr unsigned segments_per_chunk {4369}; // about 2^32 integers per device chunk +inline constexpr std::size_t chunk_bytes {static_cast(segments_per_chunk) * segment_bytes}; +inline constexpr std::uint64_t chunk_numbers {static_cast(chunk_bytes) * 30u}; // multiple of 30 + +// Reports a failed CUDA call. Without host exceptions there is nowhere to return the +// error to, so the message goes to stderr and the process aborts. +inline void cuda_check(cudaError_t e, const char* what) +{ + if (e != cudaSuccess) + { +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + throw std::runtime_error(std::string("boost::math::prime_sieve CUDA error in ") + what + ": " + cudaGetErrorString(e)); +#else + std::fprintf(stderr, "boost::math::prime_sieve CUDA error in %s: %s\n", what, cudaGetErrorString(e)); + std::abort(); +#endif + } +} + +template +class device_buffer +{ +public: + device_buffer() = default; + + explicit device_buffer(std::size_t n) + { + allocate(n); + } + + ~device_buffer() + { + release(); + } + + device_buffer(const device_buffer&) = delete; + device_buffer& operator=(const device_buffer&) = delete; + + void allocate(std::size_t n) + { + release(); + if (n != 0) + { + void* p {nullptr}; + cuda_check(cudaMalloc(&p, n * sizeof(T)), "cudaMalloc"); + ptr_ = static_cast(p); + size_ = n; + } + } + + void release() noexcept + { + if (ptr_ != nullptr) + { + cudaFree(ptr_); + ptr_ = nullptr; + size_ = 0; + } + } + + void upload(const T* host, std::size_t n) + { + cuda_check(cudaMemcpy(ptr_, host, n * sizeof(T), cudaMemcpyHostToDevice), "cudaMemcpy H2D"); + } + + void download(T* host, std::size_t n) const + { + cuda_check(cudaMemcpy(host, ptr_, n * sizeof(T), cudaMemcpyDeviceToHost), "cudaMemcpy D2H"); + } + + T* get() noexcept + { + return ptr_; + } + + const T* get() const noexcept + { + return ptr_; + } + + std::size_t size() const noexcept + { + return size_; + } + +private: + T* ptr_ {nullptr}; + std::size_t size_ {0}; +}; + +// Page-locked host memory for fast device to host copies; falls back to pageable memory. +class pinned_buffer +{ +public: + pinned_buffer() = default; + + ~pinned_buffer() + { + release(); + } + + pinned_buffer(const pinned_buffer&) = delete; + pinned_buffer& operator=(const pinned_buffer&) = delete; + + void reserve(std::size_t n) + { + if (n <= size_) + { + return; + } + release(); + void* p {nullptr}; + if (cudaMallocHost(&p, n * sizeof(std::uint64_t)) == cudaSuccess) + { + ptr_ = static_cast(p); + pinned_ = true; + } + else + { + (void)cudaGetLastError(); + fallback_.resize(n); + ptr_ = fallback_.data(); + pinned_ = false; + } + size_ = n; + } + + std::uint64_t* data() noexcept + { + return ptr_; + } + +private: + void release() noexcept + { + if (pinned_ && ptr_ != nullptr) + { + cudaFreeHost(ptr_); + } + fallback_.clear(); + fallback_.shrink_to_fit(); + ptr_ = nullptr; + size_ = 0; + pinned_ = false; + } + + std::uint64_t* ptr_ {nullptr}; + std::size_t size_ {0}; + bool pinned_ {false}; + std::vector fallback_; +}; + +// Tables the kernels need, passed by value so no device globals are required. Kernels copy +// them into shared memory first: constant memory serializes lane-divergent indexing. +struct alignas(4) device_tables +{ + std::uint8_t unset_bit[8]; + std::uint8_t bit_values[64]; + std::uint8_t keep_low[37]; + std::uint8_t keep_high[37]; + std::uint8_t class_of_residue[30]; // bit_of_residue for residues coprime to 30 + std::uint8_t inverse_mod_30[30]; // multiplicative inverse for residues coprime to 30 + std::uint8_t init_factor[30]; // wheel30 init: distance to the next coprime multiplier + std::uint8_t init_phase[30]; // wheel30 init: phase of that multiplier + std::uint8_t padding[2]; + std::uint32_t wheel[64]; // wheel30 element by state: unset | factor << 8 | correct << 16 | next << 24 +}; + +__device__ __forceinline__ std::uint8_t wheel_unset(std::uint32_t w) { return static_cast(w); } +__device__ __forceinline__ std::uint32_t wheel_factor(std::uint32_t w) { return (w >> 8) & 0xffu; } +__device__ __forceinline__ std::uint32_t wheel_correct(std::uint32_t w) { return (w >> 16) & 0xffu; } +__device__ __forceinline__ unsigned wheel_next(std::uint32_t w) { return w >> 24; } + +static_assert(sizeof(device_tables) % 4 == 0, "device_tables must be copyable in 32-bit words"); + +// Block-cooperative copy of the kernel's table argument into shared memory. +__device__ __forceinline__ void load_tables(const device_tables& from, device_tables& to) +{ + const std::uint32_t* src {reinterpret_cast(&from)}; + std::uint32_t* dst {reinterpret_cast(&to)}; + for (unsigned i {threadIdx.x}; i < sizeof(device_tables) / 4; i += blockDim.x) + { + dst[i] = src[i]; + } + __syncthreads(); +} + +constexpr device_tables make_device_tables() noexcept +{ + device_tables t {}; + for (unsigned k {0}; k < 8; ++k) + { + t.unset_bit[k] = unset_bit[k]; + } + for (unsigned b {0}; b < 64; ++b) + { + t.bit_values[b] = bit_values[b]; + t.wheel[b] = static_cast(wheel30.wheel[b].unset_bit) + | (static_cast(wheel30.wheel[b].next_multiple_factor) << 8) + | (static_cast(wheel30.wheel[b].correct) << 16) + | (static_cast(wheel30.wheel[b].next) << 24); + } + for (unsigned d {0}; d < 37; ++d) + { + t.keep_low[d] = keep_low[d]; + t.keep_high[d] = keep_high[d]; + } + for (unsigned r {0}; r < 30; ++r) + { + t.class_of_residue[r] = bit_of_residue(r); + t.init_factor[r] = wheel30.init[r].next_multiple_factor; + t.init_phase[r] = wheel30.init[r].wheel_index; + t.inverse_mod_30[r] = 0; + for (unsigned x {1}; x < 30; ++x) + { + if ((r * x) % 30 == 1) + { + t.inverse_mod_30[r] = static_cast(x); + } + } + } + return t; +} + +// Built at compile time; copied into a kernel argument per launch. +inline constexpr device_tables device_tables_value = make_device_tables(); + +// Per sieving prime: p, chunk_low mod p (advanced per chunk) and the Barrett constant for p, +// so that segment_low mod p needs no 64-bit division. +struct device_prime +{ + std::uint32_t p; + std::uint32_t residue; // chunk_low mod p + std::uint64_t barrett; // floor(2^64 / p) +}; + +// r = t mod p for t < 2^63 using the precomputed floor(2^64 / p). +__device__ __forceinline__ std::uint32_t barrett_mod(std::uint64_t t, std::uint32_t p, std::uint64_t m) +{ + std::uint64_t q {__umul64hi(t, m)}; + std::uint64_t r {t - q * p}; + if (r >= p) + { + r -= p; + } + return static_cast(r); +} + +// Integer offset from segment_low and wheel state of the first multiple of p that is at least +// max(segment_low, p * p) and coprime to 30. segment_low_mod_p is segment_low modulo p. +__device__ __forceinline__ void first_multiple_offset(const device_tables& t, std::uint32_t p, std::uint64_t segment_low, + std::uint32_t segment_low_mod_p, std::uint64_t& d, unsigned& state) +{ + // d0: offset (in integers) of the first multiple >= segment_low + d = segment_low_mod_p == 0 ? 0u : p - segment_low_mod_p; + const unsigned p_mod_30 {p % 30u}; + unsigned q_mod_30 {}; + const std::uint64_t square {static_cast(p) * p}; + if (segment_low + d < square) + { + // never cross off the prime itself or multiples a smaller prime already removed + d = square - segment_low; + q_mod_30 = p_mod_30; + } + else + { + // q0 mod 30 from d0 mod 30 (segment_low is a multiple of 30) and the inverse of p mod 30 + q_mod_30 = static_cast((d % 30u) * t.inverse_mod_30[p_mod_30] % 30u); + } + d += static_cast(t.init_factor[q_mod_30]) * p; + state = t.class_of_residue[p_mod_30] * 8u + t.init_phase[q_mod_30]; + // offsets below 7 belong to the previous segment's last byte: step once more + while (d < 7) + { + const std::uint32_t w {t.wheel[state]}; + d += static_cast(wheel_factor(w)) * p; + state = wheel_next(w); + } +} + +// Byte index (relative to the segment) and wheel state of the first multiple in a segment. +// Multiples beyond the segment yield an index >= segment_bytes. +__device__ __forceinline__ void first_multiple_device(const device_tables& t, std::uint32_t p, std::uint64_t segment_low, + std::uint32_t segment_low_mod_p, std::uint32_t& byte_index, std::uint32_t& state) +{ + std::uint64_t d {}; + unsigned s {}; + first_multiple_offset(t, p, segment_low, segment_low_mod_p, d, s); + const std::uint64_t byte {(d - 7) / 30}; + byte_index = byte > segment_bytes ? segment_bytes : static_cast(byte); + state = s; +} + +__device__ __forceinline__ void clear_bit(std::uint32_t* words, std::uint32_t byte_index, std::uint8_t mask) +{ + const std::uint32_t word_mask {~(static_cast(static_cast(~mask)) << (8u * (byte_index & 3u)))}; + atomicAnd(&words[byte_index >> 2], word_mask); +} + +// One block sieves one segment of the chunk. primes[0, n_warp) are handled per warp, primes +// [n_warp, n_thread) per thread. The finished segment is written to chunk_words. +__global__ void __launch_bounds__(block_threads) +sieve_segments_kernel(device_tables table_arg, const device_prime* primes, std::uint32_t n_warp, std::uint32_t n_thread, + const std::uint8_t* presieve0, std::uint32_t period0, const std::uint8_t* presieve1, std::uint32_t period1, + const std::uint8_t* presieve2, std::uint32_t period2, + std::uint32_t* chunk_words, std::uint64_t chunk_low, std::uint64_t start, std::uint64_t stop, std::uint32_t n_segments) +{ + __shared__ std::uint32_t words[segment_bytes / 4]; + __shared__ device_tables tables; + const std::uint32_t segment {blockIdx.x}; + if (segment >= n_segments) + { + return; + } + load_tables(table_arg, tables); + const std::uint64_t segment_low {chunk_low + static_cast(segment) * segment_span}; + std::uint8_t* bytes {reinterpret_cast(words)}; + + // bytes actually covering [segment_low + 7, stop] + std::uint32_t used_bytes {segment_bytes}; + if (stop - segment_low < static_cast(segment_span) + 1) + { + const std::uint64_t remaining {stop - segment_low}; + used_bytes = remaining < 7 ? 0u : static_cast((remaining - 7) / 30 + 1); + } + + // phase 0: pre-sieve 7..37 from the periodic tables + { + const std::uint32_t byte_low {static_cast((segment_low / 30) % period0)}; + const std::uint32_t byte_low1 {static_cast((segment_low / 30) % period1)}; + const std::uint32_t byte_low2 {static_cast((segment_low / 30) % period2)}; + for (std::uint32_t i {threadIdx.x}; i < segment_bytes; i += block_threads) + { + std::uint32_t p0 {byte_low + i}; + if (p0 >= period0) { p0 -= period0; if (p0 >= period0) { p0 %= period0; } } + std::uint32_t p1 {byte_low1 + i}; + if (p1 >= period1) { p1 -= period1; if (p1 >= period1) { p1 %= period1; } } + std::uint32_t p2 {byte_low2 + i}; + if (p2 >= period2) { p2 -= period2; if (p2 >= period2) { p2 %= period2; } } + bytes[i] = presieve0[p0] & presieve1[p1] & presieve2[p2]; + } + } + // the first segments of the number line contain the pre-sieved primes themselves + if (threadIdx.x == 0 && segment_low <= presieve_limit) + { + const std::uint8_t small[9] = {7, 11, 13, 17, 19, 23, 29, 31, 37}; + for (unsigned k {0}; k < 9; ++k) + { + if (small[k] >= segment_low + 7) + { + bytes[(small[k] - segment_low - 7) / 30] |= static_cast(1u << tables.class_of_residue[small[k] % 30]); + } + } + } + __syncthreads(); + + // phase 1: warp per prime for the small primes + { + const unsigned lane {threadIdx.x & 31u}; + const unsigned warp {threadIdx.x >> 5}; + const unsigned warps {block_threads / 32u}; + for (std::uint32_t idx {warp}; idx < n_warp; idx += warps) + { + const device_prime pr {primes[idx]}; + const std::uint32_t span_mod {barrett_mod(segment_span, pr.p, pr.barrett)}; + const std::uint32_t low_mod {barrett_mod(static_cast(pr.residue) + static_cast(segment) * span_mod, pr.p, pr.barrett)}; + std::uint32_t i {}; + std::uint32_t state {}; + first_multiple_device(tables, pr.p, segment_low, low_mod, i, state); + // one wheel turn is exactly p bytes; the eight offsets within a turn are fixed + std::uint32_t offsets[8]; + std::uint8_t masks[8]; + const std::uint32_t p30 {pr.p / 30u}; + std::uint32_t o {0}; + for (unsigned k {0}; k < 8; ++k) + { + const std::uint32_t w {tables.wheel[state]}; + offsets[k] = o; + masks[k] = wheel_unset(w); + o += p30 * wheel_factor(w) + wheel_correct(w); + state = wheel_next(w); + } + for (std::uint32_t turn_start {i + lane * pr.p}; turn_start < segment_bytes; turn_start += 32u * pr.p) + { + for (unsigned k {0}; k < 8; ++k) + { + const std::uint32_t b {turn_start + offsets[k]}; + if (b < segment_bytes) + { + clear_bit(words, b, masks[k]); + } + } + } + } + } + + // phase 2: thread per prime for the medium primes + for (std::uint32_t idx {n_warp + threadIdx.x}; idx < n_thread; idx += block_threads) + { + const device_prime pr {primes[idx]}; + const std::uint32_t span_mod {barrett_mod(segment_span, pr.p, pr.barrett)}; + const std::uint32_t low_mod {barrett_mod(static_cast(pr.residue) + static_cast(segment) * span_mod, pr.p, pr.barrett)}; + std::uint32_t i {}; + std::uint32_t state {}; + first_multiple_device(tables, pr.p, segment_low, low_mod, i, state); + const std::uint32_t p30 {pr.p / 30u}; + while (i < segment_bytes) + { + const std::uint32_t w {tables.wheel[state]}; + clear_bit(words, i, wheel_unset(w)); + i += p30 * wheel_factor(w) + wheel_correct(w); + state = wheel_next(w); + } + } + __syncthreads(); + + // phase 3: range masks, tail zeroing, store to global memory + if (threadIdx.x == 0) + { + if (segment_low < start && start - segment_low < 37) + { + bytes[0] &= tables.keep_low[start - segment_low]; + } + if (used_bytes != 0 && used_bytes < segment_bytes) + { + const std::uint64_t e {stop - segment_low - 30u * static_cast(used_bytes - 1)}; + bytes[used_bytes - 1] &= tables.keep_high[e > 36 ? 36 : static_cast(e)]; + } + else if (used_bytes == segment_bytes && stop - segment_low < static_cast(segment_span) + 1) + { + const std::uint64_t e {stop - segment_low - 30u * static_cast(segment_bytes - 1)}; + bytes[segment_bytes - 1] &= tables.keep_high[e > 36 ? 36 : static_cast(e)]; + } + } + __syncthreads(); + for (std::uint32_t i {threadIdx.x}; i < segment_bytes; i += block_threads) + { + if (i >= used_bytes) + { + bytes[i] = 0; + } + } + __syncthreads(); + std::uint32_t* out {chunk_words + static_cast(segment) * (segment_bytes / 4)}; + for (std::uint32_t w {threadIdx.x}; w < segment_bytes / 4; w += block_threads) + { + out[w] = words[w]; + } +} + +// Primes above the segment span: one thread per prime over the whole chunk bitset. +__global__ void __launch_bounds__(block_threads) +sieve_big_kernel(device_tables table_arg, const device_prime* primes, std::uint32_t first, std::uint32_t count, + std::uint32_t* chunk_words, std::uint64_t chunk_low, std::uint64_t stop, std::uint64_t chunk_bytes_used) +{ + __shared__ device_tables tables; + load_tables(table_arg, tables); + const std::uint32_t idx {first + blockIdx.x * block_threads + threadIdx.x}; + if (idx >= count) + { + return; + } + const device_prime pr {primes[idx]}; + std::uint64_t d {}; + unsigned state {}; + first_multiple_offset(tables, pr.p, chunk_low, pr.residue, d, state); + const std::uint64_t limit {stop - chunk_low}; + while (d <= limit) + { + const std::uint64_t byte {(d - 7) / 30}; + if (byte >= chunk_bytes_used) + { + break; + } + const std::uint32_t w {tables.wheel[state]}; + const std::uint32_t word_mask {~(static_cast(static_cast(~wheel_unset(w))) << (8u * static_cast(byte & 3u)))}; + atomicAnd(&chunk_words[byte >> 2], word_mask); + d += static_cast(wheel_factor(w)) * pr.p; + state = wheel_next(w); + } +} + +// residue = (residue + chunk_mod) mod p after a chunk advanced by chunk_numbers. +__global__ void advance_residues_kernel(device_prime* primes, std::uint32_t count) +{ + const std::uint32_t idx {blockIdx.x * block_threads + threadIdx.x}; + if (idx < count) + { + const std::uint32_t p {primes[idx].p}; + std::uint32_t r {primes[idx].residue + static_cast(chunk_numbers % p)}; + if (r >= p) + { + r -= p; + } + primes[idx].residue = r; + } +} + +// Per-block popcounts over 256 * 4 words each (block_totals) and a global total. +__global__ void __launch_bounds__(block_threads) +count_kernel(const std::uint32_t* words, std::uint32_t n_words, std::uint32_t* block_totals, unsigned long long* total) +{ + __shared__ std::uint32_t partial[block_threads / 32]; + std::uint32_t c {0}; + const std::uint32_t base {blockIdx.x * block_threads * 4u}; + for (unsigned k {0}; k < 4; ++k) + { + const std::uint32_t w {base + k * block_threads + threadIdx.x}; + if (w < n_words) + { + c += static_cast(__popc(words[w])); + } + } + for (unsigned offset {16}; offset > 0; offset >>= 1) + { + c += __shfl_down_sync(0xffffffffu, c, offset); + } + if ((threadIdx.x & 31u) == 0) + { + partial[threadIdx.x >> 5] = c; + } + __syncthreads(); + if (threadIdx.x == 0) + { + std::uint32_t sum {0}; + for (unsigned i {0}; i < block_threads / 32; ++i) + { + sum += partial[i]; + } + if (block_totals != nullptr) + { + block_totals[blockIdx.x] = sum; + } + if (total != nullptr) + { + atomicAdd(total, static_cast(sum)); + } + } +} + +// Exclusive scan of the block totals in place (single block), writing the grand total. +__global__ void scan_totals_kernel(std::uint32_t* totals, std::uint32_t n, unsigned long long* grand_total) +{ + __shared__ unsigned long long carry; + __shared__ std::uint32_t values[1024]; + if (threadIdx.x == 0) + { + carry = 0; + } + __syncthreads(); + for (std::uint32_t base {0}; base < n; base += 1024) + { + const std::uint32_t i {base + threadIdx.x}; + const std::uint32_t v {i < n ? totals[i] : 0u}; + values[threadIdx.x] = v; + __syncthreads(); + // Hillis-Steele inclusive scan in shared memory + for (unsigned offset {1}; offset < 1024; offset <<= 1) + { + const std::uint32_t add {threadIdx.x >= offset ? values[threadIdx.x - offset] : 0u}; + __syncthreads(); + values[threadIdx.x] += add; + __syncthreads(); + } + const unsigned long long exclusive {carry + values[threadIdx.x] - v}; + if (i < n) + { + totals[i] = static_cast(exclusive); + } + __syncthreads(); + if (threadIdx.x == 1023) + { + carry += values[1023]; + } + __syncthreads(); + } + if (threadIdx.x == 0 && grand_total != nullptr) + { + *grand_total = carry; + } +} + +// Writes the integers of the set bits, in order, using the scanned block offsets. +__global__ void __launch_bounds__(block_threads) +compact_kernel(device_tables table_arg, const std::uint32_t* words, std::uint32_t n_words, const std::uint32_t* block_offsets, + std::uint64_t chunk_low, std::uint64_t* out) +{ + __shared__ std::uint32_t thread_counts[block_threads]; + __shared__ device_tables tables; + load_tables(table_arg, tables); + const std::uint32_t base {blockIdx.x * block_threads * 4u}; + std::uint32_t c {0}; + // each thread owns four consecutive words so that the output stays sorted + for (unsigned k {0}; k < 4; ++k) + { + const std::uint32_t w {base + 4u * threadIdx.x + k}; + if (w < n_words) + { + c += static_cast(__popc(words[w])); + } + } + thread_counts[threadIdx.x] = c; + __syncthreads(); + for (unsigned offset {1}; offset < block_threads; offset <<= 1) + { + const std::uint32_t add {threadIdx.x >= offset ? thread_counts[threadIdx.x - offset] : 0u}; + __syncthreads(); + thread_counts[threadIdx.x] += add; + __syncthreads(); + } + std::uint64_t pos {block_offsets[blockIdx.x] + (thread_counts[threadIdx.x] - c)}; + for (unsigned k {0}; k < 4; ++k) + { + const std::uint32_t w {base + 4u * threadIdx.x + k}; + if (w < n_words) + { + std::uint32_t bits {words[w]}; + const std::uint64_t low {chunk_low + static_cast(w) * 120u}; + while (bits != 0) + { + const int b {__ffs(bits) - 1}; + out[pos++] = low + tables.bit_values[b]; + bits &= bits - 1; + } + } + } +} + +// Host-side state for one call. +class cuda_sieve +{ +public: + cuda_sieve(std::uint64_t start, std::uint64_t stop) : start_(start), stop_(stop), tables_(device_tables_value) + { + // the chunk grid begins at a multiple of 30 covering start + chunk_low_ = 30u * ((start - 7) / 30); + + // sieving primes: 41..163 from the small table, 167.. from the CPU engine + const std::uint64_t root {boost::math::tools::isqrt(stop)}; + std::vector primes; + for (std::size_t i {0}; i < 71 && small_primes_to_353[i] <= presieve_max_prime; ++i) + { + if (small_primes_to_353[i] > presieve_limit && small_primes_to_353[i] <= root) + { + primes.push_back(small_primes_to_353[i]); + } + } +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_THREADS + const std::vector rest {sieving_primes_upto_parallel(root, prime_sieve_options {})}; +#else + const std::vector rest {sieving_primes_upto(root)}; +#endif + primes.insert(primes.end(), rest.begin(), rest.end()); + + std::vector records(primes.size()); + n_warp_ = 0; + n_thread_ = 0; + for (std::size_t i {0}; i < primes.size(); ++i) + { + const std::uint32_t p {primes[i]}; + records[i].p = p; + records[i].residue = static_cast(chunk_low_ % p); + records[i].barrett = (std::numeric_limits::max)() / p; + if (p <= warp_prime_limit) + { + n_warp_ = static_cast(i + 1); + } + if (p <= segment_span) + { + n_thread_ = static_cast(i + 1); + } + } + n_primes_ = static_cast(records.size()); + if (!records.empty()) + { + primes_.allocate(records.size()); + primes_.upload(records.data(), records.size()); + } + + // pre-sieve tables for 7..37 + const std::uint8_t groups[3][3] = {{7, 11, 13}, {17, 19, 23}, {29, 31, 37}}; + for (unsigned g {0}; g < 3; ++g) + { + std::size_t period {1}; + for (const std::uint8_t p : groups[g]) + { + period *= p; + } + std::vector pattern(period, 0xff); + for (const std::uint8_t p : groups[g]) + { + presieve_mark(pattern.data(), period, p); + } + presieve_period_[g] = static_cast(period); + presieve_[g].allocate(period); + presieve_[g].upload(pattern.data(), period); + } + + words_.allocate(chunk_bytes / 4); + const std::size_t count_blocks {(chunk_bytes / 4 + block_threads * 4 - 1) / (block_threads * 4)}; + block_totals_.allocate(count_blocks); + totals_.allocate(1); + } + + // Runs f(chunk_low, chunk_stop) for every chunk with the chunk's bitset sieved into words_. + template + void for_each_chunk(F&& f) + { + std::uint64_t chunk_low {chunk_low_}; + while (true) + { + const std::uint64_t remaining {stop_ - chunk_low}; + if (remaining < 7) + { + break; + } + const std::uint64_t needed_bytes {(remaining - 7) / 30 + 1}; + const bool last {needed_bytes <= chunk_bytes}; + const std::uint64_t used_bytes {last ? needed_bytes : chunk_bytes}; + // A full chunk covers every candidate of its bytes, including offset 31 of the last + // byte (chunk_low + chunk_numbers + 1), which the next chunk cannot represent. + const std::uint64_t chunk_stop {last ? stop_ : chunk_low + chunk_numbers + 1}; + const std::uint32_t n_segments {static_cast((used_bytes + segment_bytes - 1) / segment_bytes)}; + const std::size_t n_words {static_cast(n_segments) * (segment_bytes / 4)}; + + sieve_segments_kernel<<>>(tables_, primes_.get(), n_warp_, n_thread_, + presieve_[0].get(), presieve_period_[0], presieve_[1].get(), presieve_period_[1], presieve_[2].get(), presieve_period_[2], + words_.get(), chunk_low, start_, chunk_stop, n_segments); + cuda_check(cudaGetLastError(), "sieve_segments_kernel launch"); + if (n_primes_ > n_thread_) + { + const std::uint32_t big {n_primes_ - n_thread_}; + sieve_big_kernel<<<(big + block_threads - 1) / block_threads, block_threads>>>(tables_, primes_.get(), n_thread_, n_primes_, + words_.get(), chunk_low, chunk_stop, used_bytes); + cuda_check(cudaGetLastError(), "sieve_big_kernel launch"); + } + f(chunk_low, chunk_stop, static_cast(n_words)); + if (last) + { + break; + } + advance_residues_kernel<<<(n_primes_ + block_threads - 1) / block_threads, block_threads>>>(primes_.get(), n_primes_); + cuda_check(cudaGetLastError(), "advance_residues_kernel launch"); + chunk_low += chunk_numbers; + } + } + + std::uint64_t count() + { + unsigned long long total {0}; + for_each_chunk([&](std::uint64_t, std::uint64_t, std::uint32_t n_words) + { + const unsigned long long zero {0}; + cuda_check(cudaMemcpy(totals_.get(), &zero, sizeof(zero), cudaMemcpyHostToDevice), "cudaMemcpy total"); + const std::uint32_t blocks {(n_words + block_threads * 4 - 1) / (block_threads * 4)}; + count_kernel<<>>(words_.get(), n_words, nullptr, totals_.get()); + cuda_check(cudaGetLastError(), "count_kernel launch"); + unsigned long long chunk_total {0}; + cuda_check(cudaMemcpy(&chunk_total, totals_.get(), sizeof(chunk_total), cudaMemcpyDeviceToHost), "cudaMemcpy total"); + total += chunk_total; + }); + return total; + } + + template + void generate(Consumer& consume) + { + pinned_buffer host; + for_each_chunk([&](std::uint64_t chunk_low, std::uint64_t, std::uint32_t n_words) + { + const std::uint32_t blocks {(n_words + block_threads * 4 - 1) / (block_threads * 4)}; + count_kernel<<>>(words_.get(), n_words, block_totals_.get(), nullptr); + cuda_check(cudaGetLastError(), "count_kernel launch"); + scan_totals_kernel<<<1, 1024>>>(block_totals_.get(), blocks, totals_.get()); + cuda_check(cudaGetLastError(), "scan_totals_kernel launch"); + unsigned long long chunk_total {0}; + cuda_check(cudaMemcpy(&chunk_total, totals_.get(), sizeof(chunk_total), cudaMemcpyDeviceToHost), "cudaMemcpy total"); + if (chunk_total == 0) + { + return; + } + if (output_.size() < chunk_total) + { + output_.allocate(static_cast(chunk_total)); + } + compact_kernel<<>>(tables_, words_.get(), n_words, block_totals_.get(), chunk_low, output_.get()); + cuda_check(cudaGetLastError(), "compact_kernel launch"); + host.reserve(static_cast(chunk_total)); + output_.download(host.data(), static_cast(chunk_total)); + consume(host.data(), static_cast(chunk_total)); + }); + } + +private: + std::uint64_t start_; + std::uint64_t stop_; + std::uint64_t chunk_low_ {0}; + device_tables tables_; + device_buffer primes_; + std::uint32_t n_primes_ {0}; + std::uint32_t n_warp_ {0}; + std::uint32_t n_thread_ {0}; + device_buffer presieve_[3]; + std::uint32_t presieve_period_[3] {}; + device_buffer words_; + device_buffer block_totals_; + device_buffer totals_; + device_buffer output_; +}; + +} // namespace cuda_detail + +// Counts the primes in [start, stop] (start >= 7) on the device. +inline std::uint64_t cuda_count(std::uint64_t start, std::uint64_t stop, const prime_sieve_options&) +{ + if (stop < start) + { + return 0; + } + cuda_detail::cuda_sieve sieve {start, stop}; + return sieve.count(); +} + +// Passes the primes in [start, stop] (start >= 7) in ascending batches to consume(const std::uint64_t*, std::size_t). +template +void cuda_range(std::uint64_t start, std::uint64_t stop, const prime_sieve_options&, Consumer& consume) +{ + if (stop < start) + { + return; + } + cuda_detail::cuda_sieve sieve {start, stop}; + sieve.generate(consume); +} + +} // namespace boost::math::detail::prime_sieve + +#endif // __CUDACC__ && BOOST_MATH_ENABLE_CUDA && !BOOST_MATH_HAS_NVRTC +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_CUDA_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/driver.hpp b/include/boost/math/special_functions/detail/prime_sieve/driver.hpp new file mode 100644 index 0000000000..56f431ff2e --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/driver.hpp @@ -0,0 +1,136 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Sieving prime generation and the single-threaded 64-bit driver. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_DRIVER_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_DRIVER_HPP + +#include +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +// Upper bound on the number of primes <= x (Dusart 2010 for x >= 60184). +inline std::uint64_t prime_count_upper_bound(std::uint64_t x) noexcept +{ + if (x < 2) + { + return 0; + } + if (x < 60184) + { + return static_cast(1.25 * static_cast(x) / std::log(static_cast(x))) + 1; + } + const double d {static_cast(x)}; + return static_cast(d / (std::log(d) - 1.1)) + 1; +} + +// Upper bound on the number of primes in [lower, upper]. +inline std::uint64_t prime_count_upper_bound(std::uint64_t lower, std::uint64_t upper) noexcept +{ + if (upper < lower) + { + return 0; + } + if (lower < 60184) + { + return prime_count_upper_bound(upper); + } + // pi(upper) - pi(lower) <= (upper - lower) / (ln lower - 1.1) since density decreases + const double width {static_cast(upper - lower) + 1.0}; + return static_cast(width / (std::log(static_cast(lower)) - 1.1)) + 1; +} + +// Odd-only byte sieve for the primes in [167, n] with n <= 2^32 - 1 (used for n up to 65535 +// to seed the segmented generator, but correct for any n that fits memory). +inline std::vector simple_primes_from_167(std::uint32_t n) +{ + std::vector out; + if (n < 167) + { + return out; + } + std::vector composite((n / 2) + 1, 0); + for (std::uint32_t i {3}; static_cast(i) * i <= n; i += 2) + { + if (!composite[i / 2]) + { + for (std::uint64_t j {static_cast(i) * i}; j <= n; j += 2u * i) + { + composite[static_cast(j / 2)] = 1; + } + } + } + out.reserve(static_cast(prime_count_upper_bound(n))); + for (std::uint32_t i {167}; i <= n; i += 2) + { + if (!composite[i / 2]) + { + out.push_back(i); + } + } + return out; +} + +// Primes in [167, n] for n < 2^32, produced by the segmented engine itself. +inline std::vector sieving_primes_upto(std::uint64_t n) +{ + std::vector out; + if (n < 167) + { + return out; + } + if (n < 100000) + { + return simple_primes_from_167(static_cast(n)); + } + const std::uint32_t root {static_cast(boost::math::tools::isqrt(n))}; + const std::vector seed {simple_primes_from_167(root)}; + + prime_sieve_options options {}; + const sieve_geometry g {make_geometry(167, n, options)}; + extract_sink_u32 sink {out, static_cast(prime_count_upper_bound(n)) + extract_sink::slack}; + segment_sieve engine {g, seed}; + engine.run(sink); + return out; +} + +// Sieves [start, stop] (start >= 7) sequentially into sink. +template +void run_u64(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options, Sink& sink) +{ + if (stop < start) + { + sink.flush(); + return; + } + const sieve_geometry g {make_geometry(start, stop, options)}; + const std::vector primes {sieving_primes_upto(g.sqrt_stop)}; + segment_sieve engine {g, primes}; + engine.run(sink); +} + +// Counts the primes in [start, stop] (start >= 7) sequentially. +inline std::uint64_t count_u64(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options) +{ + count_sink sink {}; + run_u64(start, stop, options, sink); + return sink.count; +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_DRIVER_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/engine.hpp b/include/boost/math/special_functions/detail/prime_sieve/engine.hpp new file mode 100644 index 0000000000..24e994d21d --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/engine.hpp @@ -0,0 +1,157 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// The segmented sieve engine: iterates segments of sieve_bytes bytes over [start, stop], +// pre-sieves, feeds sieving primes lazily (a prime joins when the segment reaches p * p), +// runs the size classes, masks the range ends and hands each finished segment to a sink. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ENGINE_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ENGINE_HPP + +#include +#include +#include +#include +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +class segment_sieve +{ +public: + // sieving_primes must hold the primes in [167, sqrt(stop)] in ascending order. + segment_sieve(const sieve_geometry& geometry, const std::vector& sieving_primes) + : geo_(geometry), primes_(&sieving_primes) + { + words_.resize(geo_.sieve_bytes / 8); + reset_range(geo_.start, geo_.stop); + } + + // Prepares a new sub-range with the same geometry; the range must lie within the + // geometry's [start, stop] so that sieving primes up to sqrt(stop) suffice. + void reset_range(std::uint64_t start, std::uint64_t stop) + { + start_ = start < 7 ? 7 : start; + stop_ = stop; + next_prime_ = 0; + small_.init(stop_, geo_.l1_bytes); + medium_.init(stop_); + big_.init(stop_, geo_.sqrt_stop, geo_.sieve_bytes, geo_.has_big); + } + + template + void run(Sink& sink) + { + if (stop_ < start_) + { + return; + } + // The byte holding start_ covers offsets 7..36 above segment_low + std::uint64_t segment_low {30u * ((start_ - 7) / 30)}; + std::uint8_t* sieve {reinterpret_cast(words_.data())}; + const std::size_t sieve_bytes {geo_.sieve_bytes}; + + while (true) + { + const std::uint64_t remaining {stop_ - segment_low}; + if (remaining < 7) + { + break; + } + const std::uint64_t needed {(remaining - 7) / 30 + 1}; + const bool last {needed <= sieve_bytes}; + const std::size_t bytes {last ? static_cast(needed) : sieve_bytes}; + const std::uint64_t span {30u * static_cast(bytes) + 1}; + const std::uint64_t segment_high {span <= remaining ? segment_low + span : stop_}; + + presieve_segment(sieve, bytes, segment_low); + feed(segment_low, segment_high); + + small_.cross_off(sieve, bytes); + medium_.cross_off(sieve, bytes); + if (geo_.has_big) + { + big_.cross_off(sieve); + } + + if (segment_low < start_) + { + sieve[0] &= keep_low[static_cast(start_ - segment_low)]; + } + if (last) + { + const std::uint64_t e {stop_ - segment_low - 30u * static_cast(bytes - 1)}; + sieve[bytes - 1] &= keep_high[e > 36 ? 36 : static_cast(e)]; + } + + const std::size_t n_words {(bytes + 7) / 8}; + std::fill(sieve + bytes, sieve + n_words * 8, std::uint8_t {0}); + sink.segment(words_.data(), n_words, segment_low); + + if (last) + { + break; + } + segment_low += 30u * static_cast(sieve_bytes); + } + sink.flush(); + } + + const sieve_geometry& geometry() const noexcept + { + return geo_; + } + +private: + // Adds the sieving primes whose square lies at or below segment_high. + void feed(std::uint64_t segment_low, std::uint64_t segment_high) + { + const std::vector& primes {*primes_}; + while (next_prime_ < primes.size()) + { + const std::uint64_t p {primes[next_prime_]}; + if (p * p > segment_high) + { + break; + } + if (p <= geo_.max_small) + { + small_.add(p, segment_low); + } + else if (p <= geo_.max_medium) + { + medium_.add(p, segment_low); + } + else + { + big_.add(p, segment_low); + } + ++next_prime_; + } + } + + sieve_geometry geo_; + const std::vector* primes_; + std::vector words_; + erat_small small_; + erat_medium medium_; + erat_big big_; + std::uint64_t start_ {7}; + std::uint64_t stop_ {7}; + std::size_t next_prime_ {0}; +}; + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ENGINE_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/erat_big.hpp b/include/boost/math/special_functions/detail/prime_sieve/erat_big.hpp new file mode 100644 index 0000000000..79598cb18d --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/erat_big.hpp @@ -0,0 +1,273 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Big sieving primes (at most one multiple per segment): Oliveira e Silva's bucket sieve. +// Each prime is filed in the list of the segment holding its next multiple, so a segment +// only touches the primes that actually hit it. Lists are chains of fixed 8 KiB buckets +// from a memory pool; a write pointer sitting on an 8 KiB boundary means "bucket full". +// Uses the mod 210 wheel. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_BIG_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_BIG_HPP + +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +inline constexpr std::size_t bucket_bytes {8192}; + +// Entries looked ahead in a bucket; reading past a bucket's end stays inside the 8 KiB block +// (or the following one) and is harmless. +inline constexpr std::size_t prefetch_distance {16}; + +inline void prefetch_byte(const std::uint8_t* p) noexcept +{ +#if defined(__GNUC__) || defined(__clang__) + __builtin_prefetch(p, 1, 0); +#else + (void)p; +#endif +} + +// A bucket is exactly bucket_bytes long and aligned to bucket_bytes, so the bucket owning +// a write pointer is found by masking, and a pointer that reached the next boundary is full. +struct alignas(bucket_bytes) bucket +{ + static constexpr std::size_t capacity {(bucket_bytes - 2 * sizeof(void*)) / sizeof(sieving_prime)}; + + sieving_prime* end; // one past the last used slot, valid while the bucket is processed + bucket* previous; // older bucket of the same list + sieving_prime primes[capacity]; + + static bool is_full(const sieving_prime* write) noexcept + { + // also true for nullptr, which makes an empty list allocate its first bucket + return (reinterpret_cast(write) & (bucket_bytes - 1)) == 0; + } + + static bucket* owner(const sieving_prime* write) noexcept + { + return reinterpret_cast((reinterpret_cast(write) - 1) & ~static_cast(bucket_bytes - 1)); + } +}; + +static_assert(sizeof(bucket) == bucket_bytes, "bucket must be exactly one block"); + +// Hands out aligned buckets from large blocks; freed buckets go to a free list. +class bucket_pool +{ +public: + bucket* get() + { + if (free_ == nullptr) + { + grow(); + } + bucket* b {free_}; + free_ = b->previous; + b->previous = nullptr; + return b; + } + + void put(bucket* b) noexcept + { + b->previous = free_; + free_ = b; + } + + // Returns every bucket to the free list, keeping the blocks + void release_all() noexcept + { + free_ = nullptr; + for (const block& blk : blocks_) + { + for (std::size_t i {0}; i < blk.count; ++i) + { + put(blk.first + i); + } + } + } + +private: + struct block + { + std::unique_ptr storage; + bucket* first; + std::size_t count; + }; + + void grow() + { + // 512 KiB, then doubling up to 16 MiB per block + const std::size_t count {blocks_.empty() ? std::size_t(64) : (std::min)(blocks_.back().count * 2, std::size_t(2048))}; + block blk {}; + blk.storage.reset(new std::uint8_t[(count + 2) * bucket_bytes]); // one bucket of padding for alignment, one for the look-ahead reads + const std::uintptr_t raw {reinterpret_cast(blk.storage.get())}; + const std::uintptr_t aligned {(raw + bucket_bytes - 1) & ~static_cast(bucket_bytes - 1)}; + blk.first = reinterpret_cast(aligned); + blk.count = count; + for (std::size_t i {0}; i < count; ++i) + { + put(blk.first + i); + } + blocks_.push_back(std::move(blk)); + } + + std::vector blocks_; + bucket* free_ {nullptr}; +}; + +class erat_big +{ + // Dusart's bound on pi(x), used only for sizing + static std::uint64_t prime_count_upper_bound_local(std::uint64_t x) noexcept + { + if (x < 60184) + { + return x / 8 + 64; + } + const double d {static_cast(x)}; + return static_cast(d / (std::log(d) - 1.1)) + 1; + } + +public: + void init(std::uint64_t stop, std::uint64_t max_prime, std::size_t sieve_bytes, bool enabled) + { + stop_ = stop; + enabled_ = enabled; + staged_ = 0; + pool_.release_all(); + lists_.clear(); + if (!enabled) + { + return; + } + sieve_bytes_ = sieve_bytes; + log2_sieve_bytes_ = static_cast(63 - boost::math::tools::countl_zero(sieve_bytes)); + mask_ = sieve_bytes - 1; + // Enough lists for the largest possible jump: 10 wheel steps of the largest prime + const std::size_t lists {static_cast(((max_prime / 30 + 1) * 10 + sieve_bytes) / sieve_bytes) + 2}; + lists_.assign(lists, nullptr); + head_ = 0; + staging_.reserve(static_cast((std::min)(prime_count_upper_bound_local(max_prime), std::uint64_t(1u << 20)))); + } + + // Primes are staged unconditionally and the staging index advances by the accept flag: + // for short windows at large magnitudes about half of the primes have no multiple in + // range, and a branch on that outcome mispredicts (about 3 ns per prime measured). + void add(std::uint64_t prime, std::uint64_t segment_low) + { + if (staged_ == staging_.size()) + { + staging_.resize(staged_ + 65536); + } + staged_prime& st {staging_[staged_]}; + const bool hit {first_multiple(wheel210, prime, segment_low, stop_, st.multiple_index, st.wheel_index)}; + st.prime30 = static_cast(prime / 30); + staged_ += hit ? 1u : 0u; + } + + void cross_off(std::uint8_t* sieve) + { + flush_staging(); + // A prime whose next multiple is still inside this segment is re-filed into the + // current list, so keep draining it until nothing is left. + while (lists_[head_] != nullptr) + { + sieving_prime* write {lists_[head_]}; + lists_[head_] = nullptr; + bucket* b {bucket::owner(write)}; + b->end = write; + while (b != nullptr) + { + for (const sieving_prime* sp {b->primes}; sp != b->end; ++sp) + { + // the byte touched a few entries ahead is known: hide the cache miss + prefetch_byte(sieve + sp[prefetch_distance].multiple_index()); + const auto& e = wheel210.wheel[sp->wheel_index()]; + const std::size_t i {sp->multiple_index()}; + sieve[i] &= e.unset_bit; + const std::uint64_t next {static_cast(i) + static_cast(sp->prime30) * e.next_multiple_factor + e.correct}; + sieving_prime moved {*sp}; + store(moved, next, e.next); + } + bucket* done {b}; + b = b->previous; + if (b != nullptr) + { + b->end = b->primes + bucket::capacity; + } + pool_.put(done); + } + } + head_ = head_ + 1 == lists_.size() ? 0 : head_ + 1; + } + +private: + struct staged_prime + { + std::uint64_t multiple_index; + std::uint32_t prime30; + unsigned wheel_index; + }; + + // Distributes the accepted staged primes into their lists (a predictable loop). + void flush_staging() + { + for (std::size_t i {0}; i < staged_; ++i) + { + const staged_prime& st {staging_[i]}; + sieving_prime sp {}; + sp.prime30 = st.prime30; + store(sp, st.multiple_index, st.wheel_index); + } + staged_ = 0; + } + + // Files the prime in the list of the segment (relative to the current one) that holds + // the byte index, keeping only the offset inside that segment. + void store(sieving_prime& sp, std::uint64_t multiple_index, unsigned wheel_index) + { + const std::size_t segment {static_cast(multiple_index >> log2_sieve_bytes_)}; + sp.set(static_cast(multiple_index & mask_), wheel_index); + // segment is below the list count by construction; the wrap compiles to a select + const std::size_t slot {head_ + segment}; + const std::size_t wrapped {slot >= lists_.size() ? slot - lists_.size() : slot}; + sieving_prime*& write {lists_[wrapped]}; + if (bucket::is_full(write)) + { + bucket* b {pool_.get()}; + b->previous = write == nullptr ? nullptr : bucket::owner(write); + write = b->primes; + } + *write++ = sp; + } + + std::vector lists_; + bucket_pool pool_; + std::vector staging_; + std::size_t staged_ {0}; + std::size_t head_ {0}; + std::size_t sieve_bytes_ {0}; + std::size_t mask_ {0}; + unsigned log2_sieve_bytes_ {0}; + std::uint64_t stop_ {0}; + bool enabled_ {false}; +}; + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_BIG_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/erat_medium.hpp b/include/boost/math/special_functions/detail/prime_sieve/erat_medium.hpp new file mode 100644 index 0000000000..b25dc0da50 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/erat_medium.hpp @@ -0,0 +1,180 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Medium sieving primes (a handful of multiples per segment). Primes are kept in 64 lists +// keyed by wheel state; each list is processed by one function specialized for that state +// (Duff's device over the eight phases), so the branch predictor sees one pattern and the +// per-prime overhead is a loop iteration rather than a call. After a segment each prime is +// re-filed under its new state. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_MEDIUM_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_MEDIUM_HPP + +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +namespace medium_detail { + +using list_array = std::array, 64>; + +// Crosses off the multiples of every prime in [first, last) (all in wheel state State) over +// sieve[0, n) and files each prime, with its offset into the next segment, into next. +template +void cross_off_list(std::uint8_t* sieve, std::size_t n, const sieving_prime* first, const sieving_prime* last, list_array& next) noexcept +{ + constexpr unsigned base {(State / 8) * 8}; + constexpr const auto& w = wheel30.wheel; + + for (const sieving_prime* sp {first}; sp != last; ++sp) + { + const std::size_t p {static_cast(sp->prime30)}; + std::size_t i {sp->multiple_index()}; + unsigned state {State}; + if (i < n) + { + const std::size_t d0 {p * w[base + 0].next_multiple_factor + w[base + 0].correct}; + const std::size_t d1 {p * w[base + 1].next_multiple_factor + w[base + 1].correct}; + const std::size_t d2 {p * w[base + 2].next_multiple_factor + w[base + 2].correct}; + const std::size_t d3 {p * w[base + 3].next_multiple_factor + w[base + 3].correct}; + const std::size_t d4 {p * w[base + 4].next_multiple_factor + w[base + 4].correct}; + const std::size_t d5 {p * w[base + 5].next_multiple_factor + w[base + 5].correct}; + const std::size_t d6 {p * w[base + 6].next_multiple_factor + w[base + 6].correct}; + const std::size_t d7 {p * w[base + 7].next_multiple_factor + w[base + 7].correct}; + switch (State % 8) + { + for (;;) + { + case 0: + if (i >= n) { state = base + 0; break; } + sieve[i] &= w[base + 0].unset_bit; + i += d0; + [[fallthrough]]; + case 1: + if (i >= n) { state = base + 1; break; } + sieve[i] &= w[base + 1].unset_bit; + i += d1; + [[fallthrough]]; + case 2: + if (i >= n) { state = base + 2; break; } + sieve[i] &= w[base + 2].unset_bit; + i += d2; + [[fallthrough]]; + case 3: + if (i >= n) { state = base + 3; break; } + sieve[i] &= w[base + 3].unset_bit; + i += d3; + [[fallthrough]]; + case 4: + if (i >= n) { state = base + 4; break; } + sieve[i] &= w[base + 4].unset_bit; + i += d4; + [[fallthrough]]; + case 5: + if (i >= n) { state = base + 5; break; } + sieve[i] &= w[base + 5].unset_bit; + i += d5; + [[fallthrough]]; + case 6: + if (i >= n) { state = base + 6; break; } + sieve[i] &= w[base + 6].unset_bit; + i += d6; + [[fallthrough]]; + case 7: + if (i >= n) { state = base + 7; break; } + sieve[i] &= w[base + 7].unset_bit; + i += d7; + } + } + } + sieving_prime moved {*sp}; + moved.set(i - n, state); + next[state].push_back(moved); + } +} + +using cross_off_fn = void (*)(std::uint8_t*, std::size_t, const sieving_prime*, const sieving_prime*, list_array&) noexcept; + +// Indirection keeps the pack expansion simple enough for every front end (nvcc included) +template +constexpr cross_off_fn list_function() noexcept +{ + return &cross_off_list; +} + +template +constexpr std::array make_table(std::index_sequence) noexcept +{ + return {{list_function(I)>()...}}; +} + +inline constexpr std::array table = make_table(std::make_index_sequence<64>{}); + +} // namespace medium_detail + +class erat_medium +{ +public: + void init(std::uint64_t stop) + { + stop_ = stop; + for (auto& l : lists_) + { + l.clear(); + } + for (auto& l : next_) + { + l.clear(); + } + } + + void add(std::uint64_t prime, std::uint64_t segment_low) + { + std::uint64_t multiple_index {}; + unsigned wheel_index {}; + if (first_multiple(wheel30, prime, segment_low, stop_, multiple_index, wheel_index)) + { + sieving_prime sp {}; + sp.prime30 = static_cast(prime / 30); + sp.set(static_cast(multiple_index), wheel_index); + lists_[wheel_index].push_back(sp); + } + } + + void cross_off(std::uint8_t* sieve, std::size_t bytes) + { + for (unsigned w {0}; w < 64; ++w) + { + const std::vector& list {lists_[w]}; + if (!list.empty()) + { + medium_detail::table[w](sieve, bytes, list.data(), list.data() + list.size(), next_); + } + } + for (auto& l : lists_) + { + l.clear(); + } + lists_.swap(next_); + } + +private: + medium_detail::list_array lists_ {}; + medium_detail::list_array next_ {}; + std::uint64_t stop_ {0}; +}; + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_MEDIUM_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/erat_small.hpp b/include/boost/math/special_functions/detail/prime_sieve/erat_small.hpp new file mode 100644 index 0000000000..1965e5ecc6 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/erat_small.hpp @@ -0,0 +1,228 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Small sieving primes (many multiples per segment). The segment is processed in L1-sized +// chunks; for each prime one full wheel turn (8 multiples, exactly p bytes) is unrolled, and +// the state is carried into the next chunk. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_SMALL_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_SMALL_HPP + +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +namespace small_detail { + +// Cumulative byte offsets, bit masks and wheel states of the eight multiples of one wheel turn +// starting at State. +struct turn_chain +{ + std::array factor {}; // multiply by p / 30 + std::array correct {}; // add + std::array mask {}; + std::array state {}; +}; + +template +constexpr turn_chain make_chain() noexcept +{ + turn_chain c {}; + unsigned s {State}; + unsigned f {0}; + unsigned k {0}; + for (unsigned j {0}; j < 8; ++j) + { + c.factor[j] = f; + c.correct[j] = k; + c.mask[j] = wheel30.wheel[s].unset_bit; + c.state[j] = s; + f += wheel30.wheel[s].next_multiple_factor; + k += wheel30.wheel[s].correct; + s = wheel30.wheel[s].next; + } + return c; +} + +// Full wheel turns of a prime in state State over sieve[i, n): eight multiples per iteration +// with one bounds check. Leaves i at the first multiple that does not have a whole turn of room. +template +BOOST_MATH_FORCEINLINE void turn_loop(std::uint8_t* sieve, std::size_t n, std::size_t& i, std::size_t p) noexcept +{ + constexpr turn_chain c {make_chain()}; + constexpr unsigned cls {State / 8}; + const std::size_t turn {30u * p + class_residue(cls)}; + const std::size_t o1 {c.factor[1] * p + c.correct[1]}; + const std::size_t o2 {c.factor[2] * p + c.correct[2]}; + const std::size_t o3 {c.factor[3] * p + c.correct[3]}; + const std::size_t o4 {c.factor[4] * p + c.correct[4]}; + const std::size_t o5 {c.factor[5] * p + c.correct[5]}; + const std::size_t o6 {c.factor[6] * p + c.correct[6]}; + const std::size_t o7 {c.factor[7] * p + c.correct[7]}; + if (n > o7) + { + const std::size_t limit {n - o7}; + std::size_t j {i}; + for (; j < limit; j += turn) + { + sieve[j] &= c.mask[0]; + sieve[j + o1] &= c.mask[1]; + sieve[j + o2] &= c.mask[2]; + sieve[j + o3] &= c.mask[3]; + sieve[j + o4] &= c.mask[4]; + sieve[j + o5] &= c.mask[5]; + sieve[j + o6] &= c.mask[6]; + sieve[j + o7] &= c.mask[7]; + } + i = j; + } +} + +// One step of the chain of class C at phase K: exits with the state when the multiple leaves +// the chunk, otherwise clears the bit and advances to the next phase. +#define BOOST_MATH_PRIME_SIEVE_STEP_BODY(C, K) \ + if (i >= n) \ + { \ + state = (C) * 8 + (K); \ + goto done; \ + } \ + sieve[i] &= wheel30.wheel[(C) * 8 + (K)].unset_bit; \ + i += p * wheel30.wheel[(C) * 8 + (K)].next_multiple_factor + wheel30.wheel[(C) * 8 + (K)].correct; + +#define BOOST_MATH_PRIME_SIEVE_STEP(C, K) \ + step_##C##_##K: \ + BOOST_MATH_PRIME_SIEVE_STEP_BODY(C, K) + +// Entry points of class C: phase 0 goes straight to the unrolled loop, other phases take +// single steps down the chain until it wraps to phase 0. +#define BOOST_MATH_PRIME_SIEVE_CASES(C) \ + case (C) * 8 + 0: goto loop_##C; \ + case (C) * 8 + 1: goto step_##C##_1; \ + case (C) * 8 + 2: goto step_##C##_2; \ + case (C) * 8 + 3: goto step_##C##_3; \ + case (C) * 8 + 4: goto step_##C##_4; \ + case (C) * 8 + 5: goto step_##C##_5; \ + case (C) * 8 + 6: goto step_##C##_6; \ + case (C) * 8 + 7: goto step_##C##_7; + +// The body of class C: whole turns from phase 0, then one chain of single steps that wraps. +// One unrolled loop per class (eight in total) keeps the function within the micro-op cache; +// a loop per state (64) measured 20 percent slower on x86. +#define BOOST_MATH_PRIME_SIEVE_CHAIN(C) \ + loop_##C: \ + turn_loop<(C) * 8>(sieve, n, i, p); \ + BOOST_MATH_PRIME_SIEVE_STEP_BODY(C, 0) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 1) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 2) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 3) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 4) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 5) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 6) \ + BOOST_MATH_PRIME_SIEVE_STEP(C, 7) \ + goto loop_##C; + +// Crosses off every prime of [first, last) over the chunk sieve[0, n) and stores each prime's +// offset into the next chunk. One function for all 64 states (the switch is a jump table). +inline void cross_off_primes(std::uint8_t* sieve, std::size_t n, sieving_prime* first, sieving_prime* last) noexcept +{ + for (sieving_prime* sp {first}; sp != last; ++sp) + { + std::size_t i {sp->multiple_index()}; + unsigned state {sp->wheel_index()}; + if (i >= n) + { + sp->set(i - n, state); + continue; + } + const std::size_t p {static_cast(sp->prime30)}; + switch (state) + { + BOOST_MATH_PRIME_SIEVE_CASES(0) + BOOST_MATH_PRIME_SIEVE_CASES(1) + BOOST_MATH_PRIME_SIEVE_CASES(2) + BOOST_MATH_PRIME_SIEVE_CASES(3) + BOOST_MATH_PRIME_SIEVE_CASES(4) + BOOST_MATH_PRIME_SIEVE_CASES(5) + BOOST_MATH_PRIME_SIEVE_CASES(6) + BOOST_MATH_PRIME_SIEVE_CASES(7) + default: goto done; + } + BOOST_MATH_PRIME_SIEVE_CHAIN(0) + BOOST_MATH_PRIME_SIEVE_CHAIN(1) + BOOST_MATH_PRIME_SIEVE_CHAIN(2) + BOOST_MATH_PRIME_SIEVE_CHAIN(3) + BOOST_MATH_PRIME_SIEVE_CHAIN(4) + BOOST_MATH_PRIME_SIEVE_CHAIN(5) + BOOST_MATH_PRIME_SIEVE_CHAIN(6) + BOOST_MATH_PRIME_SIEVE_CHAIN(7) + done: + sp->set(i - n, state); + } +} + +#undef BOOST_MATH_PRIME_SIEVE_CHAIN +#undef BOOST_MATH_PRIME_SIEVE_CASES +#undef BOOST_MATH_PRIME_SIEVE_STEP +#undef BOOST_MATH_PRIME_SIEVE_STEP_BODY + +} // namespace small_detail + +class erat_small +{ +public: + void init(std::uint64_t stop, std::size_t chunk_bytes) + { + stop_ = stop; + chunk_bytes_ = chunk_bytes; + primes_.clear(); + } + + void add(std::uint64_t prime, std::uint64_t segment_low) + { + std::uint64_t multiple_index {}; + unsigned wheel_index {}; + if (first_multiple(wheel30, prime, segment_low, stop_, multiple_index, wheel_index)) + { + sieving_prime sp {}; + sp.prime30 = static_cast(prime / 30); + sp.set(static_cast(multiple_index), wheel_index); + primes_.push_back(sp); + } + } + + void cross_off(std::uint8_t* sieve, std::size_t bytes) noexcept + { + for (std::size_t chunk {0}; chunk < bytes; chunk += chunk_bytes_) + { + const std::size_t n {(std::min)(chunk_bytes_, bytes - chunk)}; + std::uint8_t* s {sieve + chunk}; + small_detail::cross_off_primes(s, n, primes_.data(), primes_.data() + primes_.size()); + } + } + + bool empty() const noexcept + { + return primes_.empty(); + } + +private: + std::vector primes_; + std::uint64_t stop_ {0}; + std::size_t chunk_bytes_ {32768}; +}; + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_ERAT_SMALL_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/execution.hpp b/include/boost/math/special_functions/detail/prime_sieve/execution.hpp new file mode 100644 index 0000000000..dff5aa5e1d --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/execution.hpp @@ -0,0 +1,116 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Execution policy plumbing: std::execution policies plus a CUDA tag, and the feature +// detection the rest of the sieve depends on (threads, std::execution, exceptions). + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_EXECUTION_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_EXECUTION_HPP + +#include + +// Threads are available unless the library configuration disabled them +#if defined(BOOST_MATH_HAS_THREADS) && !defined(BOOST_MATH_DISABLE_THREADS) +# define BOOST_MATH_PRIME_SIEVE_HAS_THREADS +#endif + +// Whether the host compiler accepts try / catch. BOOST_MATH_NO_EXCEPTIONS is not the right +// question here: it is forced on for CUDA and SYCL because device code cannot throw, while +// the host code in these headers still can. +#if defined(BOOST_NO_EXCEPTIONS) || (defined(__GNUC__) && !defined(__EXCEPTIONS)) || (defined(_MSC_VER) && !defined(_CPPUNWIND)) +# define BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS +#endif + +// std::execution policies. Only the policy types are used, never the parallel algorithms, +// so no TBB is needed. Besides the library-wide check, accept libstdc++ 9 and later, MSVC, +// and libc++ built with -fexperimental-library (which omits the feature-test macro). +#if defined(BOOST_MATH_EXEC_COMPATIBLE) +# define BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +#elif defined(BOOST_MATH_PRIME_SIEVE_HAS_THREADS) && defined(__has_include) +# if __has_include() +# if (defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 9)) || (defined(_MSC_VER) && (_MSC_VER >= 1914)) || (defined(_LIBCPP_VERSION) && defined(_LIBCPP_ENABLE_EXPERIMENTAL)) +# define BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +# endif +# endif +#endif + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +#include +#endif +#endif + +namespace boost::math { + +namespace execution { + +// Requests the CUDA backend; falls back to the CPU when the translation unit is not +// compiled with nvcc and BOOST_MATH_ENABLE_CUDA (test BOOST_MATH_HAS_CUDA_PRIME_SIEVE). +BOOST_MATH_EXPORT struct cuda_policy +{ +}; + +BOOST_MATH_EXPORT inline constexpr cuda_policy cuda {}; + +} // namespace execution + +namespace detail::prime_sieve { + +enum class exec_mode +{ + sequential, + parallel, + cuda +}; + +template +inline constexpr bool is_cuda_policy_v = std::is_same::value; + +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +template +inline constexpr bool is_std_policy_v = std::is_execution_policy::value; +#else +template +inline constexpr bool is_std_policy_v = false; +#endif + +template +inline constexpr bool is_execution_policy_v = is_cuda_policy_v || is_std_policy_v; + +template +constexpr exec_mode mode_of() noexcept +{ + if constexpr (is_cuda_policy_v) + { + return exec_mode::cuda; + } +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION + else if constexpr (std::is_same::value) + { + return exec_mode::sequential; + } +#if defined(__cpp_lib_execution) && (__cpp_lib_execution >= 201902L) + else if constexpr (std::is_same::value) + { + return exec_mode::sequential; + } +#endif + else + { + return exec_mode::parallel; + } +#else + else + { + return exec_mode::sequential; + } +#endif +} + +} // namespace detail::prime_sieve +} // namespace boost::math + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_EXECUTION_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/integer_traits.hpp b/include/boost/math/special_functions/detail/prime_sieve/integer_traits.hpp new file mode 100644 index 0000000000..6d9b4ddd46 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/integer_traits.hpp @@ -0,0 +1,83 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Traits that let the sieve accept builtin integers and Boost.Multiprecision integers +// without including Boost.Multiprecision. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_INTEGER_TRAITS_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_INTEGER_TRAITS_HPP + +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +template +inline constexpr bool is_multiprecision_v = boost::math::tools::detail::has_backend_type::value; + +template +inline constexpr bool is_integer_like_v = std::is_integral::value || (std::numeric_limits::is_specialized && std::numeric_limits::is_integer); + +// Common type of two bounds: the usual promotion for builtins, otherwise the class type. +template +using common_integer_t = std::conditional_t::value && std::is_integral::value, + std::common_type_t, + std::conditional_t::value, Upper, Lower>>; + +// True when the non-negative value x fits in 64 bits. +template +inline bool fits_u64(const Integer& x) +{ + if constexpr (std::is_integral::value) + { + (void)x; + return true; + } + else + { + return x <= Integer(std::numeric_limits::max()); + } +} + +template +inline std::uint64_t to_u64(const Integer& x) +{ + return static_cast(x); +} + +template +inline Integer from_u64(std::uint64_t x) +{ + return static_cast(x); +} + +// Clamps negative inputs of signed types to zero. +template +inline Integer clamp_non_negative(const Integer& x) +{ + if constexpr (std::is_integral::value && std::is_unsigned::value) + { + return x; + } + else + { + return x < Integer(0) ? Integer(0) : x; + } +} +template +inline Integer to_common(const Bound& x) +{ + return Integer(clamp_non_negative(x)); +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_INTEGER_TRAITS_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/layout.hpp b/include/boost/math/special_functions/detail/prime_sieve/layout.hpp new file mode 100644 index 0000000000..3692e9c175 --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/layout.hpp @@ -0,0 +1,364 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Bit layout of the sieve array and the wheel tables shared by every backend. +// +// One byte represents 30 consecutive integers: bit k of byte i is the integer +// segment_low + 30 * i + wheel_offsets[k] with wheel_offsets = {7, 11, 13, 17, 19, 23, 29, 31}. +// segment_low is always a multiple of 30 and the first byte of a range starting at s is the +// one holding s, i.e. segment_low = 30 * ((s - 7) / 30). Multiples of 2, 3 and 5 have no bit. +// +// A sieving prime p = 30 * p30 + r advances through its multiples p * q where q runs over the +// residues coprime to the wheel modulus. The wheel tables give, per (residue class of p, +// residue class of q), the bit to clear, the distance to the next q, and the byte correction. +// +// The tables are host objects; the CUDA backend copies what it needs into a kernel argument. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_LAYOUT_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_LAYOUT_HPP + +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#endif + +// Prime extraction with AVX-512 byte compression (Ice Lake and later, or Zen 4), selected at +// compile time; define BOOST_MATH_PRIME_SIEVE_NO_SIMD to force the portable loop. +#if defined(__AVX512F__) && defined(__AVX512VBMI__) && defined(__AVX512VBMI2__) && !defined(__CUDACC__) && !defined(BOOST_MATH_PRIME_SIEVE_NO_SIMD) +# define BOOST_MATH_PRIME_SIEVE_AVX512_EXTRACT +# ifndef BOOST_MATH_BUILD_MODULE +# include +# endif +#endif + +namespace boost::math::detail::prime_sieve { + +inline constexpr std::uint8_t wheel_offsets[8] = {7, 11, 13, 17, 19, 23, 29, 31}; +inline constexpr std::uint8_t unset_bit[8] = {0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f}; + +// Bit index of a residue modulo 30 that is coprime to 30, or 0xff otherwise. +// The same mapping orders the residue classes of the sieving primes. +BOOST_MATH_GPU_ENABLED constexpr std::uint8_t bit_of_residue(unsigned r) noexcept +{ + return r == 7 ? 0 : r == 11 ? 1 : r == 13 ? 2 : r == 17 ? 3 : r == 19 ? 4 : r == 23 ? 5 : r == 29 ? 6 : r == 1 ? 7 : 0xff; +} + +// Residue modulo 30 of the sieving primes in class c. +BOOST_MATH_GPU_ENABLED constexpr unsigned class_residue(unsigned c) noexcept +{ + return c == 7 ? 1u : wheel_offsets[c]; +} + +namespace layout_detail { + +constexpr std::array make_bit_values() noexcept +{ + std::array a {}; + for (std::size_t b {0}; b < 64; ++b) + { + a[b] = static_cast(30 * (b / 8) + wheel_offsets[b % 8]); + } + a[64] = 0; + return a; +} + +// keep_low[d]: bits of the first byte whose offset is >= d (d = start - segment_low in [7, 36]) +constexpr std::array make_keep_low() noexcept +{ + std::array a {}; + for (unsigned d {0}; d < 37; ++d) + { + std::uint8_t m {0}; + for (unsigned k {0}; k < 8; ++k) + { + if (wheel_offsets[k] >= d) + { + m = static_cast(m | (1u << k)); + } + } + a[d] = m; + } + return a; +} + +// keep_high[e]: bits of the last byte whose offset is <= e (e = stop - byte_base in [0, 36]) +constexpr std::array make_keep_high() noexcept +{ + std::array a {}; + for (unsigned e {0}; e < 37; ++e) + { + std::uint8_t m {0}; + for (unsigned k {0}; k < 8; ++k) + { + if (wheel_offsets[k] <= e) + { + m = static_cast(m | (1u << k)); + } + } + a[e] = m; + } + return a; +} + +constexpr unsigned gcd_u(unsigned a, unsigned b) noexcept +{ + while (b != 0) + { + const unsigned t {a % b}; + a = b; + b = t; + } + return a; +} + +} // namespace layout_detail + +// Offset within a 64-bit word (240 integers) of each bit; entry 64 absorbs countr_zero(0). +inline constexpr std::array bit_values = layout_detail::make_bit_values(); +inline constexpr std::array keep_low = layout_detail::make_keep_low(); +inline constexpr std::array keep_high = layout_detail::make_keep_high(); + +// Wheel tables for a modulus with Size residues coprime to it (30/8 or 210/48). +template +struct wheel_tables +{ + struct init_entry + { + std::uint8_t next_multiple_factor; // distance from q to the next coprime multiplier + std::uint8_t wheel_index; // phase of that multiplier + }; + + struct element + { + std::uint8_t unset_bit; // mask clearing the bit of p * q + std::uint8_t next_multiple_factor; // multiplier increment to the next coprime q + std::uint8_t correct; // byte correction beyond factor * (p / 30) + std::uint16_t next; // following state + }; + + static constexpr unsigned modulo {Modulo}; + static constexpr unsigned size {Size}; + static constexpr unsigned states {8 * Size}; + + std::array residues {}; + std::array init {}; + std::array wheel {}; +}; + +template +constexpr wheel_tables make_wheel_tables() noexcept +{ + wheel_tables t {}; + + unsigned n {0}; + for (unsigned r {0}; r < Modulo; ++r) + { + if (layout_detail::gcd_u(r, Modulo) == 1) + { + t.residues[n++] = static_cast(r); + } + } + + for (unsigned x {0}; x < Modulo; ++x) + { + unsigned y {x}; + unsigned distance {0}; + while (layout_detail::gcd_u(y % Modulo, Modulo) != 1) + { + ++y; + ++distance; + } + unsigned phase {0}; + while (t.residues[phase] != y % Modulo) + { + ++phase; + } + t.init[x].next_multiple_factor = static_cast(distance); + t.init[x].wheel_index = static_cast(phase); + } + + for (unsigned c {0}; c < 8; ++c) + { + const unsigned rp {class_residue(c)}; + for (unsigned i {0}; i < Size; ++i) + { + const unsigned rq {t.residues[i]}; + const unsigned product {(rp * rq) % 30}; + const unsigned bit {bit_of_residue(product)}; + const unsigned i_next {(i + 1) % Size}; + const unsigned factor {(t.residues[i_next] + Modulo - t.residues[i]) % Modulo}; + const unsigned correct {(wheel_offsets[bit] - 6u + factor * rp) / 30u}; + + auto& e = t.wheel[c * Size + i]; + e.unset_bit = unset_bit[bit]; + e.next_multiple_factor = static_cast(factor); + e.correct = static_cast(correct); + e.next = static_cast(c * Size + i_next); + } + } + return t; +} + +using wheel30_t = wheel_tables<30, 8>; +using wheel210_t = wheel_tables<210, 48>; + +inline constexpr wheel30_t wheel30 = make_wheel_tables<30, 8>(); +inline constexpr wheel210_t wheel210 = make_wheel_tables<210, 48>(); + +// Packed sieving prime: 23 bits of byte index, 9 bits of wheel state, and p / 30. +struct sieving_prime +{ + static constexpr std::uint32_t max_multiple_index {(1u << 23) - 1}; + + std::uint32_t indexes; + std::uint32_t prime30; + + constexpr std::size_t multiple_index() const noexcept + { + return indexes & max_multiple_index; + } + + constexpr unsigned wheel_index() const noexcept + { + return indexes >> 23; + } + + constexpr void set(std::size_t multiple_index, unsigned wheel_index) noexcept + { + indexes = static_cast(multiple_index) | (static_cast(wheel_index) << 23); + } +}; + +// Computes the byte index (relative to segment_low) and wheel state of the first multiple of +// prime that is at least max(prime * prime, segment_low + 7) and coprime to the wheel modulus. +// Returns false when no such multiple is <= stop; the outputs are then meaningless. +// Host only: device code uses its own copy of the tables (see cuda.hpp). +template +inline bool first_multiple(const Tables& t, std::uint64_t prime, std::uint64_t segment_low, + std::uint64_t stop, std::uint64_t& multiple_index, unsigned& wheel_index) noexcept +{ + // Offsets 7..31 shifted by 6 divide cleanly into bytes 0.. + // Written without early returns: for short windows at large magnitudes about half of the + // primes have no multiple in range and a data-dependent branch here would mispredict. + const std::uint64_t low6 {segment_low + 6}; + std::uint64_t q {low6 / prime + 1}; + q = q < prime ? prime : q; + // prime * q lies in (low6, low6 + prime] or equals prime * prime (< 2^64), so a wrapped + // product is below low6: one comparison replaces a second 64-bit division + const std::uint64_t multiple {prime * q}; + const bool in_range {static_cast(static_cast(multiple >= low6) & static_cast(multiple <= stop))}; + const auto e = t.init[q % Tables::modulo]; + const std::uint64_t advance {prime * e.next_multiple_factor}; + const bool fits {static_cast(static_cast(in_range) & static_cast(advance <= stop - multiple))}; + multiple_index = (multiple + advance - low6) / 30; + wheel_index = bit_of_residue(static_cast(prime % 30)) * Tables::size + e.wheel_index; + return fits; +} + +#ifdef BOOST_MATH_PRIME_SIEVE_AVX512_EXTRACT + +// Writes the integers whose bits are set in word (base value low) to out and returns how many. +// The 64 byte offsets are compressed by the bit mask so the set bits' offsets become the first +// count bytes; each group of eight is then spread into 64-bit lanes and added to the base. +// Writes whole groups of eight, so out needs 7 slots of slack beyond the count. +inline std::size_t extract_word(std::uint64_t word, std::uint64_t low, std::uint64_t* out) noexcept +{ + const std::size_t count {static_cast(boost::math::tools::popcount(word))}; + if (count == 0) + { + return 0; + } + const __m512i offsets {_mm512_loadu_si512(bit_values.data())}; + const __m512i compressed {_mm512_maskz_compress_epi8(static_cast<__mmask64>(word), offsets)}; + const __m512i base {_mm512_set1_epi64(static_cast(low))}; + __m512i index {_mm512_set_epi64(7, 6, 5, 4, 3, 2, 1, 0)}; + const __m512i eight {_mm512_set1_epi64(8)}; + for (std::size_t j {0}; j < count; j += 8) + { + // byte 0 of each 64-bit lane receives compressed[8 * group + lane] + const __m512i values {_mm512_maskz_permutexvar_epi8(static_cast<__mmask64>(0x0101010101010101ULL), index, compressed)}; + _mm512_storeu_si512(out + j, _mm512_add_epi64(values, base)); + index = _mm512_add_epi64(index, eight); + } + return count; +} + +// Same for 32-bit outputs: sixteen values per group, so 15 slots of slack are needed. +inline std::size_t extract_word_u32(std::uint64_t word, std::uint64_t low, std::uint32_t* out) noexcept +{ + const std::size_t count {static_cast(boost::math::tools::popcount(word))}; + if (count == 0) + { + return 0; + } + const __m512i offsets {_mm512_loadu_si512(bit_values.data())}; + const __m512i compressed {_mm512_maskz_compress_epi8(static_cast<__mmask64>(word), offsets)}; + const __m512i base {_mm512_set1_epi32(static_cast(static_cast(low)))}; + __m512i index {_mm512_set_epi32(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)}; + const __m512i sixteen {_mm512_set1_epi32(16)}; + for (std::size_t j {0}; j < count; j += 16) + { + const __m512i values {_mm512_maskz_permutexvar_epi8(static_cast<__mmask64>(0x1111111111111111ULL), index, compressed)}; + _mm512_storeu_si512(out + j, _mm512_add_epi32(values, base)); + index = _mm512_add_epi32(index, sixteen); + } + return count; +} + +#else + +// Writes the integers whose bits are set in word (base value low) to out and returns how many. +// Writes four values per iteration unconditionally, so out needs 3 slots of slack beyond +// the count (64 bits at most, so 67 slots always suffice). +inline std::size_t extract_word(std::uint64_t word, std::uint64_t low, std::uint64_t* out) noexcept +{ + const std::size_t count {static_cast(boost::math::tools::popcount(word))}; + std::size_t j {0}; + while (j < count) + { + out[j] = low + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 1] = low + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 2] = low + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 3] = low + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + j += 4; + } + return count; +} + +// Same as extract_word for values known to fit 32 bits (sieving primes). +inline std::size_t extract_word_u32(std::uint64_t word, std::uint64_t low, std::uint32_t* out) noexcept +{ + const std::size_t count {static_cast(boost::math::tools::popcount(word))}; + const std::uint32_t base {static_cast(low)}; + std::size_t j {0}; + while (j < count) + { + out[j] = base + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 1] = base + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 2] = base + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + out[j + 3] = base + bit_values[boost::math::tools::countr_zero(word)]; + word &= word - 1; + j += 4; + } + return count; +} + +#endif // BOOST_MATH_PRIME_SIEVE_AVX512_EXTRACT + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_LAYOUT_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/options.hpp b/include/boost/math/special_functions/detail/prime_sieve/options.hpp new file mode 100644 index 0000000000..cba491b83b --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/options.hpp @@ -0,0 +1,137 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// User-facing tuning options and the derived segment geometry. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_OPTIONS_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_OPTIONS_HPP + +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#endif + +namespace boost::math { + +// Strategy for ranges that fit in 64 bits: sieve to sqrt(upper) or test survivors of a +// shallow sieve individually (better for short intervals at large magnitudes). +BOOST_MATH_EXPORT enum class prime_range_strategy +{ + automatic, + full_sieve, + test_survivors +}; + +BOOST_MATH_EXPORT struct prime_sieve_options +{ + std::size_t l1d_bytes {0}; // 0 = detect + std::size_t l2_bytes {0}; // 0 = detect (per core) + std::size_t sieve_bytes {0}; // 0 = derive from sqrt(upper) and the caches; clamped to [16 KiB, 8 MiB] + unsigned max_threads {0}; // 0 = std::thread::hardware_concurrency() + std::size_t chunk_primes {1u << 19}; // target primes per parallel chunk when storing output + prime_range_strategy range_strategy {prime_range_strategy::automatic}; + bool probable_prime_only {false}; // beyond 2^64: skip the deterministic pseudosquares test +}; + +namespace detail::prime_sieve { + +inline constexpr std::size_t min_sieve_bytes {16u * 1024u}; +inline constexpr std::size_t max_sieve_bytes {8u * 1024u * 1024u}; // 23-bit multiple index +inline constexpr double factor_small {0.2}; +inline constexpr double factor_medium {3.0}; +inline constexpr double factor_sieve_size {2.0}; + +struct sieve_geometry +{ + std::uint64_t start {7}; // >= 7 + std::uint64_t stop {7}; // inclusive + std::uint64_t sqrt_stop {2}; + std::size_t sieve_bytes {min_sieve_bytes}; // multiple of 8, power of two when big primes exist + std::size_t l1_bytes {32768}; // erat_small chunk + std::uint64_t max_small {0}; // primes <= max_small go to erat_small + std::uint64_t max_medium {0}; // primes <= max_medium go to erat_medium, larger to erat_big + bool has_big {false}; +}; + +inline std::size_t round_down_multiple(std::size_t value, std::size_t unit) noexcept +{ + return unit == 0 ? value : (value / unit) * unit; +} + +inline sieve_geometry make_geometry(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options) noexcept +{ + sieve_geometry g {}; + g.start = start < 7 ? 7 : start; + g.stop = stop; + g.sqrt_stop = boost::math::tools::isqrt(stop); + + const cache_info& cache {cached_cache_info()}; + const std::size_t l1 {options.l1d_bytes != 0 ? options.l1d_bytes : cache.l1d_bytes}; + const std::size_t l2 {options.l2_bytes != 0 ? options.l2_bytes : cache.l2_bytes}; + + std::size_t sieve_bytes {}; + if (options.sieve_bytes != 0) + { + sieve_bytes = options.sieve_bytes; + } + else + { + // Scale with sqrt(stop) so that medium primes have several multiples per segment + const double wanted {factor_sieve_size * static_cast(g.sqrt_stop)}; + sieve_bytes = wanted > static_cast(max_sieve_bytes) ? max_sieve_bytes : static_cast(wanted); + if (sieve_bytes > l1) + { + sieve_bytes = round_down_multiple(sieve_bytes, l1); + } + // Keep the segment inside the per-core L2 and within 16 L1 sizes + std::size_t cap {static_cast(boost::math::tools::floor_pow2(l2))}; + cap = (std::min)(cap, 16 * l1); + cap = (std::max)(cap, l1); + sieve_bytes = (std::max)(sieve_bytes, l1); + sieve_bytes = (std::min)(sieve_bytes, cap); + } + sieve_bytes = (std::max)(sieve_bytes, min_sieve_bytes); + sieve_bytes = (std::min)(sieve_bytes, max_sieve_bytes); + + // A range that fits in one segment does not need more memory than that, but the segment + // must stay large enough that no prime falls into the big class if it can be avoided + if (stop - g.start < 30u * static_cast(sieve_bytes)) + { + std::size_t shrunk {static_cast((stop - g.start) / 30 + 2)}; + const std::size_t no_big {static_cast(static_cast(g.sqrt_stop) / factor_medium) + 1}; + shrunk = (std::max)(shrunk, no_big); + sieve_bytes = (std::min)(sieve_bytes, shrunk); + sieve_bytes = (std::max)(sieve_bytes, std::size_t(8)); + } + sieve_bytes = (sieve_bytes + 7) / 8 * 8; + + // Big primes need a power-of-two segment for cheap bucket indexing. The bucket sieve + // is not cache bound, and the largest segment measured fastest, so use the maximum + // unless the caller fixed the size. + if (static_cast(g.sqrt_stop) > factor_medium * static_cast(sieve_bytes)) + { + sieve_bytes = options.sieve_bytes != 0 ? static_cast(boost::math::tools::floor_pow2(sieve_bytes)) : max_sieve_bytes; + sieve_bytes = (std::max)(sieve_bytes, min_sieve_bytes); + } + + g.sieve_bytes = sieve_bytes; + g.l1_bytes = (std::min)(l1, sieve_bytes); + g.max_small = static_cast(factor_small * static_cast(g.l1_bytes)); + g.max_medium = static_cast(factor_medium * static_cast(sieve_bytes)); + g.max_small = (std::min)(g.max_small, g.sqrt_stop); + g.max_medium = (std::min)(g.max_medium, g.sqrt_stop); + g.has_big = g.sqrt_stop > g.max_medium; + return g; +} + +} // namespace detail::prime_sieve +} // namespace boost::math + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_OPTIONS_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/parallel.hpp b/include/boost/math/special_functions/detail/prime_sieve/parallel.hpp new file mode 100644 index 0000000000..4947b5168a --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/parallel.hpp @@ -0,0 +1,408 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Multithreaded driver: the range is cut into chunks of about 200 * sqrt(stop) that are +// handed out dynamically through an atomic counter; each worker owns a segment_sieve and +// shares the read-only sieving primes. Output chunks are committed in order through a +// bounded queue so the caller sees ascending primes without padding. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PARALLEL_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PARALLEL_HPP + +#include +#include + +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_THREADS + +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +inline constexpr std::uint64_t min_chunk_width {10000000}; + +inline std::vector sieving_primes_upto_parallel(std::uint64_t n, const prime_sieve_options& options); + +struct chunk_plan +{ + std::uint64_t start {0}; + std::uint64_t stop {0}; + std::uint64_t chunk {0}; // width in integers, multiple of 30 + std::uint64_t iters {1}; + unsigned threads {1}; +}; + +inline unsigned worker_count(const prime_sieve_options& options) noexcept +{ + if (options.max_threads != 0) + { + return options.max_threads; + } + const unsigned hw {std::thread::hardware_concurrency()}; + return hw == 0 ? 2u : hw; +} + +// primesieve's balancing: chunks of at most 200 * sqrt(stop) so the O(pi(sqrt stop)) setup +// per chunk stays below one percent, an iteration count that is a multiple of the thread +// count, and a floor of 1e7 integers per chunk. +inline chunk_plan plan_chunks(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options, bool extracting) noexcept +{ + chunk_plan plan {}; + plan.start = start; + plan.stop = stop; + const std::uint64_t dist {stop - start + 1}; + const unsigned hw {worker_count(options)}; + const std::uint64_t root {boost::math::tools::isqrt(stop)}; + const std::uint64_t threshold {(std::max)(root / 5, min_chunk_width)}; + std::uint64_t threads {dist / threshold}; + threads = (std::max)(threads, std::uint64_t(1)); + threads = (std::min)(threads, static_cast(hw)); + if (threads <= 1) + { + plan.chunk = dist; + plan.iters = 1; + plan.threads = 1; + return plan; + } + + std::uint64_t chunk {(std::min)(200 * root, dist / threads)}; + if (extracting) + { + // Keep one chunk's worth of primes near chunk_primes entries + const double per_chunk {static_cast(options.chunk_primes) * std::log(static_cast(stop))}; + if (per_chunk < static_cast(chunk)) + { + chunk = static_cast(per_chunk); + } + } + chunk = (std::max)(chunk, min_chunk_width); + std::uint64_t iters {dist / chunk}; + iters = (iters / threads) * threads; + iters = (std::max)(iters, threads); + chunk = (dist - 1) / iters + 1; + chunk = (std::max)(chunk, min_chunk_width); + chunk += 30 - chunk % 30; + iters = (dist - 1) / chunk + 1; + + plan.chunk = chunk; + plan.iters = iters; + plan.threads = static_cast((std::min)(threads, iters)); + return plan; +} + +// Bounds of chunk i, inclusive. +inline void chunk_bounds(const chunk_plan& plan, std::uint64_t i, std::uint64_t& lo, std::uint64_t& hi) noexcept +{ + lo = plan.start + i * plan.chunk; + const std::uint64_t remaining {plan.stop - lo}; + hi = remaining < plan.chunk ? plan.stop : lo + plan.chunk - 1; +} + +// Counts the primes in [start, stop] (start >= 7) on several threads. +inline std::uint64_t parallel_count(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options) +{ + if (stop < start) + { + return 0; + } + const chunk_plan plan {plan_chunks(start, stop, options, false)}; + const sieve_geometry g {make_geometry(start, stop, options)}; + const std::vector primes {sieving_primes_upto_parallel(g.sqrt_stop, options)}; + if (plan.threads <= 1) + { + // Too narrow to split, but the sieving primes above were still generated in parallel + segment_sieve engine {g, primes}; + count_sink sink {}; + engine.run(sink); + return sink.count; + } + + alignas(64) std::atomic next {0}; + + auto worker = [&]() -> std::uint64_t + { + segment_sieve engine {g, primes}; + count_sink sink {}; + std::uint64_t i {}; + while ((i = next.fetch_add(1, std::memory_order_relaxed)) < plan.iters) + { + std::uint64_t lo {}; + std::uint64_t hi {}; + chunk_bounds(plan, i, lo, hi); + engine.reset_range(lo, hi); + engine.run(sink); + } + return sink.count; + }; + + std::vector> futures; + futures.reserve(plan.threads); + for (unsigned t {0}; t < plan.threads; ++t) + { +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + try +#endif + { + futures.push_back(std::async(std::launch::async, worker)); + } +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + catch (const std::system_error&) + { + break; + } +#endif + } + // Any chunks left over (thread creation failed) are processed here + std::uint64_t total {worker()}; + for (auto& f : futures) + { + total += f.get(); + } + return total; +} + +// In-order hand-off of per-chunk prime buffers from workers to the consuming thread. +class ordered_chunk_queue +{ +public: + explicit ordered_chunk_queue(std::size_t capacity) : slots_(capacity), ready_(capacity, false) + { + } + + // Returns the empty buffer for chunk i once the chunk that previously used the slot is drained. + std::vector& acquire(std::uint64_t i) + { + std::unique_lock lock {mutex_}; + free_.wait(lock, [&] { return failed_ || i < next_ + slots_.size(); }); + return slots_[static_cast(i % slots_.size())]; + } + + void publish(std::uint64_t i) + { + { + std::lock_guard lock {mutex_}; + ready_[static_cast(i % slots_.size())] = true; + } + ready_cv_.notify_all(); + } + + void fail(std::exception_ptr e) + { + { + std::lock_guard lock {mutex_}; + if (!failed_) + { + failed_ = true; + error_ = e; + } + } + ready_cv_.notify_all(); + free_.notify_all(); + } + + bool failed() const + { + std::lock_guard lock {mutex_}; + return failed_; + } + + // Blocks until chunk i is published; returns nullptr when a worker failed. + std::vector* wait_ready(std::uint64_t i) + { + std::unique_lock lock {mutex_}; + ready_cv_.wait(lock, [&] { return failed_ || ready_[static_cast(i % slots_.size())]; }); + if (failed_) + { + return nullptr; + } + return &slots_[static_cast(i % slots_.size())]; + } + + void release(std::uint64_t i) + { + { + std::lock_guard lock {mutex_}; + const std::size_t s {static_cast(i % slots_.size())}; + ready_[s] = false; + slots_[s].clear(); + next_ = i + 1; + } + free_.notify_all(); + } + + void rethrow() + { + std::exception_ptr e {}; + { + std::lock_guard lock {mutex_}; + e = error_; + } + if (e) + { + std::rethrow_exception(e); + } + } + +private: + mutable std::mutex mutex_; + std::condition_variable ready_cv_; + std::condition_variable free_; + std::vector> slots_; + std::vector ready_; + std::uint64_t next_ {0}; + bool failed_ {false}; + std::exception_ptr error_ {}; +}; + +// Sieves [start, stop] (start >= 7) on several threads and passes the primes in ascending +// order to consume(const std::uint64_t*, std::size_t) on the calling thread. +template +void parallel_range(std::uint64_t start, std::uint64_t stop, const prime_sieve_options& options, Consumer& consume) +{ + if (stop < start) + { + return; + } + const chunk_plan plan {plan_chunks(start, stop, options, true)}; + const sieve_geometry g {make_geometry(start, stop, options)}; + const std::vector primes {sieving_primes_upto_parallel(g.sqrt_stop, options)}; + if (plan.threads <= 1) + { + segment_sieve engine {g, primes}; + extract_sink sink {consume}; + engine.run(sink); + return; + } + + alignas(64) std::atomic next {0}; + ordered_chunk_queue queue {2 * static_cast(plan.threads)}; + + auto worker = [&]() + { +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + try +#endif + { + segment_sieve engine {g, primes}; + std::uint64_t i {}; + while ((i = next.fetch_add(1, std::memory_order_relaxed)) < plan.iters) + { + std::uint64_t lo {}; + std::uint64_t hi {}; + chunk_bounds(plan, i, lo, hi); + std::vector& buffer {queue.acquire(i)}; + if (queue.failed()) + { + return; + } + buffer.reserve(static_cast(prime_count_upper_bound(lo, hi))); + append_u64 appender {buffer}; + extract_sink sink {appender}; + engine.reset_range(lo, hi); + engine.run(sink); + queue.publish(i); + } + } +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + catch (...) + { + queue.fail(std::current_exception()); + } +#endif + }; + + std::vector> futures; + futures.reserve(plan.threads); + for (unsigned t {0}; t < plan.threads; ++t) + { +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + try +#endif + { + futures.push_back(std::async(std::launch::async, worker)); + } +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + catch (const std::system_error&) + { + break; + } +#endif + } + if (futures.empty()) + { + // No worker could be started: fall back to the sequential path + extract_sink sink {consume}; + run_u64(start, stop, options, sink); + return; + } + +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + try +#endif + { + for (std::uint64_t i {0}; i < plan.iters; ++i) + { + std::vector* buffer {queue.wait_ready(i)}; + if (buffer == nullptr) + { + break; + } + consume(buffer->data(), buffer->size()); + queue.release(i); + } + } +#ifndef BOOST_MATH_PRIME_SIEVE_NO_EXCEPTIONS + catch (...) + { + // The consumer threw: release the workers blocked on the full queue before waiting + // on the futures, whose destructors would otherwise block forever. + queue.fail(std::current_exception()); + for (auto& f : futures) + { + f.wait(); + } + throw; + } +#endif + for (auto& f : futures) + { + f.wait(); + } + queue.rethrow(); +} + +// Sieving primes in [167, n] generated on several threads when there are many of them. +inline std::vector sieving_primes_upto_parallel(std::uint64_t n, const prime_sieve_options& options) +{ + if (n < 100000000) + { + return sieving_primes_upto(n); + } + std::vector out; + out.reserve(static_cast(prime_count_upper_bound(n))); + append_u32 appender {out}; + parallel_range(167, n, options, appender); + return out; +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_PRIME_SIEVE_HAS_THREADS +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PARALLEL_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/presieve.hpp b/include/boost/math/special_functions/detail/prime_sieve/presieve.hpp new file mode 100644 index 0000000000..360e56adaf --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/presieve.hpp @@ -0,0 +1,268 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Pre-sieving: multiples of the primes 7..163 are removed by ANDing periodic byte patterns +// into each segment instead of crossing them off one by one. The 16 patterns (products of +// two or three primes, 6 to 10 KB each) are generated on first use. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRESIEVE_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRESIEVE_HPP + +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +inline constexpr unsigned presieve_max_prime {163}; +inline constexpr std::size_t presieve_table_count {16}; + +// The 35 primes handled by pre-sieving, 7..163. +inline constexpr std::uint8_t presieve_primes[35] = +{ + 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163 +}; + +// Groups whose products give tables of 6 to 10 KB each. +inline constexpr std::uint8_t presieve_groups[presieve_table_count][3] = +{ + {7, 23, 37}, {11, 19, 31}, {13, 17, 29}, {41, 163, 0}, {43, 157, 0}, {47, 151, 0}, {53, 149, 0}, {59, 139, 0}, + {61, 137, 0}, {67, 131, 0}, {71, 127, 0}, {73, 113, 0}, {79, 109, 0}, {83, 107, 0}, {89, 103, 0}, {97, 101, 0} +}; + +// Crosses the multiples of p off a pattern of period bytes covering the integers +// [0, 30 * period + 1]. Usable at compile time. +constexpr void presieve_mark(std::uint8_t* pattern, std::size_t period, std::uint64_t p) noexcept +{ + constexpr std::uint8_t residues[8] = {1, 7, 11, 13, 17, 19, 23, 29}; + const std::uint64_t limit {30 * static_cast(period) + 1}; + for (std::uint64_t base {0}; ; base += 30) + { + for (unsigned k {0}; k < 8; ++k) + { + const std::uint64_t n {p * (base + residues[k])}; + if (n > limit) + { + return; + } + pattern[static_cast((n - 7) / 30)] = static_cast(pattern[(n - 7) / 30] & unset_bit[bit_of_residue(static_cast(n % 30))]); + } + } +} + +template +struct presieve_pattern +{ + std::uint8_t bytes[Period]; +}; + +// The pattern for one group of primes; the third prime may be 1 (absent). +template +constexpr presieve_pattern make_presieve_pattern() noexcept +{ + presieve_pattern t {}; + for (std::size_t i {0}; i < A * B * C; ++i) + { + t.bytes[i] = 0xff; + } + presieve_mark(t.bytes, A * B * C, A); + presieve_mark(t.bytes, A * B * C, B); + if (C != 1) + { + presieve_mark(t.bytes, A * B * C, C); + } + return t; +} + +// View of the 16 patterns used by presieve_segment. +struct presieve_tables +{ + const std::uint8_t* table[presieve_table_count]; + std::size_t period[presieve_table_count]; +}; + +// The patterns are constant expressions on compilers whose constexpr step limits allow it. +// MSVC (100000 steps by default) and the nvcc front end fall back to generation at first use. +#if !defined(BOOST_MATH_PRIME_SIEVE_RUNTIME_PRESIEVE) && !defined(_MSC_VER) && !defined(__CUDACC__) +# define BOOST_MATH_PRIME_SIEVE_CONSTEXPR_PRESIEVE +#endif + +#ifdef BOOST_MATH_PRIME_SIEVE_CONSTEXPR_PRESIEVE + +namespace presieve_detail { + +inline constexpr auto pattern_0 = make_presieve_pattern<7, 23, 37>(); +inline constexpr auto pattern_1 = make_presieve_pattern<11, 19, 31>(); +inline constexpr auto pattern_2 = make_presieve_pattern<13, 17, 29>(); +inline constexpr auto pattern_3 = make_presieve_pattern<41, 163, 1>(); +inline constexpr auto pattern_4 = make_presieve_pattern<43, 157, 1>(); +inline constexpr auto pattern_5 = make_presieve_pattern<47, 151, 1>(); +inline constexpr auto pattern_6 = make_presieve_pattern<53, 149, 1>(); +inline constexpr auto pattern_7 = make_presieve_pattern<59, 139, 1>(); +inline constexpr auto pattern_8 = make_presieve_pattern<61, 137, 1>(); +inline constexpr auto pattern_9 = make_presieve_pattern<67, 131, 1>(); +inline constexpr auto pattern_10 = make_presieve_pattern<71, 127, 1>(); +inline constexpr auto pattern_11 = make_presieve_pattern<73, 113, 1>(); +inline constexpr auto pattern_12 = make_presieve_pattern<79, 109, 1>(); +inline constexpr auto pattern_13 = make_presieve_pattern<83, 107, 1>(); +inline constexpr auto pattern_14 = make_presieve_pattern<89, 103, 1>(); +inline constexpr auto pattern_15 = make_presieve_pattern<97, 101, 1>(); + +inline constexpr presieve_tables tables +{ + { + pattern_0.bytes, pattern_1.bytes, pattern_2.bytes, pattern_3.bytes, pattern_4.bytes, pattern_5.bytes, pattern_6.bytes, pattern_7.bytes, + pattern_8.bytes, pattern_9.bytes, pattern_10.bytes, pattern_11.bytes, pattern_12.bytes, pattern_13.bytes, pattern_14.bytes, pattern_15.bytes + }, + { + sizeof(pattern_0.bytes), sizeof(pattern_1.bytes), sizeof(pattern_2.bytes), sizeof(pattern_3.bytes), sizeof(pattern_4.bytes), sizeof(pattern_5.bytes), sizeof(pattern_6.bytes), sizeof(pattern_7.bytes), + sizeof(pattern_8.bytes), sizeof(pattern_9.bytes), sizeof(pattern_10.bytes), sizeof(pattern_11.bytes), sizeof(pattern_12.bytes), sizeof(pattern_13.bytes), sizeof(pattern_14.bytes), sizeof(pattern_15.bytes) + } +}; + +} // namespace presieve_detail + +inline const presieve_tables& get_presieve_tables() noexcept +{ + return presieve_detail::tables; +} + +#else + +namespace presieve_detail { + +struct runtime_tables +{ + std::vector storage[presieve_table_count]; + presieve_tables view {}; + + runtime_tables() + { + for (std::size_t k {0}; k < presieve_table_count; ++k) + { + std::size_t period {1}; + for (const std::uint8_t p : presieve_groups[k]) + { + if (p != 0) + { + period *= p; + } + } + storage[k].assign(period, 0xff); + for (const std::uint8_t p : presieve_groups[k]) + { + if (p != 0) + { + presieve_mark(storage[k].data(), period, p); + } + } + view.table[k] = storage[k].data(); + view.period[k] = period; + } + } +}; + +} // namespace presieve_detail + +inline const presieve_tables& get_presieve_tables() +{ + static const presieve_detail::runtime_tables tables {}; + return tables.view; +} + +#endif + +// Applies four tables to sieve[0, bytes) starting at segment_low; the first pass assigns, +// later passes AND. Runs are cut at table wrap points so the inner loops auto-vectorize. +template +inline void presieve_pass(std::uint8_t* sieve, std::size_t bytes, std::uint64_t segment_low, + const presieve_tables& t, std::size_t first_table) +{ + const std::uint8_t* tab[4] {}; + std::size_t pos[4] {}; + std::size_t per[4] {}; + for (std::size_t k {0}; k < 4; ++k) + { + tab[k] = t.table[first_table + k]; + per[k] = t.period[first_table + k]; + pos[k] = static_cast((segment_low / 30) % per[k]); + } + + std::size_t i {0}; + while (i < bytes) + { + std::size_t run {bytes - i}; + for (std::size_t k {0}; k < 4; ++k) + { + run = (std::min)(run, per[k] - pos[k]); + } + const std::uint8_t* t0 {tab[0] + pos[0]}; + const std::uint8_t* t1 {tab[1] + pos[1]}; + const std::uint8_t* t2 {tab[2] + pos[2]}; + const std::uint8_t* t3 {tab[3] + pos[3]}; + std::uint8_t* s {sieve + i}; + if (Assign) + { + for (std::size_t j {0}; j < run; ++j) + { + s[j] = static_cast(t0[j] & t1[j] & t2[j] & t3[j]); + } + } + else + { + for (std::size_t j {0}; j < run; ++j) + { + s[j] = static_cast(s[j] & t0[j] & t1[j] & t2[j] & t3[j]); + } + } + i += run; + for (std::size_t k {0}; k < 4; ++k) + { + pos[k] += run; + if (pos[k] == per[k]) + { + pos[k] = 0; + } + } + } +} + +// Initializes sieve[0, bytes) for the segment at segment_low with multiples of 7..163 removed, +// then restores the bits of those primes themselves when the segment contains them. +inline void presieve_segment(std::uint8_t* sieve, std::size_t bytes, std::uint64_t segment_low) +{ + const presieve_tables& t {get_presieve_tables()}; + presieve_pass(sieve, bytes, segment_low, t, 0); + presieve_pass(sieve, bytes, segment_low, t, 4); + presieve_pass(sieve, bytes, segment_low, t, 8); + presieve_pass(sieve, bytes, segment_low, t, 12); + + if (segment_low <= presieve_max_prime) + { + for (const std::uint8_t p : presieve_primes) + { + if (p >= segment_low + 7) + { + const std::size_t byte {static_cast((p - segment_low - 7) / 30)}; + if (byte < bytes) + { + sieve[byte] = static_cast(sieve[byte] | (1u << bit_of_residue(p % 30))); + } + } + } + } +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRESIEVE_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/primality.hpp b/include/boost/math/special_functions/detail/prime_sieve/primality.hpp new file mode 100644 index 0000000000..f3b0dbc1ed --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/primality.hpp @@ -0,0 +1,825 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Primality tests used by the prime sieve: +// * deterministic Miller-Rabin for 64-bit integers +// * generic modular arithmetic, Jacobi symbol, integer roots +// * Baillie-PSW probable prime test +// * the Lukes-Patterson-Williams pseudosquares test (deterministic, table bounded) +// The generic templates require an Integer type that can hold m * m for the modulus m +// in use (Boost.Multiprecision integers qualify). Builtin 64-bit values use the +// dedicated *_u64 functions. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRIMALITY_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRIMALITY_HPP + +#include + +#ifndef BOOST_MATH_HAS_NVRTC + +#include +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#if defined(_MSC_VER) && !defined(__clang__) && defined(_M_X64) +# include +#endif +#endif + +namespace boost::math::detail::prime_sieve { + +// The 71 primes not exceeding 353: bases for the pseudosquares test and trial division. +inline constexpr std::uint16_t small_primes_to_353[71] = +{ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, + 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, + 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353 +}; + +// (a + b) mod m for a, b < m without overflow. +inline std::uint64_t addmod_u64(std::uint64_t a, std::uint64_t b, std::uint64_t m) noexcept +{ + return a >= m - b ? a - (m - b) : a + b; +} + +// (a * b) mod m for any 64-bit operands. +inline std::uint64_t mulmod_u64(std::uint64_t a, std::uint64_t b, std::uint64_t m) noexcept +{ +#if defined(BOOST_MATH_HAS_INT128) + return static_cast((static_cast(a) * b) % m); +#elif defined(_MSC_VER) && !defined(__clang__) && defined(_M_X64) + std::uint64_t high {}; + const std::uint64_t low {_umul128(a, b, &high)}; + if (high >= m) + { + high %= m; + } + std::uint64_t remainder {}; + _udiv128(high, low, m, &remainder); + return remainder; +#else + a %= m; + b %= m; + std::uint64_t result {0}; + while (b != 0) + { + if (b & 1u) + { + result = addmod_u64(result, a, m); + } + a = addmod_u64(a, a, m); + b >>= 1; + } + return result; +#endif +} + +// base^exp mod m. +inline std::uint64_t powm_u64(std::uint64_t base, std::uint64_t exp, std::uint64_t m) noexcept +{ + std::uint64_t result {1 % m}; + base %= m; + while (exp != 0) + { + if (exp & 1u) + { + result = mulmod_u64(result, base, m); + } + base = mulmod_u64(base, base, m); + exp >>= 1; + } + return result; +} + +// Deterministic Miller-Rabin for all 64-bit values with the seven bases of Sinclair. +// Slower than is_prime_u64; kept as an independent oracle for testing. +inline bool is_prime_u64_miller_rabin(std::uint64_t n) noexcept +{ + if (n < 2) + { + return false; + } + for (std::size_t i {0}; i < 12; ++i) + { + const std::uint64_t p {small_primes_to_353[i]}; + if (n % p == 0) + { + return n == p; + } + } + if (n < 41u * 41u) + { + return true; + } + + constexpr std::uint64_t bases[7] = {2u, 325u, 9375u, 28178u, 450775u, 9780504u, 1795265022u}; + const int r {boost::math::tools::countr_zero(n - 1)}; + const std::uint64_t d {(n - 1) >> r}; + + for (const std::uint64_t base : bases) + { + const std::uint64_t a {base % n}; + if (a == 0) + { + continue; + } + std::uint64_t x {powm_u64(a, d, n)}; + if (x == 1 || x == n - 1) + { + continue; + } + bool composite {true}; + for (int i {1}; i < r; ++i) + { + x = mulmod_u64(x, x, n); + if (x == n - 1) + { + composite = false; + break; + } + } + if (composite) + { + return false; + } + } + return true; +} + +// Jacobi symbol (a / n) for odd n > 0. +inline int jacobi_u64(std::uint64_t a, std::uint64_t n) noexcept +{ + a %= n; + int result {1}; + while (a != 0) + { + const int twos {boost::math::tools::countr_zero(a)}; + a >>= twos; + if ((twos & 1) != 0) + { + const std::uint64_t r {n % 8}; + if (r == 3 || r == 5) + { + result = -result; + } + } + const std::uint64_t t {a}; + a = n; + n = t; + if (a % 4 == 3 && n % 4 == 3) + { + result = -result; + } + a %= n; + } + return n == 1 ? result : 0; +} + +// Montgomery arithmetic modulo an odd 64-bit n. Where no 128-bit product is available +// the "Montgomery" form degenerates to plain residues with a slow multiply. +class montgomery_u64 +{ +public: + explicit montgomery_u64(std::uint64_t n) noexcept : n_(n) + { +#if defined(BOOST_MATH_HAS_INT128) || (defined(_MSC_VER) && !defined(__clang__) && defined(_M_X64)) + // Newton iteration for n^-1 mod 2^64: n is odd so n * n == 1 (mod 8) seeds 3 bits + std::uint64_t inv {n}; + for (int i {0}; i < 5; ++i) + { + inv *= 2u - n * inv; + } + n_inv_neg_ = 0u - inv; + one_ = (0u - n) % n; // 2^64 mod n + r2_ = mulmod_u64(one_, one_, n); // 2^128 mod n +#else + one_ = 1; + r2_ = 1; +#endif + } + + std::uint64_t modulus() const noexcept + { + return n_; + } + + std::uint64_t one() const noexcept + { + return one_; + } + + std::uint64_t to_form(std::uint64_t a) const noexcept + { + return mul(a % n_, r2_); + } + + std::uint64_t add(std::uint64_t a, std::uint64_t b) const noexcept + { + return addmod_u64(a, b, n_); + } + + std::uint64_t sub(std::uint64_t a, std::uint64_t b) const noexcept + { + return a >= b ? a - b : a + (n_ - b); + } + + // a / 2 modulo n (n odd) + std::uint64_t half(std::uint64_t a) const noexcept + { + return (a & 1u) ? (a >> 1) + (n_ >> 1) + 1 : a >> 1; + } + + std::uint64_t mul(std::uint64_t a, std::uint64_t b) const noexcept + { +#if defined(BOOST_MATH_HAS_INT128) + const unsigned __int128 t {static_cast(a) * b}; + const std::uint64_t m {static_cast(t) * n_inv_neg_}; + const unsigned __int128 mn {static_cast(m) * n_}; + const std::uint64_t t_hi {static_cast(t >> 64)}; + const std::uint64_t mn_hi {static_cast(mn >> 64)}; + return reduce(t_hi, mn_hi, static_cast(t) != 0); +#elif defined(_MSC_VER) && !defined(__clang__) && defined(_M_X64) + std::uint64_t t_hi {}; + const std::uint64_t t_lo {_umul128(a, b, &t_hi)}; + const std::uint64_t m {t_lo * n_inv_neg_}; + std::uint64_t mn_hi {}; + _umul128(m, n_, &mn_hi); + return reduce(t_hi, mn_hi, t_lo != 0); +#else + return mulmod_u64(a, b, n_); +#endif + } + + std::uint64_t pow(std::uint64_t base, std::uint64_t exp) const noexcept + { + std::uint64_t result {one_}; + while (exp != 0) + { + if (exp & 1u) + { + result = mul(result, base); + } + base = mul(base, base); + exp >>= 1; + } + return result; + } + +private: + // The low halves of t and m * n sum to a multiple of 2^64, so only the carry matters. + std::uint64_t reduce(std::uint64_t t_hi, std::uint64_t mn_hi, bool carry) const noexcept + { + const std::uint64_t r {t_hi + mn_hi + (carry ? 1u : 0u)}; + if (r < t_hi || r >= n_) + { + return r - n_; + } + return r; + } + + std::uint64_t n_; + std::uint64_t n_inv_neg_ {0}; + std::uint64_t one_; + std::uint64_t r2_; +}; + +// Deterministic primality for all 64-bit values: trial division, then Baillie-PSW (a strong +// base-2 test and a strong Lucas test with Selfridge's parameters), which has been verified +// to have no counterexample below 2^64. +inline bool is_prime_u64(std::uint64_t n) noexcept +{ + if (n < 2) + { + return false; + } + for (std::size_t i {0}; i < 16; ++i) + { + const std::uint64_t p {small_primes_to_353[i]}; + if (n % p == 0) + { + return n == p; + } + } + if (n < 59u * 59u) + { + return true; + } + + const montgomery_u64 mont {n}; + const std::uint64_t one {mont.one()}; + const std::uint64_t minus_one {n - one}; + + // strong Fermat test to base 2 + const int r {boost::math::tools::countr_zero(n - 1)}; + const std::uint64_t d {(n - 1) >> r}; + std::uint64_t x {mont.pow(mont.to_form(2), d)}; + if (x != one && x != minus_one) + { + bool witness {true}; + for (int i {1}; i < r; ++i) + { + x = mont.mul(x, x); + if (x == minus_one) + { + witness = false; + break; + } + } + if (witness) + { + return false; + } + } + + // perfect squares would make the search for D below loop forever + const std::uint64_t root {boost::math::tools::isqrt(n)}; + if (root * root == n) + { + return false; + } + + // Selfridge method A: D = 5, -7, 9, -11, ... with (D / n) == -1; P = 1, Q = (1 - D) / 4 + long d_small {5}; + while (true) + { + const std::uint64_t magnitude {static_cast(d_small < 0 ? -d_small : d_small)}; + int j {jacobi_u64(magnitude % n, n)}; + if (d_small < 0 && n % 4 == 3) + { + j = -j; + } + if (j == -1) + { + break; + } + if (j == 0 && magnitude % n != 0) + { + return false; + } + d_small = d_small > 0 ? -(d_small + 2) : -(d_small - 2); + } + const long q_small {(1 - d_small) / 4}; + const std::uint64_t d_res {d_small < 0 ? n - (static_cast(-d_small) % n) : static_cast(d_small) % n}; + const std::uint64_t q_res {q_small < 0 ? n - (static_cast(-q_small) % n) : static_cast(q_small) % n}; + const std::uint64_t big_d {mont.to_form(d_res)}; + const std::uint64_t big_q {mont.to_form(q_res)}; + + const int s {boost::math::tools::countr_zero(n + 1)}; + const std::uint64_t k {(n + 1) >> s}; + std::uint64_t u {one}; + std::uint64_t v {one}; + std::uint64_t qk {big_q}; + for (int i {63 - boost::math::tools::countl_zero(k)}; i-- > 0;) + { + u = mont.mul(u, v); + v = mont.sub(mont.mul(v, v), mont.add(qk, qk)); + qk = mont.mul(qk, qk); + if (((k >> i) & 1u) != 0) + { + const std::uint64_t u_next {mont.half(mont.add(u, v))}; + const std::uint64_t v_next {mont.half(mont.add(mont.mul(big_d, u), v))}; + u = u_next; + v = v_next; + qk = mont.mul(qk, big_q); + } + } + if (u == 0 || v == 0) + { + return true; + } + for (int i {1}; i < s; ++i) + { + v = mont.sub(mont.mul(v, v), mont.add(qk, qk)); + if (v == 0) + { + return true; + } + qk = mont.mul(qk, qk); + } + return false; +} + +// Reduces x into [0, m) for signed Integer types. +template +inline Integer mod_positive(Integer x, const Integer& m) +{ + x %= m; + if (x < 0) + { + x += m; + } + return x; +} + +// base^exp mod m by square and multiply. +template +Integer powm(Integer base, Integer exp, const Integer& m) +{ + Integer result {1}; + base = mod_positive(base, m); + while (exp > 0) + { + if (exp % 2 == 1) + { + result = (result * base) % m; + } + exp /= 2; + if (exp > 0) + { + base = (base * base) % m; + } + } + return result; +} + +// Jacobi symbol (a / n) for a >= 0 and odd n > 0. +template +int jacobi(Integer a, Integer n) +{ + a %= n; + int result {1}; + while (a != 0) + { + while (a % 2 == 0) + { + a /= 2; + const Integer r {n % 8}; + if (r == 3 || r == 5) + { + result = -result; + } + } + std::swap(a, n); + if (a % 4 == 3 && n % 4 == 3) + { + result = -result; + } + a %= n; + } + return n == 1 ? result : 0; +} + +// Jacobi symbol for a small signed numerator. +template +int jacobi_signed(long a, const Integer& n) +{ + if (a < 0) + { + const int sign {n % 4 == 3 ? -1 : 1}; + return sign * jacobi(Integer(-a), n); + } + return jacobi(Integer(a), n); +} + +// Number of significant bits in n >= 0. +template +unsigned bit_length(Integer n) +{ + unsigned bits {0}; + while (n > 0) + { + n /= 2; + ++bits; + } + return bits; +} + +// Largest r with r * r <= n. +template +Integer isqrt(const Integer& n) +{ + if (n < 2) + { + return n; + } + Integer x {1}; + x <<= (bit_length(n) + 1) / 2; + while (true) + { + const Integer y {(x + n / x) / 2}; + if (y >= x) + { + return x; + } + x = y; + } +} + +// Largest r with r^k <= n, for k >= 2. +template +Integer iroot(const Integer& n, unsigned k) +{ + if (k == 2) + { + return isqrt(n); + } + Integer low {1}; + Integer high {1}; + high <<= (bit_length(n) / k + 1); + while (low < high) + { + const Integer mid {(low + high + 1) / 2}; + Integer power {1}; + bool too_big {false}; + for (unsigned i {0}; i < k; ++i) + { + power *= mid; + if (power > n) + { + too_big = true; + break; + } + } + if (too_big) + { + high = mid - 1; + } + else + { + low = mid; + } + } + return low; +} + +// True when n == r^k for some r and some prime k <= max_k. +template +bool is_perfect_power(const Integer& n, unsigned max_k) +{ + for (std::size_t i {0}; i < 71 && small_primes_to_353[i] <= max_k; ++i) + { + const unsigned k {small_primes_to_353[i]}; + const Integer r {iroot(n, k)}; + Integer power {1}; + for (unsigned j {0}; j < k; ++j) + { + power *= r; + } + if (power == n) + { + return true; + } + } + return false; +} + +// Strong Fermat probable prime test to base 2 for odd n > 2. +template +bool strong_fermat_base2(const Integer& n) +{ + Integer d {n - 1}; + unsigned r {0}; + while (d % 2 == 0) + { + d /= 2; + ++r; + } + Integer x {powm(Integer(2), d, n)}; + if (x == 1 || x == n - 1) + { + return true; + } + for (unsigned i {1}; i < r; ++i) + { + x = (x * x) % n; + if (x == n - 1) + { + return true; + } + } + return false; +} + +// Strong Lucas probable prime test with Selfridge's method A parameters for odd n > 2. +// n must not be a perfect square (the D search would not terminate). +template +bool strong_lucas_selfridge(const Integer& n) +{ + // Find D = 5, -7, 9, -11, ... with (D / n) == -1 + long d_small {5}; + while (true) + { + const int j {jacobi_signed(d_small, n)}; + if (j == -1) + { + break; + } + if (j == 0) + { + // gcd(|D|, n) > 1: composite unless n is |D| itself + return n == Integer(d_small < 0 ? -d_small : d_small); + } + d_small = d_small > 0 ? -(d_small + 2) : -(d_small - 2); + } + + const Integer big_d {static_cast(d_small)}; + const Integer big_q {mod_positive(Integer((1 - d_small) / 4), n)}; + + Integer d {n + 1}; + unsigned s {0}; + while (d % 2 == 0) + { + d /= 2; + ++s; + } + + // Binary ladder for U_d, V_d and Q^d with P == 1 + Integer u {1}; + Integer v {1}; + Integer qk {big_q}; + const unsigned bits {bit_length(d)}; + for (unsigned i {bits - 1}; i-- > 0;) + { + // k -> 2k + u = (u * v) % n; + v = mod_positive(Integer(v * v - 2 * qk), n); + qk = (qk * qk) % n; + if (((d >> i) & 1) == 1) + { + // k -> k + 1 + Integer u_next {u + v}; + if (u_next % 2 != 0) + { + u_next += n; + } + u_next /= 2; + Integer v_next {big_d * u + v}; + v_next = mod_positive(v_next, n); + if (v_next % 2 != 0) + { + v_next += n; + } + v_next /= 2; + u = u_next % n; + v = v_next % n; + qk = (qk * big_q) % n; + } + } + + if (u == 0 || v == 0) + { + return true; + } + for (unsigned i {1}; i < s; ++i) + { + v = mod_positive(Integer(v * v - 2 * qk), n); + if (v == 0) + { + return true; + } + qk = (qk * qk) % n; + } + return false; +} + +// Baillie-PSW probable prime test: no counterexample is known, deterministic below 2^64. +template +bool is_probable_prime_bpsw(const Integer& n) +{ + if (n < 2) + { + return false; + } + for (std::size_t i {0}; i < 71; ++i) + { + const Integer p {small_primes_to_353[i]}; + if (n % p == 0) + { + return n == p; + } + } + if (n < Integer(359u * 359u)) + { + return true; + } + if (!strong_fermat_base2(n)) + { + return false; + } + const Integer r {isqrt(n)}; + if (r * r == n) + { + return false; + } + return strong_lucas_selfridge(n); +} + +// Pseudosquares L_p: the least non-square m == 1 (mod 8) that is a quadratic residue modulo +// every odd prime q <= p. Each row stores the smallest prime attaining a distinct value, so +// choosing the first row with L_p > n / s uses the fewest bases. Values from Hugh Williams, +// "Edouard Lucas and Primality Testing", table 16.3.1, extended by Wooding (2003). +struct pseudosquare_entry +{ + std::uint16_t p; + std::uint64_t high; + std::uint64_t low; +}; + +inline constexpr pseudosquare_entry pseudosquares[49] = +{ + { 3, 0, 73ULL }, { 5, 0, 241ULL }, { 7, 0, 1009ULL }, + { 11, 0, 2641ULL }, { 13, 0, 8089ULL }, { 17, 0, 18001ULL }, + { 19, 0, 53881ULL }, { 23, 0, 87481ULL }, { 29, 0, 117049ULL }, + { 31, 0, 515761ULL }, { 37, 0, 1083289ULL }, { 41, 0, 3206641ULL }, + { 43, 0, 3818929ULL }, { 47, 0, 9257329ULL }, { 53, 0, 22000801ULL }, + { 59, 0, 48473881ULL }, { 67, 0, 175244281ULL }, { 71, 0, 427733329ULL }, + { 79, 0, 898716289ULL }, { 83, 0, 2805544681ULL }, { 101, 0, 10310263441ULL }, + { 103, 0, 23616331489ULL }, { 107, 0, 85157610409ULL }, { 113, 0, 196265095009ULL }, + { 131, 0, 2871842842801ULL }, { 149, 0, 26250887023729ULL }, { 157, 0, 112434732901969ULL }, + { 173, 0, 178936222537081ULL }, { 181, 0, 696161110209049ULL }, { 193, 0, 2854909648103881ULL }, + { 197, 0, 6450045516630769ULL }, { 211, 0, 11641399247947921ULL }, { 227, 0, 190621428905186449ULL }, + { 229, 0, 196640148121928601ULL }, { 233, 0, 712624335095093521ULL }, { 239, 0, 1773855791877850321ULL }, + { 241, 0, 2327687064124474441ULL }, { 251, 0, 6384991873059836689ULL }, { 257, 0, 8019204661305419761ULL }, + { 263, 0, 10198100582046287689ULL }, + { 277, 0x3ULL, 0xc956f827e0524359ULL }, // 69848288320900186969 + { 281, 0xbULL, 0x539315b3b1268d59ULL }, // 208936365799044975961 + { 283, 0x1cULL, 0xec87d86ca60b50a1ULL }, // 533552663339828203681 + { 293, 0x32ULL, 0xc6d3496f20db3d81ULL }, // 936664079266714697089 + { 311, 0x74ULL, 0x210967a12ba94be1ULL }, // 2142202860370269916129 + { 331, 0x2e3ULL, 0xec11ddc09fd65c51ULL }, // 13649154491558298803281 + { 337, 0x753ULL, 0x641c14b397c27bf1ULL }, // 34594858801670127778801 + { 347, 0x1511ULL, 0x85fdf38d1fc9ce21ULL }, // 99492945930479213334049 + { 353, 0x3e8bULL, 0xaba417e222ca5091ULL } // 295363187400900310880401 +}; + +// Builds the pseudosquare value of a table row in the Integer type. +template +Integer pseudosquare_value(std::size_t index) +{ + Integer value {pseudosquares[index].high}; + value <<= 32; + value <<= 32; + value += Integer(pseudosquares[index].low); + return value; +} + +// Smallest table index whose pseudosquare exceeds n_over_s, or 49 when the table is exhausted. +template +std::size_t pseudosquare_index_for(const Integer& n_over_s) +{ + for (std::size_t i {0}; i < 49; ++i) + { + if (pseudosquare_value(i) > n_over_s) + { + return i; + } + } + return 49; +} + +// Lukes-Patterson-Williams test. Preconditions: n odd, n has no prime factor <= s, +// s >= 2, and pseudosquares[index] satisfies L_p > n / s. Returns true iff n is prime. +// Conditions (Sorenson, "The pseudosquares prime sieve", theorem 2.1): +// (1) q^((n-1)/2) == +-1 (mod n) for every prime q <= p +// (2) 2^((n-1)/2) == -1 (mod n) when n == 5 (mod 8) +// (3) some q gives -1 when n == 1 (mod 8) +// which prove n is a prime or a prime power; the perfect power check removes the latter. +template +bool pseudosquares_prime_test(const Integer& n, std::size_t index, std::uint64_t s) +{ + const unsigned p {pseudosquares[index].p}; + const Integer exponent {(n - 1) / 2}; + const Integer n_mod_8 {n % 8}; + bool saw_minus_one {false}; + + for (std::size_t i {0}; i < 71 && small_primes_to_353[i] <= p; ++i) + { + const Integer q {small_primes_to_353[i]}; + const Integer e {powm(q, exponent, n)}; + if (e == n - 1) + { + saw_minus_one = true; + } + else if (e != 1) + { + return false; + } + else if (i == 0 && n_mod_8 == 5) + { + return false; + } + } + if (n_mod_8 == 1 && !saw_minus_one) + { + return false; + } + + // Any prime power q^k with q > s has k <= log(n) / log(s) + const unsigned log2_s {s < 2 ? 1u : static_cast(63 - boost::math::tools::countl_zero(s))}; + const unsigned max_k {bit_length(n) / log2_s}; + if (max_k >= 2 && is_perfect_power(n, max_k)) + { + return false; + } + return true; +} + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_HAS_NVRTC +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_PRIMALITY_HPP diff --git a/include/boost/math/special_functions/detail/prime_sieve/sinks.hpp b/include/boost/math/special_functions/detail/prime_sieve/sinks.hpp new file mode 100644 index 0000000000..830e3f98be --- /dev/null +++ b/include/boost/math/special_functions/detail/prime_sieve/sinks.hpp @@ -0,0 +1,219 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Sinks consume finished segments: count set bits, or extract the primes into a +// 64-bit buffer that a consumer converts to the user's integer type. + +#ifndef BOOST_MATH_SF_DETAIL_PRIME_SIEVE_SINKS_HPP +#define BOOST_MATH_SF_DETAIL_PRIME_SIEVE_SINKS_HPP + +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#endif + +namespace boost::math::detail::prime_sieve { + +// Sink contract: segment(words, n_words, low) receives the final bits of one segment whose +// first byte represents low + {7, ..., 31}; bits outside the requested range are already clear. +struct count_sink +{ + std::uint64_t count {0}; + + void segment(const std::uint64_t* words, std::size_t n_words, std::uint64_t) noexcept + { + count += count_bits(words, n_words); + } + + // Without a hardware popcount instruction the compiler expands popcount into a library + // call or a long bit trick per word; a Harley-Seal carry-save adder over 8 words needs + // about a third of the operations (Hacker's Delight, 2nd edition, chapter 5). + static std::uint64_t count_bits(const std::uint64_t* words, std::size_t n) noexcept + { +#if defined(__x86_64__) && !defined(__POPCNT__) + std::uint64_t total {0}; + std::uint64_t ones {0}; + std::uint64_t twos {0}; + std::uint64_t fours {0}; + std::size_t i {0}; + for (; i + 8 <= n; i += 8) + { + std::uint64_t twos_a {}; + std::uint64_t twos_b {}; + std::uint64_t fours_a {}; + std::uint64_t fours_b {}; + csa(ones, twos_a, ones, words[i], words[i + 1]); + csa(ones, twos_b, ones, words[i + 2], words[i + 3]); + csa(twos, fours_a, twos, twos_a, twos_b); + csa(ones, twos_a, ones, words[i + 4], words[i + 5]); + csa(ones, twos_b, ones, words[i + 6], words[i + 7]); + csa(twos, fours_b, twos, twos_a, twos_b); + std::uint64_t eights {}; + csa(fours, eights, fours, fours_a, fours_b); + total += static_cast(swar_popcount(eights)); + } + total = 8 * total + 4 * swar_popcount(fours) + 2 * swar_popcount(twos) + swar_popcount(ones); + for (; i < n; ++i) + { + total += static_cast(swar_popcount(words[i])); + } + return total; +#else + std::uint64_t c {0}; + for (std::size_t i {0}; i < n; ++i) + { + c += static_cast(boost::math::tools::popcount(words[i])); + } + return c; +#endif + } + + static void csa(std::uint64_t& sum, std::uint64_t& carry, std::uint64_t a, std::uint64_t b, std::uint64_t c) noexcept + { + const std::uint64_t u {a ^ b}; + sum = u ^ c; + carry = (a & b) | (u & c); + } + + static std::uint64_t swar_popcount(std::uint64_t x) noexcept + { + x = x - ((x >> 1) & 0x5555555555555555ULL); + x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL); + x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fULL; + return (x * 0x0101010101010101ULL) >> 56; + } + + void flush() noexcept + { + } +}; + +// Extracts primes into a buffer of batch entries and hands full batches to Consumer, +// which is called as consume(const std::uint64_t* primes, std::size_t n). +template +class extract_sink +{ +public: + static constexpr std::size_t slack {68}; + + explicit extract_sink(Consumer& consume, std::size_t batch = 65536) : consume_(consume), batch_(batch) + { + buffer_.resize(batch + slack); + } + + void segment(const std::uint64_t* words, std::size_t n_words, std::uint64_t low) + { + const unsigned char* bytes {reinterpret_cast(words)}; + for (std::size_t w {0}; w < n_words; ++w) + { + if (size_ >= batch_) + { + flush(); + } + const std::uint64_t word {boost::math::tools::load_le64(bytes + 8 * w)}; + size_ += extract_word(word, low + 240u * w, buffer_.data() + size_); + } + } + + void flush() + { + if (size_ != 0) + { + consume_(buffer_.data(), size_); + size_ = 0; + } + } + +private: + Consumer& consume_; + std::vector buffer_; + std::size_t size_ {0}; + std::size_t batch_; +}; + +// Consumer that converts to Integer and writes through an output iterator. +template +struct output_converter +{ + OutputIterator out; + + void operator()(const std::uint64_t* primes, std::size_t n) + { + for (std::size_t i {0}; i < n; ++i) + { + *out = static_cast(primes[i]); + ++out; + } + } +}; + +// Writes 32-bit primes straight into a pre-sized vector (sieving prime generation), avoiding +// the 64-bit batch buffer and the conversion pass. +class extract_sink_u32 +{ +public: + // capacity must be an upper bound on the number of primes plus 68 slots of slack + extract_sink_u32(std::vector& out, std::size_t capacity) : out_(out) + { + out_.resize(capacity); + } + + void segment(const std::uint64_t* words, std::size_t n_words, std::uint64_t low) noexcept + { + const unsigned char* bytes {reinterpret_cast(words)}; + std::uint32_t* dst {out_.data()}; + for (std::size_t w {0}; w < n_words; ++w) + { + const std::uint64_t word {boost::math::tools::load_le64(bytes + 8 * w)}; + size_ += extract_word_u32(word, low + 240u * w, dst + size_); + } + } + + void flush() + { + out_.resize(size_); + } + +private: + std::vector& out_; + std::size_t size_ {0}; +}; + +// Consumer that appends 32-bit values to a vector (sieving primes). +struct append_u32 +{ + std::vector& out; + + void operator()(const std::uint64_t* primes, std::size_t n) + { + const std::size_t old_size {out.size()}; + out.resize(old_size + n); + std::uint32_t* dst {out.data() + old_size}; + for (std::size_t i {0}; i < n; ++i) + { + dst[i] = static_cast(primes[i]); + } + } +}; + +// Consumer that appends 64-bit values to a vector. +struct append_u64 +{ + std::vector& out; + + void operator()(const std::uint64_t* primes, std::size_t n) + { + out.insert(out.end(), primes, primes + n); + } +}; + +} // namespace boost::math::detail::prime_sieve + +#endif // BOOST_MATH_SF_DETAIL_PRIME_SIEVE_SINKS_HPP diff --git a/include/boost/math/special_functions/prime_sieve.hpp b/include/boost/math/special_functions/prime_sieve.hpp new file mode 100644 index 0000000000..541c277428 --- /dev/null +++ b/include/boost/math/special_functions/prime_sieve.hpp @@ -0,0 +1,360 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Segmented sieve of Eratosthenes with wheel factorization. +// +// prime_sieve(policy, upper, out) primes in [2, upper) +// prime_range(policy, lower, upper, out) primes in [lower, upper) +// prime_count(policy, upper) number of primes in [2, upper) +// prime_count(policy, lower, upper) number of primes in [lower, upper) +// +// Integer may be any builtin integer or a Boost.Multiprecision integer. The policy is a +// std::execution policy (seq / unseq sequential, par / par_unseq threaded) or +// boost::math::execution::cuda. Requires C++17. + +#ifndef BOOST_MATH_SPECIAL_FUNCTIONS_PRIME_SIEVE_HPP +#define BOOST_MATH_SPECIAL_FUNCTIONS_PRIME_SIEVE_HPP + +#include + +#ifndef BOOST_MATH_HAS_NVRTC + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef BOOST_MATH_BUILD_MODULE +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace boost::math { + +namespace detail::prime_sieve { + +template +struct is_std_vector : std::false_type +{ +}; + +template +struct is_std_vector> : std::true_type +{ +}; + +// Runs the 64-bit engine on [first, last] with the backend selected by Mode, feeding +// consume(const std::uint64_t*, std::size_t) in ascending order. +template +void run_mode(std::uint64_t first, std::uint64_t last, const prime_sieve_options& options, Consumer& consume) +{ + if (prefer_test_path(first, last, options)) + { + test_range_u64(first, last, consume); + return; + } +#ifdef BOOST_MATH_HAS_CUDA_PRIME_SIEVE + if constexpr (Mode == exec_mode::cuda) + { + cuda_range(first, last, options, consume); + return; + } +#endif +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_THREADS + if constexpr (Mode != exec_mode::sequential) + { + parallel_range(first, last, options, consume); + return; + } +#endif + extract_sink sink {consume}; + run_u64(first, last, options, sink); +} + +template +std::uint64_t count_mode(std::uint64_t first, std::uint64_t last, const prime_sieve_options& options) +{ + if (prefer_test_path(first, last, options)) + { + struct counter + { + std::uint64_t count {0}; + void operator()(const std::uint64_t*, std::size_t n) noexcept + { + count += n; + } + } consume {}; + test_range_u64(first, last, consume); + return consume.count; + } +#ifdef BOOST_MATH_HAS_CUDA_PRIME_SIEVE + if constexpr (Mode == exec_mode::cuda) + { + return cuda_count(first, last, options); + } +#endif +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_THREADS + if constexpr (Mode != exec_mode::sequential) + { + return parallel_count(first, last, options); + } +#endif + return count_u64(first, last, options); +} + +// Emits 2, 3, 5 as needed and narrows [lower, upper) to the engine's [first, last] (first >= 7). +// Returns false when nothing beyond the small primes remains. +template +bool prepare_bounds(Integer& lower, const Integer& upper, Small&& small, Integer& start, Integer& stop) +{ + lower = clamp_non_negative(lower); + if (lower < Integer(2)) + { + lower = Integer(2); + } + if (!(lower < upper)) + { + return false; + } + for (const unsigned q : {2u, 3u, 5u}) + { + if (lower <= Integer(q) && Integer(q) < upper) + { + small(q); + } + } + start = lower < Integer(7) ? Integer(7) : lower; + stop = upper - Integer(1); + return !(stop < start); +} + +// Appends the primes in [lower, upper) to a vector, converting from 64-bit values in batches. +template +void range_to_vector(Integer lower, Integer upper, std::vector& out, const prime_sieve_options& options) +{ + Integer start {}; + Integer stop {}; + if (!prepare_bounds(lower, upper, [&](unsigned q) { out.push_back(static_cast(q)); }, start, stop)) + { + return; + } + struct appender + { + std::vector& v; + void operator()(const std::uint64_t* primes, std::size_t n) + { + v.insert(v.end(), primes, primes + n); + } + } consume {out}; + if (fits_u64(stop)) + { + run_mode(to_u64(start), to_u64(stop), options, consume); + return; + } + if constexpr (!std::is_integral::value) + { + if (fits_u64(start)) + { + // Head below 2^64 through the engine (2^64 - 1 is composite), tail by the window method + run_mode(to_u64(start), (std::numeric_limits::max)(), options, consume); + start = Integer((std::numeric_limits::max)()) + Integer(1); + } + big_range_impl(start, stop, options, [&](const Integer& p) { out.push_back(static_cast(p)); }); + } +} + +// Writes the primes in [lower, upper) to out and returns the advanced iterator. +template +OutputIterator range_dispatch(Integer lower, Integer upper, OutputIterator out, const prime_sieve_options& options) +{ + Integer start {}; + Integer stop {}; + if (!prepare_bounds(lower, upper, [&](unsigned q) { *out = Integer(q); ++out; }, start, stop)) + { + return out; + } + output_converter consumer {out}; + if (fits_u64(stop)) + { + run_mode(to_u64(start), to_u64(stop), options, consumer); + return consumer.out; + } + if constexpr (std::is_integral::value) + { + return consumer.out; + } + else + { + if (fits_u64(start)) + { + run_mode(to_u64(start), (std::numeric_limits::max)(), options, consumer); + start = Integer((std::numeric_limits::max)()) + Integer(1); + } + return big_range(start, stop, consumer.out, options); + } +} + +// Counts the primes in [lower, upper). +template +std::uint64_t count_dispatch(Integer lower, Integer upper, const prime_sieve_options& options) +{ + std::uint64_t count {0}; + Integer start {}; + Integer stop {}; + if (!prepare_bounds(lower, upper, [&](unsigned) { ++count; }, start, stop)) + { + return count; + } + if (fits_u64(stop)) + { + return count + count_mode(to_u64(start), to_u64(stop), options); + } + if constexpr (std::is_integral::value) + { + return count; + } + else + { + if (fits_u64(start)) + { + count += count_mode(to_u64(start), (std::numeric_limits::max)(), options); + start = Integer((std::numeric_limits::max)()) + Integer(1); + } + return count + big_count(start, stop, options); + } +} + +} // namespace detail::prime_sieve + +BOOST_MATH_EXPORT template > && + !detail::prime_sieve::is_std_vector>::value, bool> = true> +inline OutputIterator prime_sieve(ExecutionPolicy&&, Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {}) +{ + constexpr auto mode = detail::prime_sieve::mode_of>(); + return detail::prime_sieve::range_dispatch(Integer(0), upper_bound, out, options); +} + +BOOST_MATH_EXPORT template && + !detail::prime_sieve::is_std_vector>::value, bool> = true> +inline OutputIterator prime_sieve(Integer upper_bound, OutputIterator out, const prime_sieve_options& options = {}) +{ + return detail::prime_sieve::range_dispatch(Integer(0), upper_bound, out, options); +} + +// Vector overloads append in bulk, which is noticeably faster than a back_insert_iterator. +BOOST_MATH_EXPORT template >, bool> = true> +inline void prime_sieve(ExecutionPolicy&&, Integer upper_bound, std::vector& out, const prime_sieve_options& options = {}) +{ + constexpr auto mode = detail::prime_sieve::mode_of>(); + detail::prime_sieve::range_to_vector(Integer(0), upper_bound, out, options); +} + +BOOST_MATH_EXPORT template , bool> = true> +inline void prime_sieve(Integer upper_bound, std::vector& out, const prime_sieve_options& options = {}) +{ + detail::prime_sieve::range_to_vector(Integer(0), upper_bound, out, options); +} + +// The two bounds may have different integer types (e.g. a std::uint64_t and a literal); +// they are converted to their common type first. +BOOST_MATH_EXPORT template > && + detail::prime_sieve::is_integer_like_v && detail::prime_sieve::is_integer_like_v && + !detail::prime_sieve::is_std_vector>::value, bool> = true> +inline OutputIterator prime_range(ExecutionPolicy&&, Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + constexpr auto mode = detail::prime_sieve::mode_of>(); + return detail::prime_sieve::range_dispatch(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), out, options); +} + +BOOST_MATH_EXPORT template && detail::prime_sieve::is_integer_like_v && + !detail::prime_sieve::is_std_vector>::value, bool> = true> +inline OutputIterator prime_range(Lower lower_bound, Upper upper_bound, OutputIterator out, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + return detail::prime_sieve::range_dispatch(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), out, options); +} + +BOOST_MATH_EXPORT template > && + detail::prime_sieve::is_integer_like_v && detail::prime_sieve::is_integer_like_v, bool> = true> +inline void prime_range(ExecutionPolicy&&, Lower lower_bound, Upper upper_bound, std::vector& out, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + constexpr auto mode = detail::prime_sieve::mode_of>(); + detail::prime_sieve::range_to_vector(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), out, options); +} + +BOOST_MATH_EXPORT template && detail::prime_sieve::is_integer_like_v, bool> = true> +inline void prime_range(Lower lower_bound, Upper upper_bound, std::vector& out, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + detail::prime_sieve::range_to_vector(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), out, options); +} + +BOOST_MATH_EXPORT template >, bool> = true> +inline std::uint64_t prime_count(ExecutionPolicy&&, Integer upper_bound, const prime_sieve_options& options = {}) +{ + constexpr auto mode = detail::prime_sieve::mode_of>(); + return detail::prime_sieve::count_dispatch(Integer(0), upper_bound, options); +} + +BOOST_MATH_EXPORT template > && + detail::prime_sieve::is_integer_like_v && detail::prime_sieve::is_integer_like_v, bool> = true> +inline std::uint64_t prime_count(ExecutionPolicy&&, Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + constexpr auto mode = detail::prime_sieve::mode_of>(); + return detail::prime_sieve::count_dispatch(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), options); +} + +BOOST_MATH_EXPORT template , bool> = true> +inline std::uint64_t prime_count(Integer upper_bound, const prime_sieve_options& options = {}) +{ + return detail::prime_sieve::count_dispatch(Integer(0), upper_bound, options); +} + +BOOST_MATH_EXPORT template && detail::prime_sieve::is_integer_like_v, bool> = true> +inline std::uint64_t prime_count(Lower lower_bound, Upper upper_bound, const prime_sieve_options& options = {}) +{ + using Integer = detail::prime_sieve::common_integer_t; + return detail::prime_sieve::count_dispatch(detail::prime_sieve::to_common(lower_bound), detail::prime_sieve::to_common(upper_bound), options); +} + +// Reserves room for every prime below upper_bound (Dusart's bound on the prime counting function). +BOOST_MATH_EXPORT template +inline void prime_reserve(Integer upper_bound, std::vector& primes) +{ + upper_bound = detail::prime_sieve::clamp_non_negative(upper_bound); + if (detail::prime_sieve::fits_u64(upper_bound)) + { + primes.reserve(static_cast(detail::prime_sieve::prime_count_upper_bound(detail::prime_sieve::to_u64(upper_bound)))); + } +} + +} // namespace boost::math + +#endif // BOOST_MATH_HAS_NVRTC +#endif // BOOST_MATH_SPECIAL_FUNCTIONS_PRIME_SIEVE_HPP diff --git a/include/boost/math/tools/bit.hpp b/include/boost/math/tools/bit.hpp new file mode 100644 index 0000000000..8b4fd19568 --- /dev/null +++ b/include/boost/math/tools/bit.hpp @@ -0,0 +1,148 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Portable bit manipulation helpers usable from host and device code. +// Everything here works on 64-bit unsigned values and never relies on +// function-local static tables, so it is safe under CUDA and SYCL. + +#ifndef BOOST_MATH_TOOLS_BIT_HPP +#define BOOST_MATH_TOOLS_BIT_HPP + +#include +#include + +#ifndef BOOST_MATH_HAS_NVRTC +#ifndef BOOST_MATH_BUILD_MODULE +#if defined(__has_include) +# if __has_include() +# include +# endif +#endif +#if defined(__cpp_lib_bitops) && (__cpp_lib_bitops >= 201907L) +# include +#endif +#if defined(_MSC_VER) && !defined(__clang__) +# include +#endif +#endif // BOOST_MATH_BUILD_MODULE +#endif // BOOST_MATH_HAS_NVRTC + +namespace boost { namespace math { namespace tools { + +// Number of trailing zero bits in x. Returns 64 when x is zero. +BOOST_MATH_GPU_ENABLED inline int countr_zero(boost::math::uint64_t x) noexcept +{ +#if defined(__CUDA_ARCH__) + return x == 0 ? 64 : __ffsll(static_cast(x)) - 1; +#elif defined(__cpp_lib_bitops) && (__cpp_lib_bitops >= 201907L) && !defined(BOOST_MATH_HAS_GPU_SUPPORT) + return std::countr_zero(static_cast(x)); +#elif defined(__GNUC__) || defined(__clang__) + return x == 0 ? 64 : __builtin_ctzll(static_cast(x)); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index {}; + return _BitScanForward64(&index, static_cast(x)) ? static_cast(index) : 64; +#else + if (x == 0) + { + return 64; + } + int n {0}; + while ((x & 1u) == 0) + { + x >>= 1; + ++n; + } + return n; +#endif +} + +// Number of leading zero bits in x. Returns 64 when x is zero. +BOOST_MATH_GPU_ENABLED inline int countl_zero(boost::math::uint64_t x) noexcept +{ +#if defined(__CUDA_ARCH__) + return __clzll(static_cast(x)); +#elif defined(__cpp_lib_bitops) && (__cpp_lib_bitops >= 201907L) && !defined(BOOST_MATH_HAS_GPU_SUPPORT) + return std::countl_zero(static_cast(x)); +#elif defined(__GNUC__) || defined(__clang__) + return x == 0 ? 64 : __builtin_clzll(static_cast(x)); +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) + unsigned long index {}; + return _BitScanReverse64(&index, static_cast(x)) ? 63 - static_cast(index) : 64; +#else + if (x == 0) + { + return 64; + } + int n {0}; + while ((x >> 63) == 0) + { + x <<= 1; + ++n; + } + return n; +#endif +} + +// Number of set bits in x. +BOOST_MATH_GPU_ENABLED inline int popcount(boost::math::uint64_t x) noexcept +{ +#if defined(__CUDA_ARCH__) + return __popcll(static_cast(x)); +#elif defined(__cpp_lib_bitops) && (__cpp_lib_bitops >= 201907L) && !defined(BOOST_MATH_HAS_GPU_SUPPORT) + return std::popcount(static_cast(x)); +#elif defined(__GNUC__) || defined(__clang__) + return __builtin_popcountll(static_cast(x)); +#else + // Parallel bit count (Hacker's Delight, figure 5-2) + x = x - ((x >> 1) & 0x5555555555555555ULL); + x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL); + x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0fULL; + return static_cast((x * 0x0101010101010101ULL) >> 56); +#endif +} + +// Largest power of two not exceeding x. Returns 0 for x == 0. +BOOST_MATH_GPU_ENABLED inline boost::math::uint64_t floor_pow2(boost::math::uint64_t x) noexcept +{ + return x == 0 ? 0 : boost::math::uint64_t(1) << (63 - countl_zero(x)); +} + +// Exact integer square root: the largest r with r * r <= n. +BOOST_MATH_GPU_ENABLED inline boost::math::uint64_t isqrt(boost::math::uint64_t n) noexcept +{ + if (n < 2) + { + return n; + } + // Newton iteration from a power of two above the root converges monotonically down. + const int bits {64 - countl_zero(n)}; + boost::math::uint64_t x {boost::math::uint64_t(1) << ((bits + 1) / 2)}; + while (true) + { + const boost::math::uint64_t y {(x + n / x) / 2}; + if (y >= x) + { + return x; + } + x = y; + } +} + +// Reads eight bytes as a little-endian 64-bit value regardless of host byte order. +BOOST_MATH_GPU_ENABLED inline boost::math::uint64_t load_le64(const unsigned char* p) noexcept +{ + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24) + | (static_cast(p[4]) << 32) + | (static_cast(p[5]) << 40) + | (static_cast(p[6]) << 48) + | (static_cast(p[7]) << 56); +} + +}}} // namespace boost::math::tools + +#endif // BOOST_MATH_TOOLS_BIT_HPP diff --git a/include/boost/math/tools/config.hpp b/include/boost/math/tools/config.hpp index 26cbe2eaeb..90d24f2b91 100644 --- a/include/boost/math/tools/config.hpp +++ b/include/boost/math/tools/config.hpp @@ -799,6 +799,13 @@ BOOST_MATH_GPU_ENABLED constexpr T gpu_safe_max(const T& a, const T& b) { return #endif +// Native 128-bit integer support (GCC, Clang, Intel, nvcc with a GCC host). +// Boost.Config's BOOST_HAS_INT128 is unavailable in standalone mode, so test the +// compiler macro directly. Define BOOST_MATH_NO_INT128 to force the portable paths. +#if defined(__SIZEOF_INT128__) && !defined(BOOST_MATH_NO_INT128) +# define BOOST_MATH_HAS_INT128 +#endif + // Static variables are not allowed with CUDA or C++20 modules // See if we can inline them instead diff --git a/module/math.cppm b/module/math.cppm index 21afdc4d97..32ffb62461 100644 --- a/module/math.cppm +++ b/module/math.cppm @@ -130,6 +130,7 @@ extern "C++" { // Special functions (umbrella) #include +#include // Statistical distributions (umbrella) #include diff --git a/reporting/performance/prime_sieve_performance.cpp b/reporting/performance/prime_sieve_performance.cpp new file mode 100644 index 0000000000..31d29087a1 --- /dev/null +++ b/reporting/performance/prime_sieve_performance.cpp @@ -0,0 +1,134 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Benchmarks of boost::math::prime_sieve / prime_count against primesieve +// (https://github.com/kimwalisch/primesieve). Built by hand, for example: +// +// clang++ -std=c++17 -O3 -march=native -DNDEBUG -DBOOST_MATH_STANDALONE -fexperimental-library \ +// -I/libs/math/include -I/include -I/opt/homebrew/include \ +// prime_sieve_performance.cpp /build/libprimesieve.a \ +// -L/opt/homebrew/lib -lbenchmark -lpthread -o prime_sieve_performance +// +// Define BOOST_MATH_BENCH_NO_PRIMESIEVE to build without the reference library. + +#include +#include +#ifndef BOOST_MATH_BENCH_NO_PRIMESIEVE +#include +#endif +#include +#include +#include + +namespace bm = boost::math; + +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +// Policies are not default constructible in every standard library, so select them by tag +template +constexpr decltype(auto) policy() +{ + if constexpr (Parallel) + { + return (std::execution::par); + } + else + { + return (std::execution::seq); + } +} + +template +void count_ours(benchmark::State& state) +{ + const std::uint64_t n {static_cast(state.range(0))}; + for (auto _ : state) + { + benchmark::DoNotOptimize(bm::prime_count(policy(), n)); + } + state.SetItemsProcessed(static_cast(state.iterations()) * static_cast(n)); +} + +template +void generate_ours(benchmark::State& state) +{ + const std::uint64_t n {static_cast(state.range(0))}; + std::vector primes; + bm::prime_reserve(n, primes); + for (auto _ : state) + { + primes.clear(); + bm::prime_sieve(policy(), n, primes); + benchmark::DoNotOptimize(primes.data()); + } + state.SetItemsProcessed(static_cast(state.iterations()) * static_cast(n)); +} + +template +void window_ours(benchmark::State& state) +{ + const std::uint64_t lo {static_cast(state.range(0))}; + const std::uint64_t width {static_cast(state.range(1))}; + for (auto _ : state) + { + benchmark::DoNotOptimize(bm::prime_count(policy(), lo, lo + width)); + } +} +#endif + +#ifndef BOOST_MATH_BENCH_NO_PRIMESIEVE +void count_primesieve(benchmark::State& state) +{ + const std::uint64_t n {static_cast(state.range(0))}; + // range(1) == 0 means all hardware threads + const int threads {state.range(1) == 0 ? static_cast(std::thread::hardware_concurrency()) : static_cast(state.range(1))}; + primesieve::set_num_threads(threads); + for (auto _ : state) + { + benchmark::DoNotOptimize(primesieve::count_primes(0, n)); + } + state.SetItemsProcessed(static_cast(state.iterations()) * static_cast(n)); +} + +void generate_primesieve(benchmark::State& state) +{ + const std::uint64_t n {static_cast(state.range(0))}; + std::vector primes; + for (auto _ : state) + { + primes.clear(); + primesieve::generate_primes(n, &primes); + benchmark::DoNotOptimize(primes.data()); + } + state.SetItemsProcessed(static_cast(state.iterations()) * static_cast(n)); +} + +void window_primesieve(benchmark::State& state) +{ + const std::uint64_t lo {static_cast(state.range(0))}; + const std::uint64_t width {static_cast(state.range(1))}; + primesieve::set_num_threads(1); + for (auto _ : state) + { + benchmark::DoNotOptimize(primesieve::count_primes(lo, lo + width - 1)); + } +} +#endif + +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION +BENCHMARK_TEMPLATE(count_ours, false)->RangeMultiplier(10)->Range(100000000, 100000000000)->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(count_ours, true)->RangeMultiplier(10)->Range(100000000, 100000000000)->Unit(benchmark::kMillisecond)->UseRealTime(); +BENCHMARK_TEMPLATE(generate_ours, false)->RangeMultiplier(10)->Range(100000000, 10000000000)->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(generate_ours, true)->RangeMultiplier(10)->Range(100000000, 10000000000)->Unit(benchmark::kMillisecond)->UseRealTime(); +BENCHMARK_TEMPLATE(window_ours, false)->Args({1000000000000, 1000000000})->Args({1000000000000000, 1000000000})->Args({1000000000000000000, 1000000000})->Unit(benchmark::kMillisecond); +#endif + +#ifndef BOOST_MATH_BENCH_NO_PRIMESIEVE +BENCHMARK(count_primesieve)->ArgsProduct({{100000000, 1000000000, 10000000000, 100000000000}, {1}})->Unit(benchmark::kMillisecond); +BENCHMARK(count_primesieve)->ArgsProduct({{100000000, 1000000000, 10000000000, 100000000000}, {0}})->Unit(benchmark::kMillisecond)->UseRealTime(); +BENCHMARK(generate_primesieve)->RangeMultiplier(10)->Range(100000000, 10000000000)->Unit(benchmark::kMillisecond); +BENCHMARK(window_primesieve)->Args({1000000000000, 1000000000})->Args({1000000000000000, 1000000000})->Args({1000000000000000000, 1000000000})->Unit(benchmark::kMillisecond); +#endif + +BENCHMARK_MAIN(); diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 15062f4fa9..ae25383638 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -1042,6 +1042,9 @@ test-suite misc : [ run univariate_statistics_test.cpp /boost/test//boost_unit_test_framework : : : gcc-mingw:-Wa,-mbig-obj off msvc:/bigobj [ check-target-builds ../config//is_cygwin_run "Cygwin CI run" : no ] [ requires cxx17_if_constexpr cxx17_std_apply ] ] [ run univariate_statistics_backwards_compatible_test.cpp /boost/test//boost_unit_test_framework : : : gcc-mingw:-Wa,-mbig-obj off msvc:/bigobj [ check-target-builds ../config//is_cygwin_run "Cygwin CI run" : no ] [ requires cxx11_hdr_forward_list cxx11_hdr_atomic cxx11_hdr_thread cxx11_hdr_tuple cxx11_hdr_future cxx11_sfinae_expr ] ] [ run ooura_fourier_integral_test.cpp /boost/test//boost_unit_test_framework : : : gcc-mingw:-Wa,-mbig-obj off msvc:/bigobj [ check-target-builds ../config//is_cygwin_run "Cygwin CI run" : no ] $(float128_type) [ requires cxx17_if_constexpr cxx17_std_apply ] ] + [ run test_prime_sieve.cpp : : : [ requires cxx17_if_constexpr cxx17_std_apply ] ] + [ run test_prime_sieve_mp.cpp : : : [ requires cxx17_if_constexpr cxx17_std_apply ] ] + [ run test_prime_sieve_mp.cpp ../tools//gmp : : : BOOST_MATH_TEST_GMP [ requires cxx17_if_constexpr cxx17_std_apply ] [ check-target-builds ../config//has_gmp : : no ] : test_prime_sieve_gmp ] [ run empirical_cumulative_distribution_test.cpp : : : [ requires cxx17_if_constexpr cxx17_std_apply ] ] [ run norms_test.cpp /boost/test//boost_unit_test_framework : : : [ requires cxx17_if_constexpr cxx17_std_apply ] ] [ run signal_statistics_test.cpp : : : [ requires cxx17_if_constexpr cxx17_std_apply ] ] @@ -2155,6 +2158,7 @@ test-suite concepts : [ run compile_test/sf_next_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] [ run compile_test/sf_powm1_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] [ run compile_test/sf_prime_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] + [ run compile_test/sf_prime_sieve_incl_test.cpp compile_test_main : : : [ requires cxx17_if_constexpr cxx17_std_apply ] [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] [ run compile_test/sf_relative_distance_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] [ run compile_test/sf_round_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] [ run compile_test/sf_sign_incl_test.cpp compile_test_main : : : [ check-target-builds ../config//is_ci_sanitizer_run "Sanitizer CI run" : no ] ] @@ -2311,6 +2315,7 @@ alias no_eh_tests : sf_next_incl_test sf_powm1_incl_test sf_prime_incl_test + sf_prime_sieve_incl_test sf_relative_distance_incl_test sf_round_incl_test sf_sign_incl_test diff --git a/test/compile_test/sf_prime_sieve_incl_test.cpp b/test/compile_test/sf_prime_sieve_incl_test.cpp new file mode 100644 index 0000000000..6bff78a191 --- /dev/null +++ b/test/compile_test/sf_prime_sieve_incl_test.cpp @@ -0,0 +1,20 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Basic sanity check that the header +// #includes all the files that it needs to. + +#include +#include + +void compile_and_link_test() +{ + std::vector v; + boost::math::prime_sieve(100u, v); + boost::math::prime_range(10u, 100u, v); + (void)boost::math::prime_count(100u); + (void)boost::math::prime_count(10u, 100u); + (void)boost::math::prime_count(boost::math::execution::cuda, 100u); +} diff --git a/test/cuda_jamfile b/test/cuda_jamfile index f2f2c9aa82..0f59244a35 100644 --- a/test/cuda_jamfile +++ b/test/cuda_jamfile @@ -397,3 +397,6 @@ run test_trigamma_float.cu ; run test_trunc_double.cu ; run test_trunc_float.cu ; + +# Prime sieve (owns its own kernels) +run test_prime_sieve_cuda.cu ; diff --git a/test/test_prime_sieve.cpp b/test/test_prime_sieve.cpp new file mode 100644 index 0000000000..131589bb91 --- /dev/null +++ b/test/test_prime_sieve.cpp @@ -0,0 +1,372 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace boost::math; +namespace ps = boost::math::detail::prime_sieve; + +// Known values of the prime counting function +struct pi_entry +{ + std::uint64_t x; + std::uint64_t pi; +}; + +constexpr pi_entry pi_table[] = +{ + {10, 4}, {100, 25}, {1000, 168}, {10000, 1229}, {100000, 9592}, {1000000, 78498}, + {10000000, 664579}, {100000000, 5761455}, {1000000000, 50847534} +}; + +void test_primality_helpers() +{ + // The two 64-bit tests agree with each other, with brute force, and with the prime table + for (unsigned n {0}; n < 5000; ++n) + { + bool brute {n >= 2}; + for (unsigned f {2}; f * f <= n; ++f) + { + if (n % f == 0) + { + brute = false; + break; + } + } + BOOST_TEST_EQ(ps::is_prime_u64(n), brute); + BOOST_TEST_EQ(ps::is_prime_u64_miller_rabin(n), brute); + } + for (unsigned i {0}; i < boost::math::max_prime; i += 97) + { + BOOST_TEST(ps::is_prime_u64(boost::math::prime(i))); + } + // Carmichael numbers and strong pseudoprimes to base 2 + for (const std::uint64_t c : {561ull, 1105ull, 1729ull, 2465ull, 2821ull, 2047ull, 3277ull, 4033ull, 4681ull, 8321ull, 3215031751ull}) + { + BOOST_TEST(!ps::is_prime_u64(c)); + } + BOOST_TEST(ps::is_prime_u64(18446744073709551557ull)); + BOOST_TEST(!ps::is_prime_u64(18446744073709551615ull)); + BOOST_TEST(!ps::is_prime_u64(18446744073709551559ull)); + std::mt19937_64 rng {12345}; + for (int i {0}; i < 20000; ++i) + { + const std::uint64_t n {rng()}; + BOOST_TEST_EQ(ps::is_prime_u64(n), ps::is_prime_u64_miller_rabin(n)); + } +} + +template +void test_pi_table() +{ + for (const pi_entry& e : pi_table) + { + if (e.x > static_cast((std::numeric_limits::max)())) + { + continue; + } + const Integer x {static_cast(e.x)}; + BOOST_TEST_EQ(prime_count(x), e.pi); + + std::vector v; + prime_reserve(x, v); + BOOST_TEST(v.capacity() >= e.pi); + prime_sieve(x, v); + BOOST_TEST_EQ(v.size(), e.pi); + for (std::size_t i {1}; i < v.size(); ++i) + { + if (!(v[i - 1] < v[i])) + { + BOOST_ERROR("output not ascending"); + break; + } + } + if (!v.empty()) + { + BOOST_TEST_EQ(v.front(), Integer(2)); + } + + std::vector w; + prime_sieve(x, std::back_inserter(w)); + BOOST_TEST(v == w); + } +} + +// Every value in [lo, hi) is checked individually against the primality test +void check_range(std::uint64_t lo, std::uint64_t hi, const prime_sieve_options& options = {}) +{ + std::vector v; + prime_range(lo, hi, v, options); + std::uint64_t expected {0}; + std::size_t k {0}; + bool ok {true}; + for (std::uint64_t n {lo}; n < hi; ++n) + { + if (ps::is_prime_u64(n)) + { + ++expected; + if (k >= v.size() || v[k] != n) + { + ok = false; + } + ++k; + } + if (n == (std::numeric_limits::max)()) + { + break; + } + } + BOOST_TEST(ok); + BOOST_TEST_EQ(v.size(), expected); + BOOST_TEST_EQ(prime_count(lo, hi, options), expected); +} + +void test_ranges() +{ + std::mt19937_64 rng {7}; + const std::uint64_t lows[] = + { + 0, 1, 2, 3, 6, 7, 8, 29, 30, 31, 32, 1000, 65536, 1000000, 1000000000, + 4294967296ull - 50000, 1000000000000ull, 1000000000000000ull, 1000000000000000000ull, + (std::numeric_limits::max)() - 100000 + }; + for (const std::uint64_t lo : lows) + { + for (int rep {0}; rep < 2; ++rep) + { + const std::uint64_t width {rep == 0 ? 100000u : static_cast(rng() % 100000)}; + const std::uint64_t hi {lo > (std::numeric_limits::max)() - width ? (std::numeric_limits::max)() : lo + width}; + check_range(lo, hi); + } + } + // every residue of the lower bound modulo 30 + for (std::uint64_t r {0}; r < 30; ++r) + { + check_range(1000000 + r, 1000000 + r + 3000); + } + // both strategies must agree on a short interval at a large magnitude + prime_sieve_options full {}; + full.range_strategy = prime_range_strategy::full_sieve; + prime_sieve_options test {}; + test.range_strategy = prime_range_strategy::test_survivors; + BOOST_TEST_EQ(prime_count(1000000000000000000ull, 1000000000001000000ull, full), 24280u); + BOOST_TEST_EQ(prime_count(1000000000000000000ull, 1000000000001000000ull, test), 24280u); + BOOST_TEST_EQ(prime_count(1000000000000000000ull, 1000000000001000000ull), 24280u); + // and on low intervals, where the sieving primes themselves lie inside the window + for (const std::uint64_t lo : {0ull, 2ull, 7ull, 8ull, 11ull, 100ull, 1000ull, 65536ull, 1000000ull}) + { + check_range(lo, lo + 20000, test); + } + // forced geometries: a small segment engages the bucket sieve, a large one the medium class only + prime_sieve_options small_segment {}; + small_segment.sieve_bytes = 16384; + BOOST_TEST_EQ(prime_count(1000000000ull, small_segment), 50847534u); + check_range(1000000000000ull, 1000000000000ull + 300000, small_segment); + prime_sieve_options large_segment {}; + large_segment.sieve_bytes = 8u * 1024u * 1024u; + BOOST_TEST_EQ(prime_count(100000000ull, large_segment), 5761455u); + prime_sieve_options small_l1 {}; + small_l1.l1d_bytes = 16384; + BOOST_TEST_EQ(prime_count(100000000ull, small_l1), 5761455u); + // windows that need the bucket sieve across many segments + BOOST_TEST_EQ(prime_count(1000000000000000ull, 1000000000000000ull + 100000000ull), 2893937u); +} + +void test_edge_cases() +{ + std::vector s; + prime_range(0, 2, s); + BOOST_TEST(s.empty()); + prime_range(0, 3, s); + BOOST_TEST(s.size() == 1 && s[0] == 2); + s.clear(); + prime_range(2, 8, s); + BOOST_TEST_EQ(s.size(), 4u); + s.clear(); + prime_range(7, 8, s); + BOOST_TEST(s.size() == 1 && s[0] == 7); + s.clear(); + prime_range(30, 31, s); + BOOST_TEST(s.empty()); + prime_range(31, 32, s); + BOOST_TEST(s.size() == 1 && s[0] == 31); + s.clear(); + prime_range(100, 100, s); + BOOST_TEST(s.empty()); + prime_range(100, 50, s); + BOOST_TEST(s.empty()); + prime_range(-100, 10, s); + BOOST_TEST_EQ(s.size(), 4u); + s.clear(); + prime_sieve(-5, s); + BOOST_TEST(s.empty()); + + // a negative lower bound must survive promotion against an unsigned upper bound + prime_range(-100, 10u, s); + BOOST_TEST_EQ(s.size(), 4u); + s.clear(); + prime_range(-100LL, static_cast(10), s); + BOOST_TEST_EQ(s.size(), 4u); + s.clear(); + prime_range(static_cast(-1), 30u, s); + BOOST_TEST_EQ(s.size(), 10u); + s.clear(); + prime_range(-100, 10u, std::back_inserter(s)); + BOOST_TEST_EQ(s.size(), 4u); + s.clear(); + BOOST_TEST_EQ(prime_count(-100, 10u), 4u); + BOOST_TEST_EQ(prime_count(-100LL, static_cast(10)), 4u); + BOOST_TEST_EQ(prime_count(-100, static_cast(10)), 4u); + + BOOST_TEST_EQ(prime_count(0), 0u); + BOOST_TEST_EQ(prime_count(2), 0u); + BOOST_TEST_EQ(prime_count(3), 1u); + BOOST_TEST_EQ(prime_count(8), 4u); + BOOST_TEST_EQ(prime_count(168), 39u); + BOOST_TEST_EQ(prime_count(-7), 0u); + BOOST_TEST_EQ(prime_count(5, 5), 0u); + BOOST_TEST_EQ(prime_count(5, 6), 1u); + BOOST_TEST_EQ(prime_count(6, 7), 0u); + + // the largest 64-bit prime and the top of the range + std::vector top; + prime_range(static_cast(18446744073709551557ull), (std::numeric_limits::max)(), top); + BOOST_TEST(top.size() == 1 && top[0] == 18446744073709551557ull); + BOOST_TEST_EQ(prime_count(static_cast(18446744073709551558ull), (std::numeric_limits::max)()), 0u); + + // other output iterators + std::list l; + prime_range(10, 100, std::back_inserter(l)); + BOOST_TEST_EQ(l.size(), 21u); + BOOST_TEST_EQ(l.front(), 11); + BOOST_TEST_EQ(l.back(), 97); + std::vector buffer(200); + unsigned* end {prime_sieve(1000u, buffer.data())}; + BOOST_TEST_EQ(static_cast(end - buffer.data()), 168u); + BOOST_TEST_EQ(buffer[167], 997u); + + // narrower integer types + BOOST_TEST_EQ(prime_count(static_cast(30000)), 3245u); + std::vector bytes; + prime_sieve(static_cast(255), bytes); + BOOST_TEST_EQ(bytes.size(), 54u); +} + +#ifndef BOOST_NO_EXCEPTIONS +// Writes are counted and the iterator throws once limit values have been accepted +struct throwing_output_iterator +{ + using iterator_category = std::output_iterator_tag; + using value_type = void; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = void; + + std::size_t* written {nullptr}; + std::size_t limit {0}; + + throwing_output_iterator& operator*() { return *this; } + throwing_output_iterator& operator++() { return *this; } + throwing_output_iterator operator++(int) { return *this; } + + template + throwing_output_iterator& operator=(const T&) + { + if (++*written > limit) + { + throw std::runtime_error("consumer failure"); + } + return *this; + } +}; +#endif + +void test_policies() +{ +#ifdef BOOST_MATH_PRIME_SIEVE_HAS_STD_EXECUTION + for (const std::uint64_t n : {1000ull, 100000ull, 100000000ull, 1000000000ull}) + { + std::vector a; + std::vector b; + prime_sieve(std::execution::seq, n, a); + prime_sieve(std::execution::par, n, b); + BOOST_TEST(a == b); + BOOST_TEST_EQ(prime_count(std::execution::seq, n), a.size()); + BOOST_TEST_EQ(prime_count(std::execution::par, n), a.size()); + BOOST_TEST_EQ(prime_count(std::execution::par_unseq, n), a.size()); + } + for (const std::uint64_t lo : {0ull, 1000000ull, 1000000000000ull, 1000000000000000000ull, (std::numeric_limits::max)() - 400000000ull}) + { + const std::uint64_t hi {lo > (std::numeric_limits::max)() - 300000000ull ? (std::numeric_limits::max)() : lo + 300000000ull}; + std::vector a; + std::vector b; + prime_range(std::execution::seq, lo, hi, std::back_inserter(a)); + prime_range(std::execution::par, lo, hi, b); + BOOST_TEST(a == b); + BOOST_TEST_EQ(prime_count(std::execution::par, lo, hi), a.size()); + std::list l; + prime_range(std::execution::par, lo, hi, std::back_inserter(l)); + BOOST_TEST_EQ(l.size(), a.size()); + } + prime_sieve_options three {}; + three.max_threads = 3; + BOOST_TEST_EQ(prime_count(std::execution::par, 1000000000ull, three), 50847534u); + prime_sieve_options one {}; + one.max_threads = 1; + BOOST_TEST_EQ(prime_count(std::execution::par, 100000000ull, one), 5761455u); + std::vector vi; + prime_sieve(std::execution::par_unseq, 1000, vi); + BOOST_TEST(vi.size() == 168 && vi.front() == 2 && vi.back() == 997); +#ifndef BOOST_NO_EXCEPTIONS + // A throwing consumer must propagate: the workers blocked on the full output queue + // have to be released, or waiting on the futures never returns. + { + std::size_t written {0}; + bool caught {false}; + try + { + prime_range(std::execution::par, 7ull, 2000000000ull, throwing_output_iterator {&written, 1000}); + } + catch (const std::runtime_error&) + { + caught = true; + } + BOOST_TEST(caught); + } +#endif +#endif + // the CUDA tag is always accepted; without nvcc it runs on the CPU + BOOST_TEST_EQ(prime_count(execution::cuda, 1000000ull), 78498u); + std::vector vc; + prime_sieve(execution::cuda, 100000ull, vc); + BOOST_TEST_EQ(vc.size(), 9592u); + prime_range(execution::cuda, 1000000000000ull, 1000000001000ull, vc); + BOOST_TEST_EQ(vc.size(), 9592u + 37u); +} + +int main() +{ + test_primality_helpers(); + test_pi_table(); + test_pi_table(); + test_pi_table(); + test_pi_table(); + test_pi_table(); + test_ranges(); + test_edge_cases(); + test_policies(); + return boost::report_errors(); +} diff --git a/test/test_prime_sieve_cuda.cu b/test/test_prime_sieve_cuda.cu new file mode 100644 index 0000000000..fc4023fc28 --- /dev/null +++ b/test/test_prime_sieve_cuda.cu @@ -0,0 +1,157 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// The CUDA prime sieve against the CPU engine: fixed counts, boundaries, random ranges, +// element-wise output comparison, two back-to-back passes and a device memory leak check. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef BOOST_MATH_HAS_CUDA_PRIME_SIEVE +#error "BOOST_MATH_HAS_CUDA_PRIME_SIEVE must be defined when compiling this test with nvcc and BOOST_MATH_ENABLE_CUDA" +#endif + +namespace bm = boost::math; + +static int failures {0}; + +static double now() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +static void check_count(std::uint64_t lo, std::uint64_t hi, std::uint64_t expected, const char* what) +{ + const double t0 {now()}; + const std::uint64_t got {bm::prime_count(bm::execution::cuda, lo, hi)}; + const double t1 {now()}; + if (got != expected) + { + std::printf("FAIL %s: count [%llu, %llu) = %llu, expected %llu\n", what, (unsigned long long)lo, (unsigned long long)hi, (unsigned long long)got, (unsigned long long)expected); + ++failures; + } + else + { + std::printf("ok %s: count [%llu, %llu) = %llu in %.3fs\n", what, (unsigned long long)lo, (unsigned long long)hi, (unsigned long long)got, t1 - t0); + } +} + +static void check_range(std::uint64_t lo, std::uint64_t hi) +{ + bm::prime_sieve_options cpu_options {}; + cpu_options.range_strategy = bm::prime_range_strategy::full_sieve; + std::vector cpu; + bm::prime_range(lo, hi, cpu, cpu_options); + std::vector gpu; + bm::prime_sieve_options gpu_options {}; + gpu_options.range_strategy = bm::prime_range_strategy::full_sieve; + bm::prime_range(bm::execution::cuda, lo, hi, gpu, gpu_options); + const std::uint64_t gpu_count {bm::prime_count(bm::execution::cuda, lo, hi, gpu_options)}; + if (cpu != gpu || gpu_count != cpu.size()) + { + std::printf("FAIL range [%llu, %llu): cpu %zu, gpu %zu, gpu count %llu\n", (unsigned long long)lo, (unsigned long long)hi, cpu.size(), gpu.size(), (unsigned long long)gpu_count); + for (std::size_t i {0}; i < cpu.size() && i < gpu.size(); ++i) + { + if (cpu[i] != gpu[i]) + { + std::printf(" first difference at index %zu: cpu %llu gpu %llu\n", i, (unsigned long long)cpu[i], (unsigned long long)gpu[i]); + break; + } + } + ++failures; + } +} + +static void run_pass() +{ + check_count(0, 1000000, 78498, "pi(1e6)"); + check_count(0, 100000000, 5761455, "pi(1e8)"); + check_count(0, 1000000000, 50847534, "pi(1e9)"); + check_count(0, 10000000000ull, 455052511, "pi(1e10)"); + check_count(0, 100000000000ull, 4118054813ull, "pi(1e11)"); + check_count(1000000000000ull, 1000000000000ull + 100000000ull, bm::prime_count(1000000000000ull, 1000000000000ull + 100000000ull), "1e12 window"); + check_count(1000000000000000ull, 1000000000000000ull + 100000000ull, 2893937, "1e15 window"); + check_count(1000000000000000000ull, 1000000000000000000ull + 100000000ull, bm::prime_count(1000000000000000000ull, 1000000000000000000ull + 100000000ull), "1e18 window"); + + const std::uint64_t top {(std::numeric_limits::max)()}; + check_count(top - 1000, top, bm::prime_count(top - 1000, top), "top of range"); + check_count(top - 300000000ull, top, bm::prime_count(static_cast(top - 300000000ull), top), "3e8 below 2^64"); + std::vector last; + bm::prime_range(bm::execution::cuda, static_cast(18446744073709551557ull), top, last); + if (last.size() != 1 || last[0] != 18446744073709551557ull) + { + std::printf("FAIL largest 64-bit prime\n"); + ++failures; + } + + // boundaries around bytes, segments and chunks + const std::uint64_t segment_span {32768ull * 30ull}; + const std::uint64_t chunk {std::uint64_t(1) << 32}; + const std::uint64_t lows[] = {0, 1, 2, 7, 8, 29, 30, 31, 36, 37, 59, 60, segment_span - 1, segment_span, segment_span + 1, + chunk - 100000, chunk - 1, chunk, chunk + 1, 3 * chunk - 50000}; + const std::uint64_t widths[] = {1, 2, 30, 1000, 200000}; + for (const std::uint64_t lo : lows) + { + for (const std::uint64_t width : widths) + { + check_range(lo, lo + width); + } + } + // several interior chunk boundaries (candidates congruent to 1 mod 30 straddle them) + check_count(chunk - 1000, 3 * chunk + 1000, bm::prime_count(std::execution::par, chunk - 1000, 3 * chunk + 1000), "three chunk boundaries"); + check_range(2, 8); + check_range(7, 8); + check_range(0, 2); + check_range(100, 100); + + // random ranges + std::mt19937_64 rng {2026}; + for (int i {0}; i < 40; ++i) + { + const double e {std::uniform_real_distribution(3.0, 19.0)(rng)}; + std::uint64_t lo {static_cast(std::pow(10.0, e))}; + const std::uint64_t width {rng() % 50000000ull + 1}; + if (lo > top - width) + { + lo = top - width; + } + check_range(lo, lo + width); + } +} + +int main() +{ + std::size_t free_before {0}; + std::size_t total {0}; + cudaMemGetInfo(&free_before, &total); + + const double t0 {now()}; + run_pass(); + const double t1 {now()}; + std::printf("first pass: %.1fs, %d failures\n", t1 - t0, failures); + const int first_failures {failures}; + run_pass(); + const double t2 {now()}; + std::printf("second pass: %.1fs, %d failures\n", t2 - t1, failures - first_failures); + + cudaDeviceSynchronize(); + std::size_t free_after {0}; + cudaMemGetInfo(&free_after, &total); + if (free_after + (16u << 20) < free_before) + { + std::printf("FAIL: device memory dropped from %zu to %zu bytes free\n", free_before, free_after); + ++failures; + } + std::printf("%s\n", failures == 0 ? "ALL OK" : "FAILED"); + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/test_prime_sieve_mp.cpp b/test/test_prime_sieve_mp.cpp new file mode 100644 index 0000000000..31a433d386 --- /dev/null +++ b/test/test_prime_sieve_mp.cpp @@ -0,0 +1,181 @@ +// (C) Copyright Matt Borland 2026. +// Use, modification and distribution are subject to the +// Boost Software License, Version 1.0. (See accompanying file +// LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Prime sieve with Boost.Multiprecision integers, including bounds beyond 2^64. +// Define BOOST_MATH_TEST_GMP to also run with mpz_int. + +#include +#include +#include +#include +#ifdef BOOST_MATH_TEST_GMP +#include +#endif +#include +#include +#include +#include + +using namespace boost::math; +namespace ps = boost::math::detail::prime_sieve; + +template +void test_helpers() +{ + // Jacobi symbol against a brute-force Legendre symbol + for (const int q : {3, 5, 7, 11, 13, 17, 19, 23, 101, 997}) + { + for (int a {0}; a < q; ++a) + { + int legendre {0}; + for (int x {1}; x < q && legendre == 0; ++x) + { + if ((x * x) % q == a) + { + legendre = 1; + } + } + if (a != 0 && legendre == 0) + { + legendre = -1; + } + BOOST_TEST_EQ(ps::jacobi(Integer(a), Integer(q)), legendre); + } + } + BOOST_TEST_EQ(ps::jacobi_signed(-1, Integer(13)), 1); + BOOST_TEST_EQ(ps::jacobi_signed(-1, Integer(7)), -1); + + // powm against Boost.Multiprecision + for (int i {1}; i < 50; ++i) + { + const Integer b {Integer(1234567) * i + 89}; + const Integer e {Integer(987654321) * i}; + const Integer m {Integer(1000000007) * i + 2}; + BOOST_TEST(ps::powm(b, e, m) == boost::multiprecision::powm(b, e, m)); + } + // integer roots and perfect powers + for (int i {1}; i < 50; ++i) + { + const Integer n {Integer(123456789) * Integer(987654321) * i}; + const Integer r {ps::isqrt(n)}; + BOOST_TEST(r * r <= n && (r + 1) * (r + 1) > n); + const Integer c {ps::iroot(n, 3)}; + BOOST_TEST(c * c * c <= n && (c + 1) * (c + 1) * (c + 1) > n); + } + BOOST_TEST(ps::is_perfect_power(Integer(1194649), 3)); // 1093^2 + BOOST_TEST(ps::is_perfect_power(Integer(1194649), 2)); + BOOST_TEST(!ps::is_perfect_power(Integer(1194651), 7)); + BOOST_TEST(ps::is_perfect_power(Integer(Integer(3511) * 3511 * 3511), 3)); + + // Baillie-PSW: pseudoprimes, Carmichael numbers, strong Lucas pseudoprimes, small primes + for (const unsigned c : {561u, 1105u, 1729u, 2465u, 2821u, 2047u, 3277u, 4033u, 4681u, 8321u, 5459u, 5777u, 10877u, 16109u, 18971u}) + { + BOOST_TEST(!ps::is_probable_prime_bpsw(Integer(c))); + } + BOOST_TEST(!ps::is_probable_prime_bpsw(Integer(3215031751ull))); + for (unsigned n {0}; n < 3000; ++n) + { + BOOST_TEST_EQ(ps::is_probable_prime_bpsw(Integer(n)), ps::is_prime_u64(n)); + } + // the pseudosquares themselves are classified like Miller-Rabin does + for (std::size_t i {0}; i < 49; ++i) + { + const Integer L {ps::pseudosquare_value(i)}; + BOOST_TEST_EQ(ps::is_probable_prime_bpsw(L), boost::multiprecision::miller_rabin_test(L, 25)); + } + // Wieferich squares must fail the pseudosquares test through the perfect power check + const Integer w1 {1194649}; + BOOST_TEST(!ps::pseudosquares_prime_test(w1, ps::pseudosquare_index_for(Integer(w1 / 1000)), 1000)); + const Integer w2 {Integer(3511) * 3511}; + BOOST_TEST(!ps::pseudosquares_prime_test(w2, ps::pseudosquare_index_for(Integer(w2 / 1000)), 1000)); +} + +template +void test_sieve() +{ + // results equal the builtin ones + BOOST_TEST_EQ(prime_count(Integer(1000000)), 78498u); + std::vector s; + prime_sieve(Integer(1000), s); + BOOST_TEST(s.size() == 168 && s.back() == 997); + BOOST_TEST_EQ(prime_count(Integer("1000000000000"), Integer("1000000100000")), prime_count(1000000000000ull, 1000000100000ull)); + BOOST_TEST_EQ(prime_count(Integer("1000000000000000000"), Integer("1000000000001000000")), 24280u); + + const Integer two64 {Integer(1) << 64}; + + // primes around 2^64 through an output iterator and through a vector + const char* expected[] = + { + "18446744073709551557", "18446744073709551629", "18446744073709551653", "18446744073709551667", + "18446744073709551697", "18446744073709551709", "18446744073709551757", "18446744073709551923", + "18446744073709551947", "18446744073709552009" + }; + std::vector v; + prime_range(Integer(two64 - 60), Integer(two64 + 400), std::back_inserter(v)); + BOOST_TEST_EQ(v.size(), 10u); + for (std::size_t i {0}; i < v.size() && i < 10; ++i) + { + BOOST_TEST(v[i] == Integer(expected[i])); + } + std::vector w; + prime_range(Integer(two64 - 60), Integer(two64 + 400), w); + BOOST_TEST(v == w); + BOOST_TEST_EQ(prime_count(Integer(two64 - 60), Integer(two64 + 400)), 10u); + + // a range straddling 2^64 is the sum of its two halves + std::vector straddle; + prime_range(Integer(two64 - 5000), Integer(two64 + 5000), straddle); + const std::uint64_t below {prime_count(18446744073709551615ull - 4999, 18446744073709551615ull)}; + const std::uint64_t above {prime_count(two64, Integer(two64 + 5000))}; + BOOST_TEST_EQ(straddle.size(), below + above); + + // reference counts in windows beyond 2^64 (pseudosquares path and BPSW path) + struct reference + { + const char* lower; + std::uint64_t width; + std::uint64_t count; + }; + const reference references[] = + { + {"18446744073709551616", 200000, 4335}, + {"100000000000000000000", 200000, 4294}, + {"10000000000000000000000", 100000, 1979}, + {"100000000000000000000000", 50000, 942}, + {"1000000000000000000000000", 50000, 919} + }; + prime_sieve_options probable {}; + probable.probable_prime_only = true; + for (const reference& r : references) + { + const Integer lo {r.lower}; + const Integer hi {lo + r.width}; + BOOST_TEST_EQ(prime_count(lo, hi), r.count); + BOOST_TEST_EQ(prime_count(lo, hi, probable), r.count); + std::vector primes; + prime_range(lo, hi, primes); + BOOST_TEST_EQ(primes.size(), r.count); + bool ok {true}; + for (std::size_t i {0}; i < primes.size(); ++i) + { + if (!boost::multiprecision::miller_rabin_test(primes[i], 25) || (i > 0 && !(primes[i - 1] < primes[i]))) + { + ok = false; + } + } + BOOST_TEST(ok); + } +} + +int main() +{ + test_helpers(); + test_sieve(); +#ifdef BOOST_MATH_TEST_GMP + test_helpers(); + test_sieve(); +#endif + return boost::report_errors(); +}