Compare commits

..
Author SHA1 Message Date
mlemanczyk fc22c01ec8 Various improvements to make sure scratch buffers don't override each others memory 2025-11-03 23:49:55 +01:00
Marek Lemańczyk dcda4b5cba Merge branch 'codex/optimize-evenperfectbitscanner-for-static-arrays' from github 2025-11-03 18:39:00 +01:00
Marek Lemańczyk 507daaddb9 Merge pull request #253 from mlemanczyk/codex/optimize-evenperfectbitscanner-for-primetestergpucontextleas
Add GPU lease warm-up and shared limiter
2025-11-02 14:02:42 +01:00
Marek Lemańczyk 1cbe7bf869 Use GpuPrimeWorkLimiter for GPU lease limiting 2025-11-02 14:02:15 +01:00
Marek Lemańczyk 00d4e9bde2 Add GPU lease warm-up and shared limiter 2025-11-02 13:47:57 +01:00
mlemanczyk 0e7bbae132 GPU contexts and accelerators made shared to prevent application crashes with high no. of threads. 2025-11-02 12:51:31 +01:00
mlemanczyk b33a3c8e75 Compilation error fixed 2025-11-02 11:25:41 +01:00
mlemanczyk 07be671726 Various small optimizations 2025-11-02 11:24:18 +01:00
Marek Lemańczyk 76dfe108a9 Address feedback on GPU pooling and heuristic factoring 2025-11-02 11:22:49 +01:00
Marek Lemańczyk 5339c3c304 Use span growth instead of dictionaries in partial factor 2025-11-02 11:22:38 +01:00
Marek Lemańczyk ab6a416de2 Warm GPU resources before thread startup 2025-11-02 11:22:28 +01:00
mlemanczyk 2281d9535e Revert "Merge pull request #251 from mlemanczyk/codex/refactor-prime-checking-on-gpu-n5t2pm"
This reverts commit 936ce239db, reversing
changes made to e14b77e504.
2025-11-02 11:22:14 +01:00
Marek Lemańczyk 936ce239db Merge pull request #251 from mlemanczyk/codex/refactor-prime-checking-on-gpu-n5t2pm
Warm GPU resources before thread startup
2025-11-01 04:09:00 +01:00
mlemanczyk 22f20bfe98 Various small optimizations 2025-11-01 03:42:36 +01:00
Marek Lemańczyk b7c34f4960 Preload divisor generator tables for GPU pool contexts 2025-10-31 23:01:28 +01:00
Marek Lemańczyk 469c8057d3 Flatten heuristic trial division bounds checks 2025-10-31 22:45:59 +01:00
Marek Lemańczyk 8c662c5c85 Address feedback on GPU pooling and heuristic factoring 2025-10-31 21:47:42 +01:00
Marek Lemańczyk 567b92792c Use span growth instead of dictionaries in partial factor 2025-10-31 18:52:00 +01:00
Marek Lemańczyk 6864b316d5 Warm GPU resources before thread startup 2025-10-31 16:56:25 +01:00
mlemanczyk e14b77e504 Batch size made dynamic - dependent on the value 2025-10-30 02:43:15 +01:00
Marek Lemańczyk 30af16a4f6 Merge pull request #249 from mlemanczyk/codex/optimize-heuristiccombinedprimetester-calculations
Use divisor square tables in heuristic combined prime tester
2025-10-29 22:31:52 +01:00
mlemanczyk fc5700f82c IsPrime with cancellation token removed. 2025-10-29 22:31:28 +01:00
Marek Lemańczyk f9504a6a19 Use atomic flag writes in heuristic trial division 2025-10-29 20:40:40 +01:00
Marek Lemańczyk 5ac05489c1 Use precomputed divisor squares in heuristic prime tester 2025-10-29 20:22:41 +01:00
Marek Lemańczyk f7a0627ce5 Align heuristic prime testers with squared limits 2025-10-29 19:35:59 +01:00
Marek Lemańczyk 5536dc4865 Fix GPU by-divisor hang for small primes 2025-10-29 19:21:41 +01:00
Marek Lemańczyk c61df1c33b Inline batch copies for GPU prime tester 2025-10-29 19:21:32 +01:00
Marek Lemańczyk 2b0fc5be13 Pool reusable GPU leases with embedded kernel state 2025-10-29 18:22:40 +01:00
Marek Lemańczyk a4ac82e74d Inline GPU kernel state into prime tester leases 2025-10-29 18:13:51 +01:00
Marek Lemańczyk 23fa56b356 Embed kernel state lookup in GPU leases 2025-10-29 18:13:46 +01:00
Marek Lemańczyk 08499fdda1 Refine GPU prime tester leasing 2025-10-29 17:39:38 +01:00
Marek Lemańczyk 6395b06b19 Optimize single GPU prime path and reuse lease buffers 2025-10-29 17:20:21 +01:00
Marek Lemańczyk 83a6c6c558 Scope GPU prime tester buffers to leases 2025-10-29 17:05:17 +01:00
Marek Lemańczyk ca53ddc798 Inline GPU lease buffers into the context lease 2025-10-29 16:53:29 +01:00
Marek Lemańczyk aab785650d Manage GPU lease buffers without kernel scratch pool 2025-10-29 16:53:22 +01:00
Marek Lemańczyk 13b59abb1c Embed GPU scratch buffers in context leases 2025-10-29 16:25:38 +01:00
Marek Lemańczyk b753654ebb Enforce valid divisor selectors in heuristic combined tester 2025-10-29 15:32:40 +01:00
Marek Lemańczyk b0158a844a Use divisor squares to bound heuristic combined trial division 2025-10-29 15:13:54 +01:00
mlemanczyk 4ab77ba170 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-29 14:06:08 +01:00
Marek Lemańczyk 47eee81d3c Merge pull request #248 from mlemanczyk/codex/optimize-heuristicprimetester-for-divisor-selection
Add filtered group B divisor tables for heuristic GPU checks
2025-10-29 14:05:02 +01:00
mlemanczyk b2865b54ce Tiny optimization 2025-10-29 14:04:25 +01:00
mlemanczyk 4c79c4087e Various optimizations 2025-10-29 14:00:38 +01:00
mlemanczyk 3147728d5b Benchmarks for heuristic combined methods added 2025-10-29 04:53:13 +01:00
mlemanczyk 880b1cd962 Revert "Unroll divisor-by-divisor GPU kernel loops"
This reverts commit 5d14efb739.
2025-10-29 04:51:28 +01:00
Marek Lemańczyk 5d14efb739 Unroll divisor-by-divisor GPU kernel loops 2025-10-29 04:31:45 +01:00
Marek Lemańczyk f49963c4f6 Restore Math.Sqrt heuristic bound 2025-10-29 01:43:12 +01:00
Marek Lemańczyk 5a9e103bd5 Replace heuristic sqrt with log2-based estimate 2025-10-29 01:29:12 +01:00
Marek Lemańczyk eeeff42be6 Replace heuristic sqrt with integer algorithm 2025-10-29 01:02:31 +01:00
Marek Lemańczyk b54f4b6bd6 Simplify combined heuristic divisor selection 2025-10-29 00:44:46 +01:00
Marek Lemańczyk b2cb17a96f Return to one-A-one-B combined schedule 2025-10-29 00:44:39 +01:00
Marek Lemańczyk f993561cd5 Eagerly build combined divisors and restore two-A-one-B default 2025-10-28 23:43:15 +01:00
Marek Lemańczyk e8d163f595 Add three-A-two-B combined divisor schedule 2025-10-28 23:22:16 +01:00
Marek Lemańczyk 465b40bf35 Add three-A-one-B schedule to combined tester 2025-10-28 23:09:41 +01:00
Marek Lemańczyk 4c445f2a42 Switch combined heuristic divisors to 1A-1B schedule 2025-10-28 22:55:17 +01:00
Marek Lemańczyk 41bff698e8 Add combined heuristic prime tester with shared divisor arrays 2025-10-28 22:40:51 +01:00
mlemanczyk 0850227881 Switched to heuristic GPU for a better performance than the standard one. 2025-10-28 22:15:33 +01:00
Marek Lemańczyk c693a05708 Add reusable group B divisor tables without trailing-three primes 2025-10-28 21:56:18 +01:00
Marek Lemańczyk b83bddf49a Merge pull request #247 from mlemanczyk/codex/fix-unhandled-exception-in-evenperfectbitscanner
Fix GPU Montgomery multiply to avoid unsupported intrinsics
2025-10-28 21:10:09 +01:00
mlemanczyk e5f5cd563e Merge branch 'codex/fix-unhandled-exception-in-evenperfectbitscanner' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/fix-unhandled-exception-in-evenperfectbitscanner 2025-10-28 21:08:14 +01:00
Marek Lemańczyk 3304335072 Rename CPU Montgomery multiply and align GPU helper 2025-10-28 21:06:40 +01:00
mlemanczyk ab045a62c4 Revert "Temporarily enabling heuristic GPU order checks."
This reverts commit 4db06cfe80.
2025-10-28 21:04:09 +01:00
Marek Lemańczyk eaee599f25 Use GPU-safe Montgomery multiply in kernels 2025-10-28 20:58:02 +01:00
mlemanczyk 4db06cfe80 Temporarily enabling heuristic GPU order checks. 2025-10-28 20:49:41 +01:00
mlemanczyk bead40713c Switching .IsPrime check to CPU for much higher performance. 2025-10-28 20:49:11 +01:00
mlemanczyk ca8ab2bcf8 Tester made static to better mimic production code. 2025-10-28 20:48:35 +01:00
Marek Lemańczyk d149700311 Merge pull request #246 from mlemanczyk/codex/compare-cpu-and-gpu-execution-paths
Make GPU sieve results authoritative
2025-10-28 20:14:33 +01:00
Marek Lemańczyk 1792349bad Make GPU sieve results authoritative 2025-10-28 20:14:22 +01:00
mlemanczyk 2f461ee001 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-28 20:12:45 +01:00
mlemanczyk 93aed56f23 Non-heuristic GPU benchmark uncommented, since we improved it. 2025-10-28 20:12:41 +01:00
Marek Lemańczyk ff9774925e Merge pull request #245 from mlemanczyk/codex/move-buildsmallprimes-to-divisorgenerator-g256a9
Enable residue-aware GPU sieve in PrimeTester
2025-10-28 20:00:22 +01:00
Marek Lemańczyk 1f9421430b Reuse GPU divisor tables across PrimeTester batches 2025-10-28 19:53:11 +01:00
Marek Lemańczyk c53966e481 Enable residue-aware GPU sieve in PrimeTester 2025-10-28 18:39:18 +01:00
mlemanczyk 3d43d52f2e Console logging updated 2025-10-28 16:04:42 +01:00
Marek Lemańczyk fa4bb0bcc1 Merge pull request #243 from mlemanczyk/codex/modify-evenperfectbitscanner-to-use-taskthreadpool
Refactor by-divisor scheduling to use task thread pool
2025-10-28 15:47:47 +01:00
Marek Lemańczyk c4c783610b Add start gate to by-divisor task scheduling 2025-10-28 15:32:15 +01:00
Marek Lemańczyk 29f58e3059 Refactor by-divisor scheduling to use task thread pool 2025-10-28 15:11:21 +01:00
Marek Lemańczyk 29af0cb0ce Merge pull request #242 from mlemanczyk/codex/modify-taskthreadpool-to-terminate-threads-after-tasks
Stop tracking double disposal in TaskThreadPool
2025-10-28 14:29:48 +01:00
Marek Lemańczyk 3edf2cd575 Stop tracking double disposal in TaskThreadPool 2025-10-28 14:29:34 +01:00
Marek Lemańczyk a89ed7576e Merge pull request #241 from mlemanczyk/codex/remove-executionscope-structure
Remove GPU context creation lock
2025-10-28 13:39:43 +01:00
Marek Lemańczyk 86990ae0f7 Remove kernel initialization lock from GPU leases 2025-10-28 13:30:47 +01:00
Marek Lemańczyk de7ddd8db3 Remove GPU context creation lock 2025-10-28 13:26:20 +01:00
mlemanczyk 139899fa5e More console logs added. 2025-10-28 02:11:24 +01:00
Marek Lemańczyk c39a99a4fd Merge pull request #240 from mlemanczyk/codex/analyze-mersenne=bydivisor-loops-for-early-breaks-kynlfe
Remove temporary by-divisor review documents
2025-10-28 02:01:52 +01:00
Marek Lemańczyk 6dd5cea79f Remove temporary by-divisor review documents 2025-10-28 02:01:28 +01:00
Marek Lemańczyk bd0727131f Merge pull request #238 from mlemanczyk/codex/optimize-evenperfectbitscanner-loops-kit1so
Hoist prime order thresholds in CPU heuristics
2025-10-27 21:31:23 +01:00
Marek Lemańczyk 968dd89d0d Remove by-divisor path map document 2025-10-27 21:28:41 +01:00
Marek Lemańczyk 94a4ed8328 Hoist prime order thresholds in CPU heuristics 2025-10-27 21:24:43 +01:00
mlemanczyk d381abe0cc We'll now clear the dictionary after renting, to avoid unnecessary clearing if it's not going to be used anymore. 2025-10-27 16:54:49 +01:00
mlemanczyk 50d66a5186 Switched to GPU version in some places. 2025-10-27 15:30:10 +01:00
Marek Lemańczyk 1bd3d5c163 Merge pull request #235 from mlemanczyk/codex/modify-primetester.isprime-for-gpu
Add GPU prime tester overload for preallocated buffers
2025-10-27 00:03:38 +01:00
Marek Lemańczyk 9a0e328995 Reintroduce MulHigh CPU helper and tighten GPU sieve loop 2025-10-26 23:41:14 +01:00
Marek Lemańczyk e2f68cedf2 Short-circuit small prime sieve kernel loops 2025-10-26 22:42:27 +01:00
Marek Lemańczyk 0e1212aa1c Split MulHigh extensions into CPU and GPU variants 2025-10-26 22:42:21 +01:00
Marek Lemańczyk 3d458fc6da Use precomputed small prime squares in GPU sieve 2025-10-26 22:28:04 +01:00
Marek Lemańczyk 794e092e57 Refactor GPU partial factoring context handling 2025-10-26 21:46:11 +01:00
Marek Lemańczyk ba439f0f0f Reuse GPU context for nested partial factoring 2025-10-26 20:58:21 +01:00
Marek Lemańczyk 6458f6740d Add GPU prime tester overload for preallocated buffers 2025-10-26 20:30:44 +01:00
mlemanczyk f2ce40ade9 Additional logging added. 2025-10-26 20:00:32 +01:00
Marek Lemańczyk 5c9fb47a3d Merge pull request #234 from mlemanczyk/codex/restore-original-primetester-path
Restore CPU PrimeTester usage and rename fallback helpers
2025-10-26 19:09:05 +01:00
Marek Lemańczyk 3758524450 Extract divisor validator and rename legacy CPU prime helper 2025-10-26 18:59:07 +01:00
Marek Lemańczyk 1e5eadac34 Refine heuristic prime tester integration 2025-10-26 18:15:27 +01:00
Marek Lemańczyk 21d645fcf0 Comment out legacy PrimeTester guards 2025-10-26 18:15:22 +01:00
Marek Lemańczyk 55554d9565 Inline legacy GPU sieve and update heuristic fallbacks 2025-10-26 17:06:13 +01:00
Marek Lemańczyk a7fc34e37a Inline PrimeTester CPU path and reuse thread pools 2025-10-26 16:45:18 +01:00
Marek Lemańczyk 9bc8fd5043 Remove heuristic hooks from PrimeTester 2025-10-26 16:18:46 +01:00
Marek Lemańczyk c01abbd03a Restore legacy by-divisor scans for Mersenne testers 2025-10-26 16:04:09 +01:00
Marek Lemańczyk a83ae56caf Use LastDigit enum across Mersenne scanners 2025-10-26 16:04:05 +01:00
Marek Lemańczyk 43c7bafd5d Restore CPU PrimeTester usage and rename fallback helpers 2025-10-26 15:19:49 +01:00
Marek Lemańczyk ff1860a7b3 Merge pull request #233 from mlemanczyk/codex/modify-primetester-for-batch-heuristics
Interleave heuristic prime tester batches across groups
2025-10-26 13:12:45 +01:00
mlemanczyk 1ed5afe689 Switched to .IsPrime which performed best in benchmarks. 2025-10-26 04:05:00 +01:00
Marek Lemańczyk a747f29ac2 Interleave heuristic prime tester batches across groups 2025-10-26 04:01:56 +01:00
Marek Lemańczyk 6e632ba03a Merge pull request #232 from mlemanczyk/codex/analyze-evenperfectbitscanner-paths-and-implement-methods
Add static heuristic prime sieves
2025-10-26 03:42:39 +01:00
Marek Lemańczyk 1f7680a465 Use heuristic sieves for prime trial division 2025-10-26 03:41:04 +01:00
Marek Lemańczyk 15c229665d Add heuristic prime sieves for cached divisor sequences 2025-10-26 03:21:29 +01:00
Marek Lemańczyk 4ee6464130 Merge pull request #230 from mlemanczyk/codex/optimize-gpu-memory-usage-in-kernels
Reduce GPU by-divisor host allocations
2025-10-25 06:04:43 +02:00
mlemanczyk 5276d9d27b Merge branch 'codex/optimize-gpu-memory-usage-in-kernels' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/optimize-gpu-memory-usage-in-kernels 2025-10-25 05:59:55 +02:00
mlemanczyk 74ab4e91e6 Console logging info added to know when a task starts & completes 2025-10-25 05:59:47 +02:00
Marek Lemańczyk 8e5a1cd4c7 Batch by-divisor GPU tasks across prime ranges 2025-10-25 05:58:50 +02:00
Marek Lemańczyk 2d103a9662 Compute initial divisor remainders inside GPU kernel 2025-10-25 05:41:05 +02:00
Marek Lemańczyk 32f9f25e9b Share GPU by-divisor batch resources across accelerators 2025-10-25 05:03:16 +02:00
Marek Lemańczyk 3735a1cf0f Gate GPU by-divisor scans to a single accelerator 2025-10-25 05:00:54 +02:00
Marek Lemańczyk 6b1c2d0665 Reuse GPU by-divisor buffers to curb host allocations 2025-10-25 03:45:46 +02:00
Marek Lemańczyk 2b00a53c78 Merge pull request #229 from mlemanczyk/codex/analyze-and-implement-exponentremainderstepper-for-gpu-lbfhg2
Optimize GPU divisor heuristics pipeline
2025-10-25 02:33:27 +02:00
Marek Lemańczyk 04d4d18f37 Rename GPU mul-mod helper 2025-10-25 02:20:15 +02:00
Marek Lemańczyk 4b13bc6497 Switch GPU powmod host helpers to simplified mul-mod 2025-10-25 02:20:09 +02:00
Marek Lemańczyk 03194650c9 Add GPU simplified mul-mod helper for host callers 2025-10-25 02:20:02 +02:00
Marek Lemańczyk 3e081141e0 Document GPU mul-mod benchmark guidance 2025-10-25 02:19:57 +02:00
Marek Lemańczyk d0ba08dd24 Fix GPU mul mod benchmark kernel initialization 2025-10-24 22:08:32 +02:00
Marek Lemańczyk e566339f09 Add GPU MulMod64 benchmarks 2025-10-24 22:00:46 +02:00
Marek Lemańczyk 2888a1f9b3 Deprecate MulMod64Gpu and migrate host callers 2025-10-24 22:00:41 +02:00
Marek Lemańczyk 8795b460c3 Document GPU by-divisor MulMod usage 2025-10-24 22:00:36 +02:00
Marek Lemańczyk ad19096bab Use constant residue initialization in GPU stepper 2025-10-24 21:59:51 +02:00
Marek Lemańczyk 4e9d57df4b Unify GPU divisor kernels with stepper path 2025-10-24 20:50:52 +02:00
Marek Lemańczyk 868c7a4fd2 Tighten GPU divisor heuristics and stepper usage 2025-10-24 20:50:46 +02:00
Marek Lemańczyk fab9ba0413 Restore reviewed divisor heuristics 2025-10-24 19:50:32 +02:00
Marek Lemańczyk bbe0c02069 Use bit masks for divisor heuristics 2025-10-24 19:50:27 +02:00
Marek Lemańczyk 46a2178d65 Remove Montgomery data from GPU divisor flow 2025-10-24 18:56:28 +02:00
Marek Lemańczyk 69acd3dad9 Cache GPU divisor partial data on GPU path 2025-10-24 18:56:21 +02:00
Marek Lemańczyk cc281ab423 Refactor GPU powmod helpers into ULongExtensions 2025-10-24 17:26:55 +02:00
Marek Lemańczyk 557bc5d06f Use field-based exponent tracking for steppers 2025-10-24 17:26:49 +02:00
Marek Lemańczyk d20362a2fc Switch GPU by-divisor stepper to standard modular arithmetic 2025-10-24 09:20:17 +02:00
Marek Lemańczyk 1e5a9128cb Split CPU and GPU exponent steppers 2025-10-24 08:49:53 +02:00
Marek Lemańczyk 035956cdb6 Replace GPU by-divisor kernel with standard modular exponentiation 2025-10-24 08:49:48 +02:00
Marek Lemańczyk fbd6235316 Handle zero-cycle GPU divisor sessions with heuristics 2025-10-24 06:40:33 +02:00
Marek Lemańczyk cf9d143ca2 Remove GPU by-divisor snapshot dependency 2025-10-24 05:05:21 +02:00
Marek Lemańczyk 8637ee8875 Restore GPU divisor heuristics to match cycle cache 2025-10-24 05:05:16 +02:00
Marek Lemańczyk 8a258f7536 Collapse GPU by-divisor batching to a single launch 2025-10-24 01:23:38 +02:00
Marek Lemańczyk 1075ab1ea1 Fuse GPU hit detection with divisor evaluation 2025-10-24 01:23:31 +02:00
Marek Lemańczyk 40d3b4b3ac Fuse GPU by-divisor candidate filtering into one kernel 2025-10-24 01:23:25 +02:00
Marek Lemańczyk ab1a069e57 Optimize GPU divisor heuristics pipeline 2025-10-24 00:29:26 +02:00
Marek Lemańczyk 6af0ae900e Merge pull request #227 from mlemanczyk/codex/refactor-exponentremainderstepper-structure
Clarify Pollard-Rho guard and seed CPU stepping upfront
2025-10-23 20:15:59 +02:00
Marek Lemańczyk 7feeb08943 Clarify Pollard-Rho parity guard and seed CPU batches 2025-10-23 19:50:39 +02:00
mlemanczyk 0d7df2e45b Unnecessary memory zeroing commented out. TODOs added for the remainders. 2025-10-23 19:42:03 +02:00
mlemanczyk 8e3e990499 Throwing exception added in case of invalid modulus to validate if the check ever kicks off 2025-10-23 16:43:52 +02:00
Marek Lemańczyk c03aa53689 Merge pull request #226 from mlemanczyk/codex/analyze-and-optimize-memory-cleaning-in-evenperfectbitscanne
Avoid redundant GPU compaction buffer clearing
2025-10-23 16:34:41 +02:00
Marek Lemańczyk c697a7e127 Throw when divisor cycle resolution fails 2025-10-23 01:48:11 +02:00
Marek Lemańczyk ff9b0122c0 Restore hit clearing when divisor cycle is missing 2025-10-23 01:48:06 +02:00
Marek Lemańczyk f0b4cdf6e2 Avoid redundant clearing in CPU divisor sessions 2025-10-23 01:48:02 +02:00
Marek Lemańczyk f672ff520c Avoid redundant compaction buffer clearing on GPU 2025-10-23 01:16:58 +02:00
Marek Lemańczyk dda53581c0 Merge pull request #225 from mlemanczyk/codex/analyze-evenperfectbitscanner-for-gpu-optimization
Add GPU candidate generation for divisor scanning
2025-10-23 00:52:59 +02:00
Marek Lemańczyk ea01585892 Convert divisor session results to hits on the GPU 2025-10-23 00:34:17 +02:00
Marek Lemańczyk faab60c2cd Reduce host hit detection overhead 2025-10-22 23:51:19 +02:00
Marek Lemańczyk 4d9158e8f3 Make GPU mask compaction branchless with shared staging 2025-10-22 22:01:39 +02:00
Marek Lemańczyk ea0770a84a Clarify stream kernel bounds assumptions 2025-10-22 21:41:51 +02:00
Marek Lemańczyk 586eb8d303 Document hot-path guards in GPU mask compaction helpers 2025-10-22 18:48:03 +02:00
Marek Lemańczyk add62df697 Compact GPU divisor masks on device 2025-10-22 18:47:55 +02:00
Marek Lemańczyk bae39ad95e Capture GPU next start for divisor batches 2025-10-22 06:43:05 +02:00
Marek Lemańczyk 5b973fb513 Document and streamline GPU candidate kernel guards 2025-10-22 06:43:00 +02:00
Marek Lemańczyk 4d8ea9dccd Use GpuUInt128 in GPU divisor candidate generation 2025-10-22 06:05:22 +02:00
Marek Lemańczyk 6c3efb90c9 Merge pull request #222 from mlemanczyk/codex/refactor-primetester.isprimegpu-for-branch-less-execution
Clarify GPU sieve invariants and trim defensive guards
2025-10-22 05:24:57 +02:00
Marek Lemańczyk bfc5712d81 Add GPU kernel for divisor candidate generation 2025-10-22 05:24:01 +02:00
Marek Lemańczyk ab4388dfba Merge pull request #224 from mlemanczyk/codex/optimize-branch-less-paths-for-mersenne
Simplify GPU by-divisor configuration guards
2025-10-22 03:49:22 +02:00
Marek Lemańczyk 00341fbb8c Simplify GPU by-divisor configuration guards 2025-10-22 03:49:04 +02:00
mlemanczyk 43cd067e09 Formatting 2025-10-22 01:47:40 +02:00
Marek Lemańczyk 4baca1ba70 Merge pull request #223 from mlemanczyk/codex/refactor-pow2montgomerygpuexecutor.execute-for-branch-less-c
Reduce branching in Pow2Montgomery GPU execution path
2025-10-22 01:43:41 +02:00
Marek Lemańczyk 24eaec7a21 Refine Montgomery GPU helpers to avoid kernel branching 2025-10-22 01:43:10 +02:00
Marek Lemańczyk f12919e220 Split Pow2 Montgomery GPU kernels by Montgomery conversion 2025-10-22 01:43:02 +02:00
Marek Lemańczyk b08175dadf Simplify Pow2Montgomery GPU executor branching 2025-10-22 01:17:35 +02:00
Marek Lemańczyk f2070a0f64 Document CPU sieve invariants and cache boolean guards 2025-10-22 00:56:17 +02:00
mlemanczyk 865a88fac0 Switched to GPU for prime testing 2025-10-22 00:40:30 +02:00
Marek Lemańczyk fa6f75c158 Document GPU sieve invariants 2025-10-22 00:38:51 +02:00
Marek Lemańczyk 27739acd02 Merge pull request #221 from mlemanczyk/codex/ensure-primetester.isprimegpu-has-its-own-accelerator-pool
Add dedicated GPU accelerator pool for PrimeTester GPU sieve
2025-10-21 22:36:31 +02:00
Marek Lemańczyk 3e7606025d Add dedicated GPU context pool for PrimeTester 2025-10-21 22:34:30 +02:00
Marek Lemańczyk ebb87d6513 Merge pull request #220 from mlemanczyk/codex/analyze-gpu-execution-path-for-evenperfectbitscanner
Isolate residue GPU leases and drop try/finally
2025-10-21 22:02:03 +02:00
Marek Lemańczyk 3afd9399db Isolate residue GPU leases and drop try/finally 2025-10-21 22:01:50 +02:00
mlemanczyk 40ba538587 Switched to range operator from Slice, where applicable. 2025-10-21 21:28:55 +02:00
Marek Lemańczyk 7631b2d4cb Merge pull request #219 from mlemanczyk/codex/refactor-gpu-kernel-launch-process
Inline GPU divisor locking adjustments
2025-10-21 21:27:12 +02:00
Marek Lemańczyk 819cb7b00b Guard residue GPU scanning with execution lock 2025-10-21 21:26:53 +02:00
Marek Lemańczyk 5dd9eb636d Inline GPU divisor locking adjustments 2025-10-21 21:19:57 +02:00
Marek Lemańczyk 9bcf1bd9a1 Merge pull request #218 from mlemanczyk/codex/analyze-mersenne=bydivisor-path-for-tracking-remainder-bbul2g
Reuse stack spans across divisor processing loops
2025-10-21 20:54:45 +02:00
Marek Lemańczyk a226ae10cb Stop clearing pooled buffers inside rent helpers 2025-10-21 20:52:21 +02:00
Marek Lemańczyk 7baf3b619f Simplify exception guards in CPU remainder paths 2025-10-21 20:52:14 +02:00
Marek Lemańczyk 6adf78faf6 Streamline CPU remainder buffers 2025-10-21 20:52:09 +02:00
Marek Lemańczyk b418d79fb3 Adopt fixed stack capacities for CPU prime order helpers 2025-10-21 19:46:59 +02:00
Marek Lemańczyk c15405e3b1 Reuse stack buffers when lowering divisor exponents 2025-10-21 18:22:56 +02:00
mlemanczyk 31a0b8d706 Added static IsPrimeGpu 2025-10-21 04:09:26 +02:00
mlemanczyk ea11db6d0f Restored the Dictionary parameter instead of arrays 2025-10-21 04:08:48 +02:00
mlemanczyk 8dc7751084 Switched back to PrimeTester.IsPrimeInternal for teseting 2025-10-21 04:07:49 +02:00
Marek Lemańczyk 1b3a89ca53 Merge pull request #216 from mlemanczyk/codex/remove-factorcache-usage-in-evenperfectbitscanner
Remove per-thread factorization cache from Mersenne divisor workflows
2025-10-20 19:28:17 +02:00
Marek Lemańczyk 8acccce471 Add cycle cache hit tracking instrumentation 2025-10-20 19:28:06 +02:00
Marek Lemańczyk 1a4818c794 Add diagnostics for removed factorization cache 2025-10-20 19:22:02 +02:00
Marek Lemańczyk 28ff9292b8 Remove per-thread factorization cache 2025-10-20 19:08:44 +02:00
Marek Lemańczyk 20f30dc182 Merge pull request #215 from mlemanczyk/codex/analyze-cpu-paths-in-evenperfectbitscanner
Adjust CPU pow2 Montgomery helper for small divisors
2025-10-20 18:41:38 +02:00
Marek Lemańczyk 33f9b51593 Clarify residue stepper usage and update TODO tracker 2025-10-20 18:37:35 +02:00
Marek Lemańczyk da6ea25729 Address windowed pow2 review feedback 2025-10-20 18:37:29 +02:00
Marek Lemańczyk 481d1010e8 Retire outdated windowed pow2 TODOs 2025-10-20 18:37:21 +02:00
Marek Lemańczyk 360a31bd54 Align Lucas-Lehmer GPU helpers with windowed pow2 guidance 2025-10-20 18:37:15 +02:00
Marek Lemańczyk 82e44e3f3f Confirm windowed pow2 usage across GPU kernels 2025-10-20 18:37:08 +02:00
Marek Lemańczyk 7cbc27d6c3 Use adaptive pow2 stepping in residue stepper 2025-10-20 18:36:58 +02:00
Marek Lemańczyk 4831fc84d1 Clean up resolved windowed pow2 TODOs on CPU paths 2025-10-20 18:36:51 +02:00
Marek Lemańczyk 798ae455b1 Adjust CPU Pow2 Montgomery helper for small divisors 2025-10-20 17:03:36 +02:00
mlemanczyk 5d5fb28765 Cache removed. It was barely every hit. 2025-10-20 16:56:49 +02:00
Marek Lemańczyk c726d6de6b Merge pull request #214 from mlemanczyk/codex/analyze-cpu-paths-in-evenperfectbitscanner
Restore cycle remainder TODO comments
2025-10-20 16:07:08 +02:00
Marek Lemańczyk 9540a9935d Restore cycle remainder TODO comments 2025-10-20 16:06:46 +02:00
Marek Lemańczyk ecc718bc69 Merge pull request #213 from mlemanczyk/codex/check-and-complete-todos-in-mulmod-benchmarks
Inline GpuUInt128 Mul64 helper
2025-10-20 13:58:34 +02:00
Marek Lemańczyk de8e769f48 Switch GpuUInt128 internals to in-place shifts 2025-10-20 13:58:16 +02:00
Marek Lemańczyk 50d2e462d7 Use in-place shifts in GpuUInt128.MulMod 2025-10-20 13:58:10 +02:00
Marek Lemańczyk d0c9389bcc Document GpuUInt128 MulMod shift benchmarking 2025-10-20 13:58:04 +02:00
Marek Lemańczyk 1809ba95b3 Inline GpuUInt128 Mul64 helper 2025-10-20 13:30:34 +02:00
Marek Lemańczyk 73c9272ce4 Merge pull request #212 from mlemanczyk/codex/modify-taskthreadpool-to-sleep-threads
Keep TaskThreadPool workers blocked when idle
2025-10-20 12:15:47 +02:00
Marek Lemańczyk 5fbbfd7590 Keep TaskThreadPool workers idle until work arrives 2025-10-20 12:15:31 +02:00
Marek Lemańczyk 9c08cf48a7 Merge pull request #211 from mlemanczyk/codex/analyze-mulmod-calculations-in-evenperfectbitscanner
Consolidate MulMod helpers across CPU factoring path
2025-10-20 11:53:00 +02:00
Marek Lemańczyk ea2476c749 Clarify modular helper invariants 2025-10-20 11:52:04 +02:00
Marek Lemańczyk 0f51454496 Remove cancellation-aware UInt128 ModPow overload 2025-10-20 11:48:14 +02:00
Marek Lemańczyk c892e1c30c Trim unreachable MulMod conditionals 2025-10-20 11:48:10 +02:00
Marek Lemańczyk 94323c2e8c Document unreachable MulMod guards 2025-10-20 11:48:07 +02:00
mlemanczyk 4e553c951b Additional console info added 2025-10-20 11:15:34 +02:00
Marek Lemańczyk 34240ab3e9 Consolidate MulMod helpers across CPU factoring path 2025-10-20 11:12:39 +02:00
Marek Lemańczyk bfc6605e97 Merge pull request #210 from mlemanczyk/codex/optimize-partialfactor-method-for-isprime-caching
Cache composite primality results during partial factorization
2025-10-20 01:07:24 +02:00
Marek Lemańczyk a7ed762428 Simplify scheduler configuration and pending composite caching 2025-10-20 01:07:02 +02:00
Marek Lemańczyk e9cff7bfcd Reuse heuristic prime order config across runtime 2025-10-20 00:51:10 +02:00
Marek Lemańczyk d99610098a Store Pollard Rho primality on pending entries 2025-10-20 00:41:32 +02:00
Marek Lemańczyk 752c07c57e Persist partial factor primality in pending entries 2025-10-20 00:14:36 +02:00
Marek Lemańczyk bad5c28fdf Align scheduler thread count with CLI configuration 2025-10-20 00:01:21 +02:00
Marek Lemańczyk e4f6cbbe25 Requeue timed-out Pollard Rho composites 2025-10-19 23:48:12 +02:00
Marek Lemańczyk 43e2894661 Refine partial factor primality caching 2025-10-19 23:48:07 +02:00
Marek Lemańczyk 0fbd635828 Reuse pooled local cache for partial factor primality checks 2025-10-19 23:35:08 +02:00
Marek Lemańczyk b7c187bbc5 Check Pollard Rho deadline inside partial factor loop 2025-10-19 23:16:35 +02:00
Marek Lemańczyk 4917f690fc Make partial factor primality cache thread static 2025-10-19 22:59:44 +02:00
Marek Lemańczyk 627d2e9b14 Cache composite primality results in PartialFactor 2025-10-19 22:49:38 +02:00
Marek Lemańczyk 324080d265 Merge pull request #209 from mlemanczyk/codex/implement-threadpool-in-perfectnumbers.core
Adjust scheduler thread pool configuration
2025-10-19 21:51:32 +02:00
Marek Lemańczyk eda3c2128a Align task scheduler defaults with guidance 2025-10-19 21:51:22 +02:00
Marek Lemańczyk 9cb50d77ca Merge pull request #208 from mlemanczyk/codex/analyze-cache-usage-in-evenperfectbitscanner
Remove unused by-divisor cache fallback
2025-10-19 21:25:42 +02:00
Marek Lemańczyk 653da2ccc3 Remove dead by-divisor cache fallback 2025-10-19 21:24:07 +02:00
Marek Lemańczyk 5799630919 Merge pull request #207 from mlemanczyk/codex/implement-pow2montgomerymodwindowedcpu-for-gpu
Restore guard comments in pow2 GPU helpers
2025-10-19 20:27:09 +02:00
Marek Lemańczyk 79e8706644 Restore original guard comments in pow2 helpers 2025-10-19 20:21:09 +02:00
mlemanczyk 6ff033a568 Instructions updated. 2025-10-19 07:24:00 +02:00
Marek Lemańczyk da7a6cb23d Merge pull request #206 from mlemanczyk/codex/implement-todos-in-mersennedivisorcycles.cs
Inline resource cleanup in GPU Montgomery executor
2025-10-19 06:11:03 +02:00
Marek Lemańczyk 37ab77e5db Restore GPU executor commented catch guidance 2025-10-19 06:10:54 +02:00
Marek Lemańczyk 75f815567d Remove try/finally from GPU Montgomery executor 2025-10-19 06:03:39 +02:00
Marek Lemańczyk 4fcf6254e6 Merge pull request #205 from mlemanczyk/codex/update-todo_list.txt-with-new-todos
Update TODO tracker for by-divisor path
2025-10-19 04:47:44 +02:00
Marek Lemańczyk f4a3685707 Refresh by-divisor TODO references 2025-10-19 04:47:14 +02:00
mlemanczyk ee52920ad9 Restored lower values 2025-10-19 04:26:21 +02:00
mlemanczyk c0b8fa07a2 Tiny cleanup 2025-10-19 03:49:00 +02:00
mlemanczyk 9e5123d3cc Resolved TODO removed 2025-10-19 03:48:17 +02:00
Marek Lemańczyk 62577a271b Merge pull request #204 from mlemanczyk/codex/execute-todos-from-latest-commit
Add tests for CalculationResultsFile helpers
2025-10-19 03:36:44 +02:00
Marek Lemańczyk 1e738f9e0d Add CalculationResultsFile coverage 2025-10-19 03:35:59 +02:00
Marek Lemańczyk 691a0d20b7 Merge pull request #203 from mlemanczyk/codex/fix-primeordergpuheuristicstests-issue
Adjust GPU pow2 tests for large exponent coverage
2025-10-19 01:49:24 +02:00
Marek Lemańczyk 405436988f Update GPU pow2 tests to use realistic exponents 2025-10-19 01:49:12 +02:00
mlemanczyk 2c6e9f5460 Tiny clarification added 2025-10-19 01:22:00 +02:00
mlemanczyk 2dcc51bfc0 Tiny cleanup 2025-10-19 01:19:20 +02:00
mlemanczyk ef5f91ddd0 TODO added. Small cleanup. 2025-10-19 01:16:12 +02:00
mlemanczyk 4ef7f636d2 Tiny cleanup 2025-10-19 00:57:41 +02:00
Marek Lemańczyk 1b16e5d2f0 Merge pull request #202 from mlemanczyk/codex/execute-and-remove-todos-from-last-commit
Extract candidate generation utilities into dedicated classes
2025-10-19 00:55:12 +02:00
Marek Lemańczyk 56c8e84c19 Align bit transform comments with original implementation 2025-10-19 00:54:58 +02:00
mlemanczyk 7f468de485 TODOs added 2025-10-18 23:59:18 +02:00
Marek Lemańczyk e78150b242 Merge pull request #201 from mlemanczyk/codex/remove-todos-from-latest-commit
Restore documentation for handler and bit statistics invariants
2025-10-18 23:18:23 +02:00
Marek Lemańczyk 8108d973ca Restore initialization guidance comments 2025-10-18 23:18:11 +02:00
mlemanczyk 98afb3ff9e TODO updated and small cleanups 2025-10-18 22:31:36 +02:00
Marek Lemańczyk 4ac2ef1e6e Merge pull request #200 from mlemanczyk/codex/implement-todos-in-evenperfectbitscanner
Move prime tracking into CalculationResultHandler
2025-10-18 22:05:37 +02:00
Marek Lemańczyk e338497c2f Move prime tracking into CalculationResultHandler 2025-10-18 22:05:24 +02:00
mlemanczyk 194f10d00b Instructions updated. 2025-10-18 20:00:14 +02:00
Marek Lemańczyk fcefaad29f Merge pull request #199 from mlemanczyk/codex/analyze-mersenne=bydivisor-path-for-todos
Refine by-divisor buffer handling and CLI parser
2025-10-18 19:58:14 +02:00
Marek Lemańczyk e6f1f87d3b Extract UTF-8 CLI parser helper 2025-10-18 19:58:04 +02:00
mlemanczyk ca07ed3388 Tiny cleanup 2025-10-18 18:08:50 +02:00
Marek Lemańczyk 2ba193bd65 Merge pull request #198 from mlemanczyk/codex/napraw-bedy-kompilacji-kerneli
Fix kernel integration compile errors
2025-10-18 18:02:05 +02:00
Marek Lemańczyk 8adeb59acc Fix kernel integration compile errors 2025-10-18 18:01:52 +02:00
mlemanczyk 9232778173 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-18 17:48:42 +02:00
mlemanczyk 6928d43aee TODO added 2025-10-18 17:48:38 +02:00
Marek Lemańczyk e87488a93e Merge pull request #197 from mlemanczyk/codex/przeredaguj-plik-agents.md-na-punkty
Clarify numeric helper placement guidance
2025-10-18 17:44:00 +02:00
Marek Lemańczyk 473d4efcdf Clarify numeric helper placement guidance 2025-10-18 17:43:24 +02:00
Marek Lemańczyk fbb1167433 Merge pull request #196 from mlemanczyk/codex/refactor-gpukernelpool-into-kernels-directory
Add kernel audit progress log for GPU namespace
2025-10-18 17:42:51 +02:00
Marek Lemańczyk 6217ea7888 Document GPU kernel audit progress 2025-10-18 17:42:33 +02:00
mlemanczyk 14b72a6a2a Console logging extended 2025-10-18 17:06:56 +02:00
Marek Lemańczyk 1e25ea0ccf Merge pull request #195 from mlemanczyk/codex/add-console-notifications-in-benchmarks
Add console notifications for benchmark data preparation
2025-10-18 14:44:26 +02:00
Marek Lemańczyk d1f3de07d7 Add console logs for benchmark data preparation 2025-10-18 14:44:14 +02:00
mlemanczyk 44f65b1c8f Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-18 14:36:47 +02:00
mlemanczyk 84b759e802 Failing benchmark disabled 2025-10-18 14:21:06 +02:00
mlemanczyk 5fbd5bc20c Small optimizations. Branches which we don't visit in runtime disabled. TODOs added. 2025-10-18 14:20:46 +02:00
Marek Lemańczyk 9f05dfb44d Merge pull request #194 from mlemanczyk/codex/merge-pow2montgomery-benchmark-classes
Preserve Pow2 benchmark dataset reuse
2025-10-18 12:22:50 +02:00
Marek Lemańczyk c6ad53fffb Restore Pow2 benchmark data reuse 2025-10-18 12:22:08 +02:00
mlemanczyk 7a22fbbba7 Original benchmark selector restored. 2025-10-18 01:34:53 +02:00
mlemanczyk c4dfbf0fb2 Benchmarks disable / re-enabled, were heuristic is / is not used. 2025-10-18 01:34:26 +02:00
Marek Lemańczyk b2ed0d5832 Merge pull request #193 from mlemanczyk/codex/optimize-benchmarks-for-data-collection
Cache benchmark data to avoid regenerating per iteration
2025-10-18 01:10:12 +02:00
Marek Lemańczyk a1bbd23594 Cache benchmark sample data across iterations 2025-10-18 01:09:59 +02:00
Marek Lemańczyk d968f553af Merge pull request #192 from mlemanczyk/codex/review-and-merge-benchmarks-in-project
Comment out GPU heuristic cycle benchmarks
2025-10-18 00:38:06 +02:00
Marek Lemańczyk 3412834f7d Disable GPU heuristic cycle benchmarks 2025-10-18 00:29:06 +02:00
Marek Lemańczyk 0d048ac078 Merge pull request #191 from mlemanczyk/codex/analyze-and-clean-up-evenperfectbitscanner
Move CPU by-divisor documentation ahead of disabled guards
2025-10-17 18:14:15 +02:00
Marek Lemańczyk 7adea979fb Move by-divisor comments ahead of disabled guards 2025-10-17 18:14:01 +02:00
mlemanczyk b8031c5993 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 17:21:29 +02:00
Marek Lemańczyk 1be1fd5925 Merge pull request #190 from mlemanczyk/codex/analyze-evenperfectbitscanner-for-duplicate-primality-checks
Move pending entry pooling into shared thread pools
2025-10-17 17:21:20 +02:00
Marek Lemańczyk 83bcd8a0de Move pending entry pooling into shared thread pools 2025-10-17 17:20:43 +02:00
mlemanczyk 4cca19b1c2 TODO added 2025-10-17 17:06:18 +02:00
mlemanczyk be7c2b7ae5 Tiny optimization 2025-10-17 16:41:15 +02:00
mlemanczyk e525484b48 Primality tests normalized on GetOrComputePrimality. CofactorIsPrime added to factoring results for reuse in other places. 2025-10-17 16:24:59 +02:00
Marek Lemańczyk c0f38fa104 Merge pull request #189 from mlemanczyk/codex/analyze-evenperfectbitscanner-for-duplicate-primality-checks
Eliminate repeated primality checks in CPU order calculations
2025-10-17 15:05:59 +02:00
Marek Lemańczyk 8428e6c5bd Clarify skip heuristic guard and tighten order factor init 2025-10-17 15:05:11 +02:00
mlemanczyk 43cf4a978f Smaller optimizations. Division by zero fixed? 2025-10-17 14:18:49 +02:00
Marek Lemańczyk 5fcd082fff Merge pull request #188 from mlemanczyk/codex/add-unlimited-taskscheduler-subclass
Add unbounded task scheduler for divisor testing
2025-10-17 11:26:40 +02:00
Marek Lemańczyk 70637c3fba Add unbounded task scheduler for divisor testing 2025-10-17 11:26:28 +02:00
Marek Lemańczyk c4e56bf753 Merge pull request #186 from mlemanczyk/codex/modify-gpukernellease-for-pooling
Reuse pooled GPU kernel leases
2025-10-17 11:21:20 +02:00
Marek Lemańczyk 0613cb9d99 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' into codex/modify-gpukernellease-for-pooling 2025-10-17 11:21:09 +02:00
Marek Lemańczyk efb93cace0 Merge pull request #187 from mlemanczyk/codex/modify-gpukernellease-for-pooling-v09r6c
Skip zero-valued factors when checking special max order
2025-10-17 11:12:24 +02:00
Marek Lemańczyk fada9ff098 Ignore zero factors in TrySpecialMaxCpu 2025-10-17 08:54:55 +02:00
mlemanczyk e5d57364b5 GPU limit restored. GpuKernelLease will now return to the pool during Dispose 2025-10-17 08:49:22 +02:00
Marek Lemańczyk 964e748498 Pool GPU kernel leases for reuse 2025-10-17 07:04:11 +02:00
mlemanczyk fddec17b3f Various try-out changes 2025-10-17 06:53:54 +02:00
mlemanczyk 301e715179 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 06:03:17 +02:00
Marek Lemańczyk f5da594240 Merge pull request #185 from mlemanczyk/codex/replace-trypopulatesmallprimefactorscpu-with-gpu
Use GPU helper for partial factor small primes
2025-10-17 06:03:07 +02:00
Marek Lemańczyk ee4787d244 Use GPU helper for partial factor small primes 2025-10-17 06:02:54 +02:00
mlemanczyk 1e1ac8e0ca Gpu check restored 2025-10-17 06:02:20 +02:00
Marek Lemańczyk b2689529fa Merge pull request #184 from mlemanczyk/codex/modify-pow2montgomerymodwindowedgpu-for-gpu-wlp4qt
Add GPU small prime factor path
2025-10-17 05:34:41 +02:00
Marek Lemańczyk 67ad36420a Add GPU kernel for small prime factoring 2025-10-17 05:31:07 +02:00
mlemanczyk f5c79c8807 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 03:51:09 +02:00
mlemanczyk b76ef0b11a Cache capacity initialized for better performance 2025-10-17 03:51:06 +02:00
Marek Lemańczyk 3b759c724e Merge pull request #182 from mlemanczyk/codex/locate-commit-adding-residue-tracking-arrays
Remove small prime stepper remainder cache
2025-10-17 03:50:20 +02:00
Marek Lemańczyk 534141e5a7 Remove small prime stepper remainder cache 2025-10-17 03:50:09 +02:00
Marek Lemańczyk 36a4617efe Merge pull request #181 from mlemanczyk/codex/revert-changes-from-commit-1b03fb3
Restore shared primality cache locking in CPU calculator
2025-10-17 03:30:54 +02:00
Marek Lemańczyk b985a46ed6 Restore cross-thread primality cache 2025-10-17 03:30:41 +02:00
mlemanczyk 56e413064a Using cleanup 2025-10-17 02:40:51 +02:00
mlemanczyk 495b3918c1 Compilation errors fixed 2025-10-17 02:40:15 +02:00
Marek Lemańczyk a0154dd587 Merge pull request #180 from mlemanczyk/codex/refactor-primeordercalculator-to-partial-class
Recycle small prime stepper arrays
2025-10-17 02:34:53 +02:00
Marek Lemańczyk ca0f498d0d Recycle small prime stepper arrays 2025-10-17 02:34:41 +02:00
mlemanczyk 06b08708d9 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 01:24:44 +02:00
Marek Lemańczyk 26894db181 Merge pull request #179 from mlemanczyk/codex/optimize-partialfactor-method-in-primeordercalculator
Hoist Pollard Rho deadline checks outside factoring loop
2025-10-17 01:15:04 +02:00
Marek Lemańczyk b81fe3de75 Hoist Pollard Rho deadline checks outside factoring loop 2025-10-17 01:14:51 +02:00
mlemanczyk d6a1fa0b51 Tiny cleanup 2025-10-17 01:05:13 +02:00
mlemanczyk 0a2e65e190 Small optimizations 2025-10-17 00:29:53 +02:00
mlemanczyk e63fba4df1 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 00:11:47 +02:00
Marek Lemańczyk 14fcd715c1 Merge pull request #178 from mlemanczyk/codex/optimize-primeordercalculator-small-prime-factors
Optimize CPU small prime exponent extraction
2025-10-17 00:11:06 +02:00
mlemanczyk 1b03fb311a Cache switched to thread static for better performance 2025-10-17 00:10:49 +02:00
Marek Lemańczyk 1ce1622388 Optimize CPU small prime exponent extraction 2025-10-17 00:01:43 +02:00
mlemanczyk 140c291344 Small optimizations to limit code branches 2025-10-16 23:37:06 +02:00
Marek Lemańczyk b5f8ed786c Merge pull request #177 from mlemanczyk/codex/naprawic-bad-kompilacji-math.divrem
Replace invalid DivRem usage in prime factor extraction
2025-10-16 22:01:44 +02:00
Marek Lemańczyk ab86fc16fe Fix quotient and remainder calculation in factor extraction 2025-10-16 22:01:34 +02:00
Marek Lemańczyk 806dc96f8c Merge pull request #176 from mlemanczyk/codex/replace-concurrentdictionary-with-dictionary-and-lock
Refine primality caching and small factor loop
2025-10-16 21:54:29 +02:00
Marek Lemańczyk d2e7e4905a Streamline primality cache locking 2025-10-16 21:52:42 +02:00
Marek Lemańczyk 8d9b8b1829 Replace primality cache with lock-backed dictionary 2025-10-16 21:51:42 +02:00
Marek Lemańczyk e8c1069a77 Merge pull request #175 from mlemanczyk/codex/optimize-evenperfectbitscanner-for-reduced-gc
Reduce GC pressure in divisor checking pipeline
2025-10-16 21:33:52 +02:00
mlemanczyk 4c81f9e5a2 IsPrime cache reused 2025-10-16 21:21:40 +02:00
mlemanczyk 10751bf841 Concurrent dictionary added for IsPrime results caching to utilize the repetition of composites. 2025-10-16 21:10:58 +02:00
mlemanczyk aa4ff799f3 Small optimizations 2025-10-16 19:46:25 +02:00
mlemanczyk 07395830a5 Unnecessary cleanup commented out 2025-10-16 18:13:28 +02:00
Marek Lemańczyk e53bb6ba43 Track small-prime remainders with cycle steppers 2025-10-16 17:18:27 +02:00
Marek Lemańczyk fb08a61c66 Restore pooled small factor buffers 2025-10-16 17:18:19 +02:00
Marek Lemańczyk 949b5f891c Return windowed Montgomery odd powers to pooling 2025-10-16 04:52:22 +02:00
Marek Lemańczyk 3e7ee6ab46 Restore pooled buffers in partial factorization 2025-10-16 04:52:17 +02:00
Marek Lemańczyk ff724732f8 Allow test mode to seed by-divisor runs 2025-10-16 03:42:55 +02:00
Marek Lemańczyk 03751c27be Add timed test mode for prime candidate scans 2025-10-16 03:17:27 +02:00
Marek Lemańczyk 9c474fc241 Use stack buffers for small composite collections 2025-10-16 00:59:00 +02:00
Marek Lemańczyk 3e7bffc812 Refactor heuristic pooling to cut GC pressure 2025-10-16 00:37:01 +02:00
Marek Lemańczyk 0a23502441 Clear heuristic candidate list without try-finally 2025-10-15 23:57:46 +02:00
Marek Lemańczyk 51730bcba3 Reduce GC pressure in divisor checking pipeline 2025-10-15 23:43:27 +02:00
Marek Lemańczyk 571b5a9938 Replace divisor scan UInt128 usage with thread-static GpuUInt128 2025-10-15 22:48:36 +02:00
Marek Lemańczyk 9ab87a8b9b Streamline factor cache lease usage in CPU tester 2025-10-15 21:55:30 +02:00
Marek Lemańczyk 64c3721db4 Streamline divisor factor pooling for CPU hot path 2025-10-15 21:50:11 +02:00
Marek Lemańczyk c99aebdf68 Restore divisor cycle fallback and enforce session disposal guard 2025-10-15 21:29:24 +02:00
Marek Lemańczyk 763714307f Eliminate GC allocations from CPU divisor scan hot path 2025-10-15 20:48:42 +02:00
Marek Lemańczyk b0ce7152f7 Pool factor cache entries to keep CPU factoring allocation-free 2025-10-15 20:48:33 +02:00
Marek Lemańczyk 34fcd62c23 Eliminate GC pressure from divisor cycle factoring 2025-10-15 19:32:53 +02:00
Marek Lemańczyk c8f8132db7 Align divisor pooling with review feedback 2025-10-15 19:32:45 +02:00
Marek Lemańczyk e44662413a Use thread-static pools in CPU divisor testing 2025-10-15 18:10:39 +02:00
Marek Lemańczyk a70113a99d Merge pull request #173 from mlemanczyk/codex/analyze-and-reduce-gc-cost-in-primeordercalculator
Reduce Pollard Rho stopwatch allocations to lower GC pressure
2025-10-15 04:39:59 +02:00
Marek Lemańczyk 0b831b5a23 Reduce PartialFactor GC pressure 2025-10-15 04:39:46 +02:00
Marek Lemańczyk 77bc2601f1 Merge pull request #172 from mlemanczyk/codex/add-max-prime-parameter-to-evenperfectbitscanner
Add max prime filter option
2025-10-15 03:31:10 +02:00
Marek Lemańczyk 0055be366d Add max prime filter option 2025-10-15 03:30:57 +02:00
mlemanczyk 2f85636a2a Tiny cleanup 2025-10-15 03:21:38 +02:00
mlemanczyk d3695a3dc3 Pools made thread static. Various small optimizations. "in" parameter added to read-only method parameters 2025-10-15 02:17:13 +02:00
mlemanczyk 7c71d5165e TODO added 2025-10-13 21:16:03 +02:00
mlemanczyk e9373dd8c9 PollardRho extended to allow running with 10_240 threads. Configuration made static to prevent unnecessary GC pressure. 2025-10-13 20:44:15 +02:00
mlemanczyk 313286f70d Configuration adjusted 2025-10-13 20:13:44 +02:00
218 changed files with 22186 additions and 17340 deletions

No files matched your search

+129 -158
View File
@@ -1,186 +1,157 @@
# Repository Guidelines
This repository contains only one solution:
- Only solution: `EvenPerfectScanner.sln` (console app scanning numeric ranges).
- Keep changes confined to the relevant solution/project unless instructed otherwise.
- Assume each solution has or will have its own tests and build configuration; avoid cross-project edits unless required.
- `EvenPerfectScanner.sln` – a console application that scans numeric ranges.
## Build and Test
Changes should be isolated to the relevant solution and project. When working on one
project, avoid modifying the other unless explicitly required. Each solution
has or will have its own test suite and build configuration.
- Build: `dotnet build EvenPerfectScanner.sln`.
- Run only tests related to the modified solution.
- Run tests without `--no-build`.
- Write tests with xUnit + FluentAssertions.
- Confirm test runtimes are reasonable; skip suites expected to run excessively long.
- Always apply timeout guards and `Category=Fast` filters when running tests.
To build `EvenPerfectScanner` run:
## File Editing
```bash
dotnet build EvenPerfectScanner.sln
```
- Do not use `apply_patch.cs` until TODOs lift the ban; edit files with shell tools and stage via git.
- Intended usage (once re-enabled): `dotnet script apply_patch.cs -- <patch-file> [target-directory]`.
- Expect the script to run in local/remote Codex environments on Windows and Linux.
- Supported options (when allowed):
- `--check`
- `--ignore-whitespace`
- `--ignore-eol`
- `--ignore-line-endings`
- `--target <root>`
- `--profile`
- `--replace-line <path> <line> <expected> <new>` (use `_` for expected to skip validation)
- Build patches in standard `git diff` format:
- Include `diff --git`, `---`, `+++`, and `@@` headers with numeric ranges.
- Exclude `***` or `*** Begin/End Patch` wrappers.
- Run `--check` before applying patches; apply only after a successful check.
- Keep patches small, precise, and regenerated after each edit; confirm target lines with `Get-Content` or `nl -ba`.
- Record exact start lines for each hunk and double-check with `nl -ba`.
- Prefer `--replace-line` for single-line edits after capturing the target line number.
- Regenerate diffs after successful patch applications and whenever files shift.
- Use `--ignore-whitespace` / `--ignore-line-endings` only when validation noise blocks `--check`.
- Maintain unique patch filenames and delete applied ones.
- Investigate failed hunks immediately and rebuild diffs instead of retrying stale context.
- Prefer single-purpose patches.
- Use `nl -ba` to show line numbers when needed.
- Use four spaces for code block indentation.
- Create patches from the repo root with relative paths.
- Do not apply changes manually or via other scripts unless modifying `apply_patch.cs`.
- If `apply_patch.cs` has issues or lacks features, fix or enhance it and continue using it.
Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long. When running tests, always provide both an explicit timeout guard and the Category=Fast filter so that only the fast suite executes.
## Test Execution Time Policy
## Making Changes in the Repository
To make any changes to the files in the repository, you should always utilize a script created for it to overcome environment / scripting issues. The script is called `apply_patch.cs` and has the following syntax:
- Estimate runtimes before launching tests or samples.
- Do not run tests expected to exceed 2 minutes unless explicitly authorized.
- Never impose the two-minute guard on benchmarks or profiling runs; let them finish unless obviously stuck.
- Keep timeouts external to the test code.
- Use targeted filters such as `--filter FullyQualifiedName~<ClassOrMethod>` or `--filter "FullyQualifiedName=Namespace.Class.Method"`.
- Keep GPU-heavy tests minimal; skip kernels needing >2 minutes for compile/warmup.
- In local Codex tasks, run only fast, newly added tests and list recommended existing tests instead.
```bash
dotnet script apply_patch.cs -- <patch-file> [target-directory]
```
> **Temporary workflow notice:** Until the pending apply_patch.cs performance and correctness TODOs are complete, do not invoke the script in this repository. Edit files directly with shell tools (sed, tee, cat, python, etc.) and stage the results via git. Re-enable the helper only after the TODO tracker explicitly calls out that the ban has been lifted.
It supports the standard `git diff` patch files as input. You should be able to run it both in the local and remote `Codex` environments. You should expect it to work both in `Windows` and `Linux` environments. Make sure that you include `diff --git` lines, headers and file prefixes in the patch files to avoid applying issues.
`apply_patch.cs` supports several parameters, depending on your needs:
* `--check` - allows checking if the patch will work without making changes to the files
* `--ignore-whitespace`
* `--ignore-eol`
* `--ignore-line-endings`
* `--target` - allows specifying target root directory for the patch
* `--profile` - capture read/parse/apply timings for each patch file.
* `--replace-line <path> <line-number> <expected-text> <new-text>` - replace a single line by absolute number (use `_` as the expected text to skip validation)
* Build patches in standard `git diff` format.
* It supports `---`, `+++`, and `@@`.
* It doesn't support `***` or `*** Begin/End Patch` wrappers.
* It requires `diff --git` at the beginning.
* Patch header needs the actual line numbers.
* When hand-writing a diff, always include the numeric ranges in every `@@ -start,count +start,count @@` header; missing or zero counts will cause `--check` to fail immediately.
* Always check if the patch will work by using `--check` parameter, without making changes to the files, which would force patch changes. Try applying the patch only when `--check` passed.
* Build patches in small, line-precise hunks; confirm target lines with `Get-Content` or `nl -ba` before writing the diff to avoid context mismatches.
* Capture the exact start lines for each hunk while drafting the `@@ -start,length +start,length @@` header; double-check them with `nl -ba` so the first verification succeeds.
* For one-off line edits, prefer the `--replace-line <path> <line-number> <expected-text> <new-text>` option. Pass `_` as the expected text to skip validation when necessary.
* Before calling `--replace-line`, capture the target number with `nl -ba` (or an equivalent command) so the replacement lands exactly once without retrying.
* After every successful patch application, regenerate a fresh diff from the updated files before preparing the next patch so the hunk headers and context reflect the latest line numbers.
* If the patch script flags repeated mismatches, regenerate a fresh diff for the updated file instead of tweaking stale context blocks.
* When `--check` reveals that only whitespace or EOL normalization differs, rerun it with `--ignore-whitespace` and/or `--ignore-line-endings` so validation concentrates on the meaningful edits.
* Regenerate the diff whenever the file shifts—never reuse an old hunk after other edits.
* Keep patch filenames unique and delete applied files to prevent accidental re-use.
* When a hunk fails, inspect the around-lines immediately and adjust rather than retrying the same diff.
* Prefer single-purpose patches (one logical change per file) so rollbacks or fixes stay focused.
* Use `nl -ba` to show line numbers, when needed.
* Always use 4 spaces for indentation of code blocks.
If you want to make any changes, create a patch file and run the script to apply it. Always run it from the root directory and reference any modified files with relative paths respectively in the patch.
Do **not** attempt to apply the changes manually, or with `Python`, or with `PowerShell` scripts, or `cat` or any other way, unless you apply changes to `apply_patch.cs` script itself. If you identify any issues with `apply_patch.cs` script, you should propose fixes to resolve them and continue using the script after updates. If you spot any missing, but required features, propose enhancements and continue using the script after updates, too.
### Test execution time policy
- Always estimate runtime before launching any tests or long-running samples. If a test or ad-hoc run is likely to exceed 2 minutes, do not run it unless the user explicitly requests a longer run and provides justification.
- Never apply the two-minute guard to benchmark or profiling runs; allow them to finish and capture the summary unless the console log shows the job is clearly stuck.
- Do not hardcode timeouts inside test code. Enforce time limits from the runner/shell layer so developers can run tests locally without artificial constraints.
- Prefer targeted filters over full suites to keep runs short: `--filter FullyQualifiedName~<ClassOrMethod>` or `--filter "FullyQualifiedName=Namespace.Class.Method"`.
- Keep GPU-heavy tests minimal and scoped. If a kernel compile or warmup is expected to take >2 minutes, skip running it during regular iterations.
- When working on a task in a local Codex environment, do not run any existing unit tests, except for fast, new, added tests, unless you're instructed otherwise. In the task summary list the names of the recommended unit tests to validate the changes, instead.
#### How to run tests within the 2-minute limit
### 120-second Guard Examples
- Windows (PowerShell):
- Run a single class or test with a 120s guard:
- `$proj = "<path-to>.csproj"`
- `$filter = 'FullyQualifiedName~SomeTests' # or FullyQualifiedName=Namespace.Class.Method`
- `$job = Start-Job -ScriptBlock { param($p,$f) dotnet test $p -c Debug --filter $f } -ArgumentList $proj,$filter`
- `if (-not (Wait-Job $job -Timeout 120)) { Stop-Job $job; throw 'Test run exceeded 120s timeout.' }`
- `Receive-Job $job`
- `$proj = "<path-to>.csproj"`
- `$filter = 'FullyQualifiedName~SomeTests'`
- `$job = Start-Job -ScriptBlock { param($p,$f) dotnet test $p -c Debug --filter $f } -ArgumentList $proj,$filter`
- `if (-not (Wait-Job $job -Timeout 120)) { Stop-Job $job; throw 'Test run exceeded 120s timeout.' }`
- `Receive-Job $job`
- Linux/macOS (bash):
- Use GNU timeout (install coreutils if needed on macOS):
- `timeout 120s dotnet test <path-to>.csproj -c Debug --filter "Category=Fast&FullyQualifiedName~SomeTests"`
- Always specify `timeout 120s` (or a shorter limit when appropriate) and include `Category=Fast` in every filter.
- Note: exit code 124 indicates timeout; handle accordingly in scripts.
- `timeout 120s dotnet test <path-to>.csproj -c Debug --filter "Category=Fast&FullyQualifiedName~SomeTests"`
- Always specify `timeout 120s` (or shorter) and include `Category=Fast` in the filter.
- Treat exit code 124 as a timeout.
- Codex/agent runs: wrap `dotnet test` commands in shell guards above.
- Filter examples: `--filter "FullyQualifiedName~PrimeTesterGcdTests"`, `--filter "FullyQualifiedName=EvenPerfectBitScanner.Tests.ResultsFileNameTests.BuildResultsFileName_contains_the_range"`.
- Codex/agent runs: when invoked as separate steps where inline time control isn’t available, wrap calls through the shell using the guards above rather than calling `dotnet test` directly from the tool.
## Coding Standards
Examples:
- Quick GCD check: `--filter "FullyQualifiedName~PrimeTesterGcdTests"`
- Single test method: `--filter "FullyQualifiedName=EvenPerfectBitScanner.Tests.ResultsFileNameTests.BuildResultsFileName_contains_reduction_and_devices"`
- Use four-space indentation; increase one level per nested block.
- Indent wrapped parameters/expressions by two indentation units.
- Reuse existing variables when prior values are obsolete; call out meaning changes in comments to keep intent clear and register pressure low.
- Avoid try/finally blocks solely for resource release unless omitting finally harms clarity or maintainability; ensure every path releases resources, but consider only successful execution. In case of errors the program must crash.
- Limit the divisor cycle cache to one in-memory block plus the on-disk snapshot; recompute on misses, discard transient results, and avoid background generation or extra caches.
- Write code, comments, commit messages, branch names, and PR descriptions in American English.
- Preserve relevant TODO notes and explanatory comments unless invalid.
- Prefer structs/classes with public fields over auto-properties for hot-path data holders.
- Pass mutable structs as parameters with the `in` modifier whenever the call site and usage allow it (skip when constraints such as lambda captures require a copy).
- Omit the `in` modifier when passing immutable structs as parameters.
- Remove or extend trivial wrapper methods that only call another method.
- Use ThreadStaticPools instead of ArrayPool<T>. Add missing pieces to it, if needed.
Always implement the functionality to achieve the highest possible performance, even if that means allowing for some code duplication. E.g. use List<T> instead of IList<T>, IEnumerable<T> etc.; calculate modulo "%" result in the most efficient way, use aggresive inlining when appropriate and so on. Always take arrays from pools, when it's desired and possible depending on the required array size.
## Performance Checklist
Don't simplify the functionality and / or algorithms to behave differently, unless you're resolving a bug that you've found. Always attempt preservint the functionality, is it's designed in iterations and there is a reason for specific behaviors / implementations.
- Favor dedicated kernels over flag-controlled/parameterized ones; duplicate code when it improves speed.
- Remove `if` statements inside hot loops when possible.
- Replace runtime flags in hot paths with dedicated method variants.
- Reuse variables instead of allocating new ones; document meaning changes.
- Extract functionality into separate classes and place reusable parts in `*.Core` projects.
- Rent arrays from `ArrayPool<T>` when stack allocation is impractical and return them after use.
- Provide cancellation-aware and cancellation-free variants of hot methods; handle cancellation in outer scopes.
- Prefer `for` loops over `foreach` to avoid enumerator allocations.
- Declare loop control variables outside loops and reuse them.
- Cache arrays, static/read-only arrays, and collection counts in locals before loops.
- Declare variables together outside loops when feasible and reuse them.
- Avoid repeated casting within loops by precomputing or using wider variables.
The only .Net SDK available in Codex environment is .Net 8.0. It'll be periodically updated to newer versions as they become available. Specify target framework to the one currently available, but always target all .csproj files against all .Net versions currently supported by MS, using the latest official release. .Net 6.0 is no longer supported, so projects should target only .Net 8.0 and .Net 9.0, with 8+ being the default.
## GPU Notes
Always enter blank line after closing "}" parenthesis, unless there is a consecutive closing "}" bracket. IF's, loops, in practice all code blocks should be put in their "{ (...) }" blocks. Even if there is only a single line in such a code block. Always use the latest officially released language features. All .csproj files should define nullable = enabled and we should always use "?" and/or "!" where appropriate.
- ILGPU 1.5.3 checks: `%` and integer division supported for 32-bit/64-bit integers; `UInt128` remains unsupported inside kernels. Use the `GpuUInt128` struct for GPU-side emulation and extend it whenever required arithmetic helpers are missing.
- Arithmetic in GPU kernels covers only 32-bit and 64-bit primitives; rely on `GpuUInt128` together with its read-only counterpart `ReadOnlyGpuUInt128` to emulate 128-bit behavior.
- CPU code should default to `UInt128` instead of `GpuUInt128` unless the same calculation executes millions or billions of times, in which case prefer mutable `GpuUInt128` when benchmarks show no material regression; benchmark summaries and recommendations live in `Benchmarks.md`.
- GPU kernels should favor mutable `GpuUInt128` and `ReadOnlyGpuUInt128` when the necessary operations exist on the accelerator and benchmarks do not show a dramatic advantage for `UInt128`.
- Perform conversions from `GpuUInt128` or other structs to `ReadOnlyGpuUInt128` outside hot loops whenever possible; push the conversion toward the top of the call chain to avoid repeated costs.
- For each numeric type, review available helpers in `UIntExtensions`, `ULongExtensions`, `UInt128Extensions`, and `GpuUInt128`; add new operations to the extension class or struct that matches the type.
- Arrays/vectors of `bool` unsupported; use `byte`.
- `UInt128` multiplication, division, bit shifts unsupported in kernels; implement manually.
- GPU context pooling supported and recommended.
- Numeric probe results (ILGPU 1.5.3, CPU accelerator):
- `System.Numerics.BigInteger`: unsupported (`InternalCompilerException`).
- `PeterO.Numbers.EInteger`: unsupported (`InternalCompilerException`).
- `PeterO.Numbers.ERational`: unsupported (`InternalCompilerException`, lacks remainder API).
- Prefer XMath in GPU calculations over Math or BitOperations.
Never ever run tests marked with trait Category = Slow. Always invoke `dotnet test` (or equivalent wrappers) with both an explicit timeout and the `Category=Fast` filter; never run tests without specifying these arguments. If there are no categories assigned yet, categorize them before running or skip the execution.
## Patch Preparation Reminders
There is only one agents file - this one. Don't search for another one.
- Manually review every patch before `--check`; remove unintended whitespace noise.
- Capture context with `nl -ba` before composing each hunk; include ≥3 context lines when available.
- Normalize trailing whitespace early or rely on ignore flags temporarily, then rerun without ignores to confirm the final diff.
- Regenerate patches from scratch after working tree changes; avoid editing old diffs.
- Note that the parser ignores blank metadata lines between headers/hunks; keep headers compact unless tools add blanks.
Always use indentation with four space characters in source code files, even if a file currently uses tabs or appears to rely on eight-space indentation. Increase indentation level by exactly one indentation unit (four spaces) per nested block. When parameters or long expressions wrap, indent the continuation with two indentation units for clarity.
Explicitly prefer reusing existing variables once their previous values are no longer required instead of declaring new locals of the same type. Call out the variable reuse in comments when its meaning changes so the intent stays clear and register pressure remains minimal.
## apply_patch.cs Profiling Snapshot (2025-10-04)
Keep the divisor cycle cache limited to a single block in memory (only the on-disk snapshot). When a lookup misses, compute the required cycle on the configured device and discard the transient result instead of scheduling background generation or caching additional blocks.
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
## Performance implementation checklist
- Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary.
- Remove `if` statements inside hot loops whenever possible. Duplicated code in additional kernels or methods is acceptable when it improves speed.
- Replace runtime flags in hot paths with dedicated method variants to eliminate conditional branches.
- Reuse existing variables of the same type instead of allocating new ones if some become unused. Add comments whenever a variable's meaning changes.
- Extract functionality into separate classes instead of growing existing classes. Place reusable components into `*.Core` projects.
- Rent arrays from `ArrayPool<T>` when stack allocation is impractical or less efficient, and return them after use.
- Provide cancellation-aware and cancellation-free variants of hot methods so cancellation tokens are not checked in every loop iteration; handle cancellation in outer scopes.
- Prefer `for` loops over `foreach` to reduce enumerator allocations.
- Declare loop control variables outside the loop and reuse them across loops when possible.
- Cache static or read-only arrays in local variables before looping to avoid repeated class field lookups.
- Store collection `Count` or array `Length` in a local variable before the loop and use the cached value.
- Declare variables together outside loops when feasible and reuse them instead of creating new ones.
- Avoid repeated casting within loops by precomputing or using wider variables when necessary.
## GPU-specific notes
The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerators:
- [x] `%` operator (kernels): supported for 32-bit and 64-bit integer types; not supported for `UInt128` inside kernels.
- [x] Integer division (kernels): supported for 32-bit and 64-bit types; not supported for `UInt128` inside kernels.
- [x] Arrays or vectors of `bool`: unsupported (`NotSupportedException`); use `byte` instead.
- [x] `UInt128` multiplication: unsupported in kernels (`AggregateException/NullReferenceException`); manual implementation required.
- [x] `UInt128` division: unsupported in kernels; manual implementation required.
- [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code.
- [x] GPU context pooling: supported and recommended (validated in a separate program).
Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1.5.3 on the CPU accelerator):
- `System.Numerics.BigInteger`: unsupported. Kernels that exercise addition, subtraction, multiplication, division, modulo, and `CompareTo` all fail with `InternalCompilerException`, indicating fundamental codegen gaps for the type.
- `PeterO.Numbers.EInteger`: unsupported. Attempts to compile kernels that call addition, subtraction, multiplication, division, remainder, and `CompareTo` throw `InternalCompilerException`, so none of the arithmetic helpers are currently usable on the device.
- `PeterO.Numbers.ERational`: unsupported. Kernels that call addition, subtraction, multiplication, division, and `CompareTo` trigger `InternalCompilerException`; the type also lacks a remainder/modulo API for device usage.
Patch-preparation checklist for reliable `--check` runs:
- Manually review every patch file before running `--check`; confirm the hunks reflect only the intended edits and eliminate accidental whitespace-only noise ahead of time.
- Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try.
- Ensure every hunk carries at least three unchanged context lines above and below the edits whenever the surrounding file provides them; regenerate the diff if the file changed since the previous attempt so the context stays accurate.
- Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly.
- Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches.
- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers and skips stray blank separators between hunks, so you no longer need to insert dummy empty lines to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally.
### apply_patch.cs profiling snapshot (2025-10-04)
- Latest timing check: `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`.
- Patch characteristics: 2 files, 4 hunks, 31 lines, 3468 bytes.
- `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`.
- Sample patch: 2 files, 4 hunks, 31 lines, 3468 bytes.
- Timings: read 0.45 ms, parse 2.96 ms, apply 2.96 ms.
- Focus next on streaming the patch reader and pooled buffer reuse to reduce the parse APPLY bottleneck highlighted above.
- Next focus: stream the patch reader and reuse pooled buffers to reduce parse/apply cost.
Mersenne divisor size estimates:
## Mersenne Divisor Guidance
- Any prime divisor `q` of a Mersenne number `2^p - 1` must satisfy `q ≡ 1 (mod 2p)`. The minimal `q` therefore appears at `q = 2p + 1` when the multiplier `k = 1` yields a prime.
- For candidate exponents `p` in the `[138,000,000, 200,000,000]` range, the minimal compliant divisor lands below `2 · 200,000,000 + 1 = 400,000,001`, which fits in 29 bits (`2^29 = 536,870,912`).
- Extending the search up to `p = 600,000,000` pushes the minimal divisor to `1,200,000,001`, requiring 31 bits (`2^31 = 2,147,483,648`).
- Larger `k` values (and thus larger `q`) are possible—up to nearly `2^p - 1`—so the heuristics and storage should plan for wider integers when deep trial factoring is necessary, but the noted bit widths cover the first admissible divisors for these exponent bands.
- Prime divisor `q` of `2^p - 1` satisfies `q ≡ 1 (mod 2p)` with minimal `q = 2p + 1` for `k = 1`.
- Target for at least `p` in `[138,000,000, 200,000,000]`, minimal `q < 400,000,001` (29 bits).
- Target for at least `p = 600,000,000`, minimal `q ≤ 1,200,000,001` (31 bits).
- Larger `k` values can grow `q` up to nearly `2^p - 1`; plan heuristics/storage for wider integers even though the noted bit widths cover the first admissible divisors.
- Worst-case divisor width: `p - 1` bits (`⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`).
- `k_max ≈ 2^{p-2} / p` for large `p`.
- Because every proper divisor of `2^p - 1` is at most `(2^p - 1) / 2`, the worst-case bit-width for any admissible `q` is `p - 1` bits (since `⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`). Consequently we need roughly `199,999,999` bits when `p ≤ 200,000,000` and `599,999,999` bits when `p ≤ 600,000,000`. In terms of the `q = 2kp + 1` form, this translates to `k_max = ⌊((2^p - 1) / 2 - 1) / (2p)⌋`, which is approximately `2^{p-2} / p` for large `p`.
## Practical Kernel Guidance
Practical guidance:
- Use native `%` and `/` in kernels when working with 32-bit or 64-bit integers if it is the most efficient option for the target backend.
- For `UInt128` in kernels, avoid `%`/`/`; implement reductions via Montgomery/Barrett, or use Mersenne-specific folding for mod `2^p-1`.
- Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops.
- Use native `%` and `/` for 32-bit/64-bit integers when efficient.
- For `UInt128` in kernels, avoid `%`/`/`; prefer Montgomery/Barrett or Mersenne folding.
- Prefer Mersenne folding for `2^p - 1` moduli to remove branches in hot loops.
- Heuristic multiply-shift benchmarks (2025-10-04):
- Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `ULongExtensions` helpers without the BenchmarkDotNet harness.
- Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic.
- `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns.
- `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns.
- `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns.
- `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns.
- The shift-first rewrite roughly doubles the per-call cost on CPU, but it stays within 64-bit arithmetic and is the preferred fallback for GPU heuristics when avoiding `UInt128` overflow is mandatory.
## Heuristic Multiply-Shift Benchmarks (2025-10-04)
- Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare `ULongExtensions` helpers without the BenchmarkDotNet harness.
- Average ns per call over 200,000,000 iterations (identical checksums):
- `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive ≈ 0.85 ns.
- `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns.
- `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns.
- `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns.
- Shift-first doubles CPU cost but stays within 64-bit arithmetic and is the GPU fallback when avoiding `UInt128` overflow.
+8 -8
View File
@@ -23,7 +23,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
|------------------------------------------- |------------------ |-----------:|----------:|----------:|------:|--------:|----------:|------------:|
| ExtensionBaseline | CrossWordBlend | 3.606 ns | 0.0030 ns | 0.0025 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | CrossWordBlend | 22.870 ns | 0.1463 ns | 0.1297 ns | 6.34 | 0.04 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | CrossWordBlend | 22.780 ns | 0.4463 ns | 0.8600 ns | 6.32 | 0.23 | - | NA |
| MulModGpu | CrossWordBlend | 22.780 ns | 0.4463 ns | 0.8600 ns | 6.32 | 0.23 | - | NA |
| InlineUInt128Operands | CrossWordBlend | 22.218 ns | 0.0319 ns | 0.0266 ns | 6.16 | 0.01 | - | NA |
| InlineUInt128OperandsWithReductionFirst | CrossWordBlend | 22.626 ns | 0.0553 ns | 0.0517 ns | 6.27 | 0.02 | - | NA |
| InlineUInt128OperandsWithOperandReduction | CrossWordBlend | 22.548 ns | 0.0487 ns | 0.0432 ns | 6.25 | 0.01 | - | NA |
@@ -35,7 +35,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
| | | | | | | | | |
| ExtensionBaseline | MixedBitPattern | 3.564 ns | 0.0059 ns | 0.0052 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | MixedBitPattern | 64.800 ns | 0.3399 ns | 0.3179 ns | 18.17 | 0.09 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | MixedBitPattern | 59.165 ns | 0.4243 ns | 0.3969 ns | 16.60 | 0.11 | - | NA |
| MulModGpu | MixedBitPattern | 59.165 ns | 0.4243 ns | 0.3969 ns | 16.60 | 0.11 | - | NA |
| InlineUInt128Operands | MixedBitPattern | 30.539 ns | 0.1396 ns | 0.1306 ns | 8.57 | 0.03 | - | NA |
| InlineUInt128OperandsWithReductionFirst | MixedBitPattern | 31.237 ns | 0.1013 ns | 0.0898 ns | 8.76 | 0.03 | - | NA |
| InlineUInt128OperandsWithOperandReduction | MixedBitPattern | 30.210 ns | 0.1323 ns | 0.1237 ns | 8.48 | 0.04 | - | NA |
@@ -47,7 +47,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
| | | | | | | | | |
| ExtensionBaseline | NearFullRange | 3.583 ns | 0.0105 ns | 0.0098 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | NearFullRange | 5.708 ns | 0.0520 ns | 0.0486 ns | 1.59 | 0.02 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | NearFullRange | 5.952 ns | 0.1253 ns | 0.1172 ns | 1.66 | 0.04 | - | NA |
| MulModGpu | NearFullRange | 5.952 ns | 0.1253 ns | 0.1172 ns | 1.66 | 0.04 | - | NA |
| InlineUInt128Operands | NearFullRange | 27.159 ns | 0.0936 ns | 0.0781 ns | 7.58 | 0.03 | - | NA |
| InlineUInt128OperandsWithReductionFirst | NearFullRange | 4.357 ns | 0.0205 ns | 0.0182 ns | 1.22 | 0.00 | - | NA |
| InlineUInt128OperandsWithOperandReduction | NearFullRange | 4.319 ns | 0.0158 ns | 0.0140 ns | 1.21 | 0.00 | - | NA |
@@ -59,7 +59,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
| | | | | | | | | |
| ExtensionBaseline | PrimeSizedModulus | 3.593 ns | 0.0039 ns | 0.0037 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | PrimeSizedModulus | 52.766 ns | 0.3086 ns | 0.2887 ns | 14.69 | 0.08 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | PrimeSizedModulus | 50.490 ns | 0.1977 ns | 0.1752 ns | 14.05 | 0.05 | - | NA |
| MulModGpu | PrimeSizedModulus | 50.490 ns | 0.1977 ns | 0.1752 ns | 14.05 | 0.05 | - | NA |
| InlineUInt128Operands | PrimeSizedModulus | 31.409 ns | 0.0984 ns | 0.0920 ns | 8.74 | 0.03 | - | NA |
| InlineUInt128OperandsWithReductionFirst | PrimeSizedModulus | 32.176 ns | 0.3427 ns | 0.3206 ns | 8.96 | 0.09 | - | NA |
| InlineUInt128OperandsWithOperandReduction | PrimeSizedModulus | 31.786 ns | 0.1483 ns | 0.1387 ns | 8.85 | 0.04 | - | NA |
@@ -71,7 +71,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
| | | | | | | | | |
| ExtensionBaseline | SparseOperands | 3.555 ns | 0.0087 ns | 0.0082 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | SparseOperands | 5.671 ns | 0.0043 ns | 0.0036 ns | 1.60 | 0.00 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | SparseOperands | 5.239 ns | 0.0103 ns | 0.0096 ns | 1.47 | 0.00 | - | NA |
| MulModGpu | SparseOperands | 5.239 ns | 0.0103 ns | 0.0096 ns | 1.47 | 0.00 | - | NA |
| InlineUInt128Operands | SparseOperands | 3.563 ns | 0.0217 ns | 0.0203 ns | 1.00 | 0.01 | - | NA |
| InlineUInt128OperandsWithReductionFirst | SparseOperands | 4.617 ns | 0.0095 ns | 0.0089 ns | 1.30 | 0.00 | - | NA |
| InlineUInt128OperandsWithOperandReduction | SparseOperands | 4.282 ns | 0.0193 ns | 0.0181 ns | 1.20 | 0.01 | - | NA |
@@ -83,7 +83,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
| | | | | | | | | |
| ExtensionBaseline | ZeroOperands | 3.599 ns | 0.0069 ns | 0.0065 ns | 1.00 | 0.00 | - | NA |
| GpuCompatibleMulModExtension | ZeroOperands | 1.976 ns | 0.0053 ns | 0.0049 ns | 0.55 | 0.00 | - | NA |
| GpuCompatibleMulModSimplifiedExtension | ZeroOperands | 1.986 ns | 0.0048 ns | 0.0045 ns | 0.55 | 0.00 | - | NA |
| MulModGpu | ZeroOperands | 1.986 ns | 0.0048 ns | 0.0045 ns | 0.55 | 0.00 | - | NA |
| InlineUInt128Operands | ZeroOperands | 3.542 ns | 0.0125 ns | 0.0117 ns | 0.98 | 0.00 | - | NA |
| InlineUInt128OperandsWithReductionFirst | ZeroOperands | 4.286 ns | 0.0178 ns | 0.0167 ns | 1.19 | 0.01 | - | NA |
| InlineUInt128OperandsWithOperandReduction | ZeroOperands | 4.256 ns | 0.0116 ns | 0.0097 ns | 1.18 | 0.00 | - | NA |
@@ -97,7 +97,7 @@ Job=.NET 8.0 Runtime=.NET 8.0
Outliers
MulMod64Benchmarks.ExtensionBaseline: .NET 8.0 -> 2 outliers were removed (4.92 ns, 5.01 ns)
MulMod64Benchmarks.GpuCompatibleMulModExtension: .NET 8.0 -> 1 outlier was removed, 2 outliers were detected (23.75 ns, 25.37 ns)
MulMod64Benchmarks.GpuCompatibleMulModSimplifiedExtension: .NET 8.0 -> 2 outliers were removed, 3 outliers were detected (22.07 ns, 26.53 ns, 26.65 ns)
MulMod64Benchmarks.MulModGpu: .NET 8.0 -> 2 outliers were removed, 3 outliers were detected (22.07 ns, 26.53 ns, 26.65 ns)
MulMod64Benchmarks.InlineUInt128Operands: .NET 8.0 -> 2 outliers were removed (23.62 ns, 23.64 ns)
MulMod64Benchmarks.InlineUInt128OperandsWithOperandReduction: .NET 8.0 -> 1 outlier was removed (24.42 ns)
MulMod64Benchmarks.MultiplyHighDecomposition: .NET 8.0 -> 2 outliers were removed (25.12 ns, 25.31 ns)
@@ -112,7 +112,7 @@ Outliers
MulMod64Benchmarks.InlineUInt128Operands: .NET 8.0 -> 2 outliers were removed (29.01 ns, 29.49 ns)
MulMod64Benchmarks.InlineUInt128OperandsWithReductionFirst: .NET 8.0 -> 1 outlier was removed (5.72 ns)
MulMod64Benchmarks.InlineUInt128OperandsWithOperandReduction: .NET 8.0 -> 1 outlier was removed (5.73 ns)
MulMod64Benchmarks.GpuCompatibleMulModSimplifiedExtension: .NET 8.0 -> 1 outlier was removed, 3 outliers were detected (51.41 ns, 51.47 ns, 52.10 ns)
MulMod64Benchmarks.MulModGpu: .NET 8.0 -> 1 outlier was removed, 3 outliers were detected (51.41 ns, 51.47 ns, 52.10 ns)
MulMod64Benchmarks.InlineUInt128Operands: .NET 8.0 -> 1 outlier was detected (32.44 ns)
MulMod64Benchmarks.InlineUInt128WithLocals: .NET 8.0 -> 2 outliers were removed (33.47 ns, 34.46 ns)
MulMod64Benchmarks.MultiplyHighDecomposition: .NET 8.0 -> 1 outlier was removed (33.93 ns)
@@ -15,7 +15,4 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -1,3 +1,4 @@
using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -9,12 +10,10 @@ namespace EvenPerfectBitScanner.Benchmarks;
public class GpuModularArithmeticBenchmarks
{
private const int MaxBatchSize = 512;
private readonly ulong[] _exponents = new ulong[MaxBatchSize];
private readonly ulong[] _moduli = new ulong[MaxBatchSize];
private readonly ulong[] _mersenneModuli = new ulong[MaxBatchSize];
private readonly int[] _mersenneBitWidths = new int[MaxBatchSize];
private readonly Random _random = new(7);
private ulong[] _exponents = Array.Empty<ulong>();
private ulong[] _moduli = Array.Empty<ulong>();
private ulong[] _mersenneModuli = Array.Empty<ulong>();
private int[] _mersenneBitWidths = Array.Empty<int>();
[Params(32, 256)]
public int BatchSize { get; set; }
@@ -22,14 +21,75 @@ public class GpuModularArithmeticBenchmarks
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < MaxBatchSize; i++)
{
_exponents[i] = NextExponent();
_moduli[i] = NextOddModulus();
ModularArithmeticSampleData data = ModularArithmeticSampleCache.Instance;
_exponents = data.Exponents;
_moduli = data.Moduli;
_mersenneModuli = data.MersenneModuli;
_mersenneBitWidths = data.MersenneBitWidths;
}
int bits = _random.Next(8, 62);
_mersenneBitWidths[i] = bits;
_mersenneModuli[i] = (1UL << bits) - 1UL;
private sealed class ModularArithmeticSampleData
{
public ModularArithmeticSampleData(ulong[] exponents, ulong[] moduli, ulong[] mersenneModuli, int[] mersenneBitWidths)
{
Exponents = exponents;
Moduli = moduli;
MersenneModuli = mersenneModuli;
MersenneBitWidths = mersenneBitWidths;
}
public ulong[] Exponents { get; }
public ulong[] Moduli { get; }
public ulong[] MersenneModuli { get; }
public int[] MersenneBitWidths { get; }
}
private static class ModularArithmeticSampleCache
{
private static readonly Lazy<ModularArithmeticSampleData> Cache = new(Create);
public static ModularArithmeticSampleData Instance => Cache.Value;
private static ModularArithmeticSampleData Create()
{
Console.WriteLine("Preparing modular arithmetic benchmark sample data...");
Random random = new(7);
var exponents = new ulong[MaxBatchSize];
var moduli = new ulong[MaxBatchSize];
var mersenneModuli = new ulong[MaxBatchSize];
var mersenneBitWidths = new int[MaxBatchSize];
for (int i = 0; i < MaxBatchSize; i++)
{
exponents[i] = NextExponent(random);
moduli[i] = NextOddModulus(random);
int bits = random.Next(8, 62);
mersenneBitWidths[i] = bits;
mersenneModuli[i] = (1UL << bits) - 1UL;
}
ModularArithmeticSampleData data = new ModularArithmeticSampleData(exponents, moduli, mersenneModuli, mersenneBitWidths);
Console.WriteLine("Finished preparing modular arithmetic benchmark sample data.");
return data;
}
private static ulong NextExponent(Random random)
{
return (ulong)random.Next(24, 63);
}
private static ulong NextOddModulus(Random random)
{
ulong value = (ulong)random.NextInt64(5L, long.MaxValue);
if ((value & 1UL) == 0UL)
{
value++;
}
return value;
}
}
@@ -69,22 +129,6 @@ public class GpuModularArithmeticBenchmarks
return checksum;
}
private ulong NextExponent()
{
return (ulong)_random.Next(24, 63);
}
private ulong NextOddModulus()
{
ulong value = (ulong)_random.NextInt64(5L, long.MaxValue);
if ((value & 1UL) == 0UL)
{
value++;
}
return value;
}
private static ulong Pow2ModBaseline(ulong exponent, ulong modulus)
{
if (modulus <= 1UL)
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -34,7 +32,7 @@ public class GpuUInt128Montgomery64Benchmarks
[Benchmark(Baseline = true)]
public ulong ExtensionMontgomeryMultiply()
{
return Input.Left.MontgomeryMultiply(Input.Right, Input.Modulus, Input.NPrime);
return Input.Left.MontgomeryMultiplyCpu(Input.Right, Input.Modulus, Input.NPrime);
}
/// <summary>
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -34,7 +33,7 @@ public class GpuUInt128Mul64Benchmarks
public GpuUInt128 HighProductMaterializedInLocal()
{
GpuUInt128 value = Input.Multiplicand;
value.Mul64(Input.Multiplier);
GpuUInt128.Mul64(ref value, Input.Multiplier.High, Input.Multiplier.Low);
return value;
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -10,7 +10,7 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class GpuUInt128MulModBenchmarks
{
private static readonly MulModInput[] Inputs =
private static readonly GpuMulModCase[] GeneralCases =
[
new(
new GpuUInt128(0UL, 5UL),
@@ -34,10 +34,28 @@ public class GpuUInt128MulModBenchmarks
"MixedMagnitude"),
];
[ParamsSource(nameof(GetInputs))]
public MulModInput Input { get; set; }
private static readonly GpuMulModCase[] ByLimbCases =
[
new(
new GpuUInt128(0UL, 17UL),
new GpuUInt128(0UL, 19UL),
new GpuUInt128(0x0000_0000_0000_0001UL, 0x0000_0000_0000_00FFUL),
"TinyOperands"),
new(
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
new GpuUInt128(0x0F1E_2D3C_4B5A_6978UL, 0x8776_6554_4332_2110UL),
new GpuUInt128(0x0FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFC3UL),
"MixedMagnitude"),
new(
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0x8000_0000_0000_0000UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
"HighWordDominant"),
];
public static IEnumerable<MulModInput> GetInputs() => Inputs;
public static IEnumerable<GpuMulModCase> GetGeneralCases() => GeneralCases;
public static IEnumerable<GpuMulModCase> GetByLimbCases() => ByLimbCases;
/// <summary>
/// In-place multiply-modulo; great for low-word-heavy operands (6.88 ns) and tiny pairs (7.15 ns) but notably slower on
@@ -47,11 +65,11 @@ public class GpuUInt128MulModBenchmarks
/// Observed means: HighWordModulus 192.794 ns (1.00×), LowWordHeavy 6.883 ns, MixedMagnitude 201.468 ns, TinyOperands 7.146 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 InPlaceMulMod()
[ArgumentsSource(nameof(GetGeneralCases))]
public GpuUInt128 InPlaceMulMod(GpuMulModCase input)
{
// TODO: Switch the extension method to allocate per iteration, as it's faster. Keep the benchmarks.
GpuUInt128 value = Input.Left;
value.MulMod(Input.Right, Input.Modulus);
GpuUInt128 value = input.Left;
value.MulMod(input.Right, input.Modulus);
return value;
}
@@ -63,9 +81,48 @@ public class GpuUInt128MulModBenchmarks
/// Observed means: HighWordModulus 184.322 ns (0.96×), LowWordHeavy 116.993 ns, MixedMagnitude 138.684 ns, TinyOperands 4.659 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 AllocatePerIteration()
[ArgumentsSource(nameof(GetGeneralCases))]
public GpuUInt128 AllocatePerIteration(GpuMulModCase input)
{
return MulModInline(Input.Left, Input.Right, Input.Modulus);
return MulModInline(input.Left, input.Right, input.Modulus);
}
/// <summary>
/// BigInteger-based reference reduction kept for validation; remains several microseconds slower than the GPU-compatible implementations.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetGeneralCases))]
public GpuUInt128 BigIntegerReference(GpuMulModCase input)
{
return MulModBigInteger(input.Left, input.Right, input.Modulus);
}
/// <summary>
/// Legacy implementation that allocates intermediate limbs; delivered 10.7 ns on tiny operands, 12.7 ns on high-word-dominant
/// pairs, and 16.3 ns on mixed magnitude, making it the faster choice overall.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 12.69 ns (1.00×), MixedMagnitude 16.26 ns, TinyOperands 10.74 ns.
/// </remarks>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetByLimbCases))]
public GpuUInt128 LegacyAllocating(GpuMulModCase input)
{
return MulModByLimbLegacy(input.Left, input.Right, input.Modulus);
}
/// <summary>
/// In-place reduction variant; avoids allocations but costs 12.9–25.1 ns, trailing the legacy path by 20–55% depending on the
/// operand mix.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 17.88 ns (1.41×), MixedMagnitude 25.14 ns, TinyOperands 12.92 ns.
/// </remarks>
[Benchmark]
[ArgumentsSource(nameof(GetByLimbCases))]
public GpuUInt128 InPlaceReduction(GpuMulModCase input)
{
return MulModByLimbInPlace(input.Left, input.Right, input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -108,7 +165,136 @@ public class GpuUInt128MulModBenchmarks
return result;
}
public readonly record struct MulModInput(GpuUInt128 Left, GpuUInt128 Right, GpuUInt128 Modulus, string Name)
private static GpuUInt128 MulModBigInteger(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
BigInteger leftBig = (UInt128)left;
BigInteger rightBig = (UInt128)right;
BigInteger modulusBig = (UInt128)modulus;
if (modulusBig.IsZero)
{
return GpuUInt128.Zero;
}
UInt128 reduced = (UInt128)((leftBig * rightBig) % modulusBig);
return new GpuUInt128((ulong)(reduced >> 64), (ulong)reduced);
}
private static GpuUInt128 MulModByLimbInPlace(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
var (p3, p2, p1, p0) = MultiplyFullLegacy(left, right);
GpuUInt128 remainder = new(p3, p2);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
ulong limb = p1;
for (int i = 0; i < 2; i++)
{
remainder.ShiftLeft(64);
remainder.Low = limb;
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
limb = p0;
}
return remainder;
}
private static GpuUInt128 MulModByLimbLegacy(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
var (p3, p2, p1, p0) = MultiplyFullLegacy(left, right);
GpuUInt128 remainder = new(p3, p2);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
ulong limb = p1;
for (int i = 0; i < 2; i++)
{
remainder <<= 64;
remainder = new GpuUInt128(remainder.High, limb);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
limb = p0;
}
return remainder;
}
private static (ulong P3, ulong P2, ulong P1, ulong P0) MultiplyFullLegacy(GpuUInt128 left, GpuUInt128 right)
{
var (h0, l0) = Mul64Legacy(left.Low, right.Low);
var (h1, l1) = Mul64Legacy(left.Low, right.High);
var (h2, l2) = Mul64Legacy(left.High, right.Low);
var (h3, l3) = Mul64Legacy(left.High, right.High);
ulong carry = 0UL;
ulong sum0 = l0;
ulong sum1 = h0;
sum1 += l1;
if (sum1 < l1)
{
carry++;
}
sum1 += l2;
if (sum1 < l2)
{
carry++;
}
ulong sum2 = h1 + h2;
ulong carry2 = sum2 < h2 ? 1UL : 0UL;
sum2 += l3;
if (sum2 < l3)
{
carry2++;
}
sum2 += carry;
if (sum2 < carry)
{
carry2++;
}
ulong p1 = sum1;
ulong p2 = sum2;
ulong p3 = h3 + carry2;
return (p3, p2, p1, sum0);
}
private static (ulong High, ulong Low) Mul64Legacy(ulong left, ulong right)
{
ulong a0 = (uint)left;
ulong a1 = left >> 32;
ulong b0 = (uint)right;
ulong b1 = right >> 32;
ulong lo = a0 * b0;
ulong mid1 = a1 * b0;
b0 = a0 * b1;
b1 *= a1;
a0 = (lo >> 32) + (uint)mid1 + (uint)b0;
a1 = (lo & 0xFFFF_FFFFUL) | (a0 << 32);
b1 += (mid1 >> 32) + (b0 >> 32) + (a0 >> 32);
return (b1, a1);
}
public readonly record struct GpuMulModCase(GpuUInt128 Left, GpuUInt128 Right, GpuUInt128 Modulus, string Name)
{
public override string ToString()
{
@@ -1,159 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128MulModByLimbBenchmarks
{
private static readonly MulModByLimbInput[] Inputs = new MulModByLimbInput[]
{
new(
new GpuUInt128(0UL, 17UL),
new GpuUInt128(0UL, 19UL),
new GpuUInt128(0x0000_0000_0000_0001UL, 0x0000_0000_0000_00FFUL),
"TinyOperands"),
new(
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
new GpuUInt128(0x0F1E_2D3C_4B5A_6978UL, 0x8776_6554_4332_2110UL),
new GpuUInt128(0x0FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFC3UL),
"MixedMagnitude"),
new(
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0x8000_0000_0000_0000UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
"HighWordDominant"),
};
[ParamsSource(nameof(GetInputs))]
public MulModByLimbInput Input { get; set; }
public static IEnumerable<MulModByLimbInput> GetInputs() => Inputs;
/// <summary>
/// Legacy implementation that allocates intermediate limbs; delivered 10.7 ns on tiny operands, 12.7 ns on high-word-dominant
/// pairs, and 16.3 ns on mixed magnitude, making it the faster choice overall.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 12.69 ns (1.00×), MixedMagnitude 16.26 ns, TinyOperands 10.74 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 LegacyAllocating()
{
return MulModByLimbLegacy(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// In-place reduction variant; avoids allocations but costs 12.9–25.1 ns, trailing the legacy path by 20–55% depending on the
/// operand mix.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 17.88 ns (1.41×), MixedMagnitude 25.14 ns, TinyOperands 12.92 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 InPlaceReduction()
{
// TODO: Switch the extension method back to legacy method, as it's faster. Keep the benchmarks.
GpuUInt128 value = Input.Left;
value.MulModByLimb(Input.Right, Input.Modulus);
return value;
}
private static GpuUInt128 MulModByLimbLegacy(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
var (p3, p2, p1, p0) = MultiplyFullLegacy(left, right);
GpuUInt128 remainder = new(p3, p2);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
ulong limb = p1;
for (int i = 0; i < 2; i++)
{
remainder <<= 64;
remainder = new GpuUInt128(remainder.High, limb);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
limb = p0;
}
return remainder;
}
private static (ulong P3, ulong P2, ulong P1, ulong P0) MultiplyFullLegacy(GpuUInt128 left, GpuUInt128 right)
{
var (h0, l0) = Mul64Legacy(left.Low, right.Low);
var (h1, l1) = Mul64Legacy(left.Low, right.High);
var (h2, l2) = Mul64Legacy(left.High, right.Low);
var (h3, l3) = Mul64Legacy(left.High, right.High);
ulong carry = 0UL;
ulong sum0 = l0;
ulong sum1 = h0;
sum1 += l1;
if (sum1 < l1)
{
carry++;
}
sum1 += l2;
if (sum1 < l2)
{
carry++;
}
ulong sum2 = h1 + h2;
ulong carry2 = sum2 < h2 ? 1UL : 0UL;
sum2 += l3;
if (sum2 < l3)
{
carry2++;
}
sum2 += carry;
if (sum2 < carry)
{
carry2++;
}
ulong p1 = sum1;
ulong p2 = sum2;
ulong p3 = h3 + carry2;
return (p3, p2, p1, sum0);
}
private static (ulong High, ulong Low) Mul64Legacy(ulong left, ulong right)
{
ulong a0 = (uint)left;
ulong a1 = left >> 32;
ulong b0 = (uint)right;
ulong b1 = right >> 32;
ulong lo = a0 * b0;
ulong mid1 = a1 * b0;
b0 = a0 * b1;
b1 *= a1;
a0 = (lo >> 32) + (uint)mid1 + (uint)b0;
a1 = (lo & 0xFFFF_FFFFUL) | (a0 << 32);
b1 += (mid1 >> 32) + (b0 >> 32) + (a0 >> 32);
return (b1, a1);
}
public readonly record struct MulModByLimbInput(GpuUInt128 Left, GpuUInt128 Right, GpuUInt128 Modulus, string Name)
{
public override string ToString() => Name;
}
}
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -184,10 +184,11 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_Current(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
@@ -209,10 +210,11 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_DoWhile(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
@@ -237,10 +239,11 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_DoubleSubtract(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
@@ -266,10 +269,11 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UInt128Modulo(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
@@ -288,22 +292,23 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledPair(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
@@ -313,32 +318,33 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledQuad(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
@@ -348,52 +354,53 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledOct(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
@@ -403,101 +410,101 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledHex(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
// EvenPerfectBitScanner never routes power-of-two divisors here; keep the guard disabled outside tests and benchmarks.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// return 1UL;
// }
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool Step(ref ulong pow, ulong divisor, ref ulong order)
private static bool AdvanceMersenneDivisorCycleStep(ref ulong pow, ulong divisor, ref ulong order)
{
pow += pow;
if (pow >= divisor)
@@ -508,4 +515,5 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
order++;
return pow == 1UL;
}
}
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod10Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 10U, "UInt32:10"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt128, 3UL, "UInt128:3"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
];
private static readonly ModBenchmarkCase[] NonUInt128Cases =
[
new(ModNumericType.UInt32, 10U, "UInt32:10"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
];
private static readonly ModBenchmarkCase[] UInt128Cases =
[
new(ModNumericType.UInt128, 3UL, "UInt128:3"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
];
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
public static IEnumerable<ModBenchmarkCase> GetNonUInt128Cases() => NonUInt128Cases;
public static IEnumerable<ModBenchmarkCase> GetUInt128Cases() => UInt128Cases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Mod10(uint value) => value - (uint)((value * UIntExtensions.Mod5Mask) >> 35) * 10U;
private static ulong ModWithHighLowModuloHelper(UInt128 value128)
{
ulong value = (ulong)value128;
return ((value % 10UL) + ((value >> 64) % 10UL) * 6UL) % 10UL;
}
private static ulong ModWithLoopModuloHelper(UInt128 value)
{
UInt128 zero = UInt128.Zero;
if (value == zero)
{
return 0UL;
}
ulong result = (ulong)value;
value >>= 64;
while (value != zero)
{
result = (result + (ulong)value * 6UL) % 10UL;
value >>= 64;
}
return result % 10UL;
}
/// <summary>
/// `% 10` baseline; ranged from 0.04-0.32 ns on <c>uint</c>, ~0.26 ns on <c>ulong</c>, and 3.25-3.38 ns on
/// <see cref="UInt128"/> inputs across the consolidated dataset.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 10U,
ModNumericType.UInt64 => (ulong)data.Value % 10UL,
ModNumericType.UInt128 => (ulong)(data.Value % 10UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Mask-based helpers for <c>uint</c> and <c>ulong</c>; they trail the raw `%` on 64-bit inputs and only match it on the
/// smaller width.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetNonUInt128Cases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => Mod10((uint)data.Value),
ModNumericType.UInt64 => ((ulong)data.Value).Mod10(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// High/low folding helper for <see cref="UInt128"/>; measured 1.78-1.82 ns in the original suite, about 1.9× faster than `%`.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetUInt128Cases))]
public ulong ModWithHighLowModulo(ModBenchmarkCase data)
{
return ModWithHighLowModuloHelper(data.Value);
}
/// <summary>
/// Loop-based folding helper for <see cref="UInt128"/>; lands between the high/low helper and the raw modulo at 2.20-2.27 ns.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetUInt128Cases))]
public ulong ModWithLoopModulo(ModBenchmarkCase data)
{
return ModWithLoopModuloHelper(data.Value);
}
}
@@ -1,66 +0,0 @@
using BenchmarkDotNet.Attributes;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10UInt128Benchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 10` using the <see cref="UInt128"/> operator; lands between 3.25 ns and 3.38 ns (3.378 ns at value 3, 3.269 ns at
/// 8191, 3.248 ns at 131071, 3.281 ns at 2147483647) and serves as the baseline.
/// </summary>
[Benchmark(Baseline = true)]
public UInt128 StandardModulo()
{
return Value % 10UL;
}
/// <summary>
/// Splits the value into high/low halves and folds with the 2^64 ≡ 6 (mod 10) rule; fastest option at 1.78-1.82 ns (1.804 ns at
/// value 3, 1.798 ns at 8191, 1.817 ns at 131071, 1.779 ns at 2147483647), beating the baseline by ~1.9x.
/// </summary>
[Benchmark]
public ulong ModWithHighLowModulo()
{
return ModWithHighLowModuloHelper(Value);
}
private static ulong ModWithHighLowModuloHelper(UInt128 value128)
{
ulong value = (ulong)value128;
return ((value % 10UL) + ((value >> 64) % 10UL) * 6UL) % 10UL;
}
/// <summary>
/// Loop-based folding that repeatedly applies the 2^64 ≡ 6 rule; measures 2.20-2.27 ns (2.204 ns at value 3, 2.234 ns at 8191,
/// 2.268 ns at 131071, 2.216 ns at 2147483647), situating it between the high/low helper and the raw modulo.
/// </summary>
[Benchmark]
public ulong ModWithLoopModulo()
{
return ModWithLoopModuloHelper(Value);
}
private static ulong ModWithLoopModuloHelper(UInt128 value)
{
UInt128 zero = UInt128.Zero;
if (value == zero)
{
return 0UL;
}
ulong result = (ulong)value;
value >>= 64;
while (value != zero)
{
result = (result + (ulong)value * 6UL) % 10UL;
value >>= 64;
}
return result % 10UL;
}
}
@@ -1,38 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod10UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod10(uint value) => value - (uint)((value * UIntExtensions.Mod5Mask) >> 35) * 10U;
[Params(10U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// Direct `% 10` on <see cref="uint"/>; BenchmarkDotNet flagged near-zero timings, but the measured means were 0.0407 ns at
/// value 10, 0.3224 ns at 2047, 0.2382 ns at 65535, and 0.0555 ns at 2147483647.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 10U;
}
/// <summary>
/// Optimized extension method that folds with a precomputed mask; still costs 0.77 ns on the tiny value 10 but drops to
/// 0.0448 ns at 2047, 0.0405 ns at 65535, and 0.0372 ns at 2147483647 (roughly 6-9x faster where the baseline exceeded the
/// empty-loop noise floor).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod10(Value);
}
}
@@ -1,31 +0,0 @@
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10ULongBenchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Scalar `% 10` baseline; consistently measured around 0.26 ns (0.2591 ns at value 3, 0.2597 ns at 8191, 0.2605 ns at 131071,
/// 0.2598 ns at 2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public ulong LegacyModulo()
{
return Value % 10UL;
}
/// <summary>
/// Extension helper that applies the multiply-high mask; 0.73-0.75 ns across the suite (0.7503 ns at value 3, 0.7320 ns at
/// 8191, 0.7346 ns at 131071, 0.7411 ns at 2147483647), roughly 2.8-2.9x slower than the raw modulo on `ulong`.
/// </summary>
[Benchmark]
public ulong ModMethodModulo()
{
return Value.Mod10();
}
}
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -6,33 +9,56 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10_8_5_3Benchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
public ulong Mod10R;
public ulong Mod8R;
public ulong Mod3R;
public ulong Mod5R;
private static readonly ulong[] ValueCases =
[
3UL,
6UL,
128UL,
256UL,
2047UL,
8191UL,
65535UL,
131071UL,
2147483647UL,
ulong.MaxValue - 1024UL,
ulong.MaxValue - 511UL,
ulong.MaxValue - 255UL,
ulong.MaxValue - 127UL,
ulong.MaxValue - 63UL,
ulong.MaxValue - 31UL,
ulong.MaxValue - 17UL,
];
public static IEnumerable<ulong> GetValues() => ValueCases;
/// <summary>
/// Baseline scalar modulo path; held steady between 1.13 ns and 1.14 ns across every value (1.138 ns at 3, 1.143 ns at 8191,
/// 1.134 ns at 131071, 1.141 ns at 2147483647), making it the fastest option overall.
/// Baseline scalar modulo path; previously 1.13-1.14 ns across the shared dataset and remains the reference point here.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModuloOperator()
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModuloOperator(ulong value)
{
ulong value = Value;
return (value % 10UL, value % 8UL, value % 3UL, value % 5UL);
}
/// <summary>
/// Legacy folding helper; useful for cross-checks but stays at 2.83-3.07 ns (2.834 ns at 3, 3.038 ns at 8191, 3.067 ns at
/// 131071, 3.039 ns at 2147483647), roughly 2.5-2.7x slower than the baseline.
/// Legacy folding helper; 2.83-3.07 ns in the original suite, now exercised on the combined dataset.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo()
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo(ulong value)
{
ulong mod10;
ulong mod8;
ulong mod3;
ulong mod5;
UInt128 zero = UInt128.Zero;
if (Value == zero)
if (value == 0UL)
{
mod3 = 0UL;
mod5 = 0UL;
@@ -41,8 +67,8 @@ public class Mod10_8_5_3Benchmarks
return (mod10, mod8, mod3, mod5);
}
UInt128 high = Value >> 64;
ulong result = Value;
UInt128 high = value >> 64;
ulong result = value;
mod8 = result & 7UL;
ulong modRem = (result % 3UL) + ((ulong)high % 3UL);
@@ -62,13 +88,86 @@ public class Mod10_8_5_3Benchmarks
}
/// <summary>
/// Method-based optimized helper; clocks in at 1.63 ns for value 3, 1.64 ns at 8191, 1.875 ns at 131071, and 2.164 ns at
/// 2147483647 (1.43-1.90x the baseline) so it trails the raw `%` but remains 1.6x quicker than the legacy fold.
/// Method-based optimized helper; 1.63-2.16 ns in the original benchmarks, roughly 1.4-1.9× slower than the raw modulo.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo(ulong value)
{
Value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
return (mod10, mod8, mod3, mod5);
}
/// <summary>
/// Legacy property-based helper; 2.35-2.46 ns at small values and up to 2.46 ns near <c>ulong.Max</c>.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod5, ulong Mod3) LegacyPropertyModulo(ulong value)
{
UInt128 wideValue = value;
Mod10R = wideValue.Mod10();
Mod8R = Mod8(wideValue);
Mod5R = Mod5(wideValue);
Mod3R = Mod3(wideValue);
return (Mod10R, Mod8R, Mod5R, Mod3R);
}
/// <summary>
/// Method variant for the property helper; 2.79-4.34 ns in the original run, about 1.2-1.8× slower than the legacy property path.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodPropertyModulo(ulong value)
{
value.Mod10_8_5_3(out Mod10R, out Mod8R, out Mod5R, out Mod3R);
return (Mod10R, Mod8R, Mod3R, Mod5R);
}
/// <summary>
/// Legacy step-per-bit helper; 11.9-13.4 ns in the original suite and remains the slowest option.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyStepsModulo(ulong value)
{
UInt128 wideValue = value;
ulong mod10 = (((ulong)(wideValue % 10UL)) << 1) % 10UL;
ulong mod8 = (((ulong)(wideValue & 7UL)) << 1) & 7UL;
ulong mod5 = (((ulong)(wideValue % 5UL)) << 1) % 5UL;
ulong mod3 = (((ulong)(wideValue % 3UL)) << 1) % 3UL;
return (mod10, mod8, mod3, mod5);
}
/// <summary>
/// Step helper exposed via <see cref="UInt128Extensions.Mod10_8_5_3Steps"/>; 3.58-3.77 ns previously, delivering 3.2-3.6× speedups over the legacy step path.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetValues))]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodStepsModulo(ulong value)
{
value.Mod10_8_5_3Steps(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
return (mod10, mod8, mod3, mod5);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(UInt128 value) => (ulong)value & 7UL;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 3UL) + (high % 3UL);
return rem >= 3UL ? rem - 3UL : rem;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod5(UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 5UL) + (high % 5UL);
return rem >= 5UL ? rem - 5UL : rem;
}
}
@@ -1,81 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10_8_5_3PropertiesBenchmarks
{
public ulong Mod10R;
public ulong Mod8R;
public ulong Mod3R;
public ulong Mod5R;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(UInt128 value) => (ulong)value & 7UL;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 3UL) + (high % 3UL);
return rem >= 3UL ? rem - 3UL : rem;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod5(UInt128 value)
{
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 5UL) + (high % 5UL);
return rem >= 5UL ? rem - 5UL : rem;
}
[Params(
3UL,
6UL,
128UL,
256UL,
2047UL,
8191UL,
65535UL,
131071UL,
2147483647UL,
ulong.MaxValue - 1024UL,
ulong.MaxValue - 511UL,
ulong.MaxValue - 255UL,
ulong.MaxValue - 127UL,
ulong.MaxValue - 63UL,
ulong.MaxValue - 31UL,
ulong.MaxValue - 17UL)]
public ulong Value { get; set; }
/// <summary>
/// Legacy property-based helper; stayed within 2.35-2.46 ns on the small inputs and 2.36-2.46 ns across the near-ulong.Max
/// cases (e.g. 2.413 ns at value 3, 2.378 ns at 6, 2.458 ns at 2147483647, 2.371 ns near ulong.Max), keeping it in front.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod5, ulong Mod3) LegacyModulo()
{
UInt128 value = Value;
Mod10R = value.Mod10();
Mod8R = Mod8(value);
Mod5R = Mod5(value);
Mod3R = Mod3(value);
return (Mod10R, Mod8R, Mod5R, Mod3R);
}
/// <summary>
/// Method variant that routes through <see cref="UInt128Extensions.Mod10_8_5_3"/>; ranged from 2.79 ns at value 3 to 4.34 ns on
/// the largest operands (3.135 ns at 6, 3.033 ns at 2047, 3.441 ns at 2147483647, 4.289 ns near ulong.Max), roughly 1.16-1.83x
/// slower than the legacy path.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
{
Value.Mod10_8_5_3(out Mod10R, out Mod8R, out Mod5R, out Mod3R);
return (Mod10R, Mod8R, Mod3R, Mod5R);
}
}
@@ -1,37 +0,0 @@
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10_8_5_3StepsBenchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Legacy step-per-bit helper; required 11.89-13.40 ns per call (12.052 ns at value 3, 11.891 ns at 8191, 13.401 ns at 131071,
/// 12.114 ns at 2147483647), making it 3-4x slower than the streamlined method variant.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo()
{
UInt128 value = Value;
ulong mod10 = (((ulong)(value % 10UL)) << 1) % 10UL;
ulong mod8 = (((ulong)(value & 7UL)) << 1) & 7UL;
ulong mod5 = (((ulong)(value % 5UL)) << 1) % 5UL;
ulong mod3 = (((ulong)(value % 3UL)) << 1) % 3UL;
return (mod10, mod8, mod3, mod5);
}
/// <summary>
/// Optimized step helper exposed on <see cref="UInt128Extensions.Mod10_8_5_3Steps"/>; trimmed execution to 3.58-3.77 ns
/// (3.576 ns at value 3, 3.682 ns at 8191, 3.773 ns at 131071, 3.649 ns at 2147483647), giving a 3.2-3.6x speedup.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
{
Value.Mod10_8_5_3Steps(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
return (mod10, mod8, mod3, mod5);
}
}
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod11Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 11U, "UInt32:11"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 11UL, "UInt64:11"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 127UL, "UInt64:Max-127"),
new(ModNumericType.UInt128, 11UL, "UInt128:11"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 255UL, "UInt128:Max-255"),
];
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Mod11(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 1023U;
value >>= 10;
remainder -= 11U * (remainder / 11U);
}
return remainder;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod11(ulong value)
{
ulong remainder = ((uint)value % 11U) + (((uint)(value >> 32) % 11U) << 2);
while (remainder >= 11UL)
{
remainder -= 11UL;
}
return remainder;
}
/// <summary>
/// `% 11` baseline across the consolidated dataset; near-empty-loop costs on <c>uint</c>, 0.23-0.27 ns on <c>ulong</c>, and
/// 2.85-2.87 ns on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 11U,
ModNumericType.UInt64 => (ulong)data.Value % 11UL,
ModNumericType.UInt128 => (ulong)(data.Value % 11UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Looping helpers; slower than `%` on <c>uint</c> and <c>ulong</c> but ~4.2× faster on <see cref="UInt128"/> thanks to digit
/// folding.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => Mod11((uint)data.Value),
ModNumericType.UInt64 => Mod11((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod11(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod11UInt128Benchmarks
{
[Params(11UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 11` for <see cref="UInt128"/>; ran in ~2.85-2.87 ns on every dataset (2.8719 ns at value 11, 2.8715 ns at 8191,
/// 2.8534 ns at 131071, 2.8716 ns at 2147483647, 2.8715 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 11UL);
}
/// <summary>
/// Extension helper that folds digits with lookup coefficients; 0.66-0.68 ns across the inputs (0.6721 ns at value 11, 0.6765 ns
/// at 8191, 0.6760 ns at 131071, 0.6614 ns at 2147483647, 0.6661 ns near ulong.Max), about 4.2x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod11();
}
}
@@ -1,47 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod11UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod11(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 1023U;
value >>= 10;
remainder -= 11U * (remainder / 11U);
}
return remainder;
}
[Params(11U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 11` baseline; essentially free at this scale with means of 0.0299 ns (value 11), 0.0333 ns (2047), 0.0393 ns (65535), and
/// 0.0367 ns (2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 11U;
}
/// <summary>
/// Looping extension method; useful for validation but slower across the board at 0.286 ns (value 11), 0.957 ns (2047),
/// 0.922 ns (65535), and 3.170 ns (2147483647), i.e. 9-86x the direct modulo.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod11(Value);
}
}
@@ -1,46 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod11ULongBenchmarks
{
[Params(11UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 11` baseline for `ulong`; timings hovered near 0.26-0.27 ns on the large inputs (0.2647 ns at 2147483647, 0.2709 ns near
/// ulong.Max) with the smaller samples in the same 0.23-0.27 ns band.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 11UL;
}
/// <summary>
/// Lookup-based helper mirroring `UIntExtensions.Mod11`; costs 0.66-0.75 ns on 32-bit scale inputs and rises to 1.15 ns near
/// ulong.Max, remaining 2.5-4.3x slower than the `%` baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod11(Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod11(ulong value)
{
ulong remainder = ((uint)value % 11U) + ((uint)(value >> 32) % 11U) << 2;
while (remainder >= 11UL)
{
remainder -= 11UL;
}
return remainder;
}
}
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod128Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 3U, "UInt32:3"),
new(ModNumericType.UInt32, 5U, "UInt32:5"),
new(ModNumericType.UInt32, 6U, "UInt32:6"),
new(ModNumericType.UInt32, 8U, "UInt32:8"),
new(ModNumericType.UInt32, 10U, "UInt32:10"),
new(ModNumericType.UInt32, 11U, "UInt32:11"),
new(ModNumericType.UInt32, 32U, "UInt32:32"),
new(ModNumericType.UInt32, 64U, "UInt32:64"),
new(ModNumericType.UInt32, 128U, "UInt32:128"),
new(ModNumericType.UInt32, 256U, "UInt32:256"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 8191U, "UInt32:8191"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 131071U, "UInt32:131071"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt32, uint.MaxValue - 1024U, "UInt32:Max-1024"),
new(ModNumericType.UInt32, uint.MaxValue - 511U, "UInt32:Max-511"),
new(ModNumericType.UInt32, uint.MaxValue - 255U, "UInt32:Max-255"),
new(ModNumericType.UInt32, uint.MaxValue - 127U, "UInt32:Max-127"),
new(ModNumericType.UInt32, uint.MaxValue - 63U, "UInt32:Max-63"),
new(ModNumericType.UInt32, uint.MaxValue - 31U, "UInt32:Max-31"),
new(ModNumericType.UInt32, uint.MaxValue - 17U, "UInt32:Max-17"),
new(ModNumericType.UInt32, uint.MaxValue, "UInt32:Max"),
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 5UL, "UInt64:5"),
new(ModNumericType.UInt64, 6UL, "UInt64:6"),
new(ModNumericType.UInt64, 8UL, "UInt64:8"),
new(ModNumericType.UInt64, 10UL, "UInt64:10"),
new(ModNumericType.UInt64, 11UL, "UInt64:11"),
new(ModNumericType.UInt64, 32UL, "UInt64:32"),
new(ModNumericType.UInt64, 64UL, "UInt64:64"),
new(ModNumericType.UInt64, 128UL, "UInt64:128"),
new(ModNumericType.UInt64, 256UL, "UInt64:256"),
new(ModNumericType.UInt64, 2047UL, "UInt64:2047"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 65535UL, "UInt64:65535"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, 4294966271UL, "UInt64:4294966271"),
new(ModNumericType.UInt64, 4294966784UL, "UInt64:4294966784"),
new(ModNumericType.UInt64, 4294967040UL, "UInt64:4294967040"),
new(ModNumericType.UInt64, 4294967168UL, "UInt64:4294967168"),
new(ModNumericType.UInt64, 4294967232UL, "UInt64:4294967232"),
new(ModNumericType.UInt64, 4294967264UL, "UInt64:4294967264"),
new(ModNumericType.UInt64, 4294967278UL, "UInt64:4294967278"),
new(ModNumericType.UInt64, 4294967295UL, "UInt64:4294967295"),
new(ModNumericType.UInt128, 128UL, "UInt128:128"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 511UL, "UInt128:Max-511"),
];
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Mod128(uint value) => value & 127U;
/// <summary>
/// `% 128` baseline; sits at 0.01-0.03 ns on <c>uint</c> and <c>ulong</c>, and 2.95-3.42 ns on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 128U,
ModNumericType.UInt64 => (ulong)data.Value % 128UL,
ModNumericType.UInt128 => (ulong)(data.Value % 128UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Bitmask helpers; usually match `%` on 32-bit inputs, are faster on <c>ulong</c> with the exception of the 4,294,966,784
/// outlier, and provide 150-1000× wins on <see cref="UInt128"/>.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => Mod128((uint)data.Value),
ModNumericType.UInt64 => ((ulong)data.Value).Mod128(),
ModNumericType.UInt128 => data.Value.Mod128(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod128UInt128Benchmarks
{
[Params(128UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 511UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 128` baseline for <see cref="UInt128"/>; measured 2.95-3.42 ns across the suite (3.423 ns at value 128, 3.025 ns at
/// 8191, 2.950 ns at 131071, 2.948 ns at 2147483647, 2.967 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 128UL);
}
/// <summary>
/// Bitmask helper (`value & 127UL`); durations were indistinguishable from the empty-loop cost at 0.004-0.025 ns—roughly
/// 150-1000x faster than the modulo baseline on every sample.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod128();
}
}
@@ -1,59 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod128UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod128(uint value) => value & 127U;
[Params(
3U,
5U,
6U,
8U,
10U,
11U,
32U,
64U,
128U,
256U,
2047U,
8191U,
65535U,
131071U,
2147483647U,
uint.MaxValue - 1024U,
uint.MaxValue - 511U,
uint.MaxValue - 255U,
uint.MaxValue - 127U,
uint.MaxValue - 63U,
uint.MaxValue - 31U,
uint.MaxValue - 17U,
uint.MaxValue)]
public uint Value { get; set; }
/// <summary>
/// `% 128` baseline; BenchmarkDotNet reported near-empty-loop costs with means from 0.010-0.026 ns across the domain (e.g.
/// 0.0191 ns at value 3, 0.0136 ns at 5, 0.0170 ns at 32, 0.0108 ns at 256, 0.0138 ns at 2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 128U;
}
/// <summary>
/// Bitmask helper that returns `value & 127`; generally matches the baseline at 0.006-0.023 ns (0.0163 ns at value 3, 0.0064 ns at
/// 6, 0.0152 ns at 10, 0.0156 ns at 2147483647) but regresses on 64 due to the benchmark harness (0.9496 ns, 67x slower).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod128(Value);
}
}
@@ -1,56 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod128ULongBenchmarks
{
[Params(
3UL,
5UL,
6UL,
8UL,
10UL,
11UL,
32UL,
64UL,
128UL,
256UL,
2047UL,
8191UL,
65535UL,
131071UL,
2147483647UL,
4294966271UL,
4294966784UL,
4294967040UL,
4294967168UL,
4294967232UL,
4294967264UL,
4294967278UL,
4294967295UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 128` baseline; timings stayed below 0.03 ns on every dataset (0.0227 ns at value 3, 0.0108 ns at 256, 0.0251 ns at 2047,
/// 0.0190 ns at 2147483647, 0.0165 ns at 4294966271).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 128UL;
}
/// <summary>
/// Bitmask helper (`value & 127UL`); typically 0.006-0.015 ns (0.0113 ns at value 3, 0.0094 ns at 64, 0.0109 ns at 2147483647),
/// but spikes to 0.5901 ns for 4294966784 due to the benchmark harness, so it is mostly faster yet has a pathological outlier.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod128();
}
}
@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod3Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 3U, "UInt32:3"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 17UL, "UInt64:Max-17"),
new(ModNumericType.UInt128, 3UL, "UInt128:3"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 17UL, "UInt128:Max-17"),
];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 3UL;
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
/// <summary>
/// `% 3` baseline across all widths; previously landed at 0.03-0.09 ns on <c>uint</c>, 0.26-0.49 ns on <c>ulong</c>,
/// and 2.88-2.97 ns on <see cref="UInt128"/> inputs throughout the merged dataset.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 3U,
ModNumericType.UInt64 => (ulong)data.Value % 3UL,
ModNumericType.UInt128 => (ulong)(data.Value % 3UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Extension helpers that fold operands by width; they matched the modulo noise floor on <c>uint</c>, trailed by
/// ~1.5-1.7× on <c>ulong</c>, and delivered a 6.4× speedup on <see cref="UInt128"/>.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => UIntExtensions.Mod3((uint)data.Value),
ModNumericType.UInt64 => Mod3((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod3(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod3UInt128Benchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 17UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 3` on <see cref="UInt128"/>; measured 2.88-2.97 ns (2.969 ns at value 3, 2.887 ns at 8191, 2.885 ns at 131071,
/// 2.884 ns at 2147483647, 2.933 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 3UL);
}
/// <summary>
/// Extension helper folding the high and low halves; trimmed timings to 0.455-0.475 ns (0.4549 ns at value 3, 0.4562 ns at
/// 8191, 0.4656 ns at 131071, 0.4710 ns at 2147483647, 0.4750 ns near ulong.Max), about 6.4x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod3();
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod3UIntBenchmarks
{
[Params(3U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 3` baseline; the near-zero timings (0.0320 ns at value 3, 0.0918 ns at 2047, 0.0355 ns at 65535, 0.0309 ns at
/// 2147483647) reflect the noise floor flagged by BenchmarkDotNet.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 3U;
}
/// <summary>
/// Extension helper that folds digits; produced 0.0217 ns at value 3, 0.0303 ns at 2047, 0.0597 ns at 65535, and 0.0350 ns at
/// 2147483647, tracking the baseline within measurement noise.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return UIntExtensions.Mod3(Value);
}
}
@@ -1,41 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod3ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 3UL;
[Params(3UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 17UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 3` baseline; delivered 0.259-0.286 ns for the standard values (0.2640 ns at 3, 0.2599 ns at 8191, 0.2620 ns at 131071,
/// 0.2858 ns at 2147483647) and 0.4916 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 3UL;
}
/// <summary>
/// Byte-slice helper; measured 0.441 ns at value 3, 0.437 ns at 8191, 0.438 ns at 131071, 0.436 ns at 2147483647, and 0.448 ns
/// near ulong.Max—about 1.5-1.7x slower than the `%` baseline but still sub-nanosecond.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod3(Value);
}
}
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod5Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 3U, "UInt32:3"),
new(ModNumericType.UInt32, 5U, "UInt32:5"),
new(ModNumericType.UInt32, 6U, "UInt32:6"),
new(ModNumericType.UInt32, 8U, "UInt32:8"),
new(ModNumericType.UInt32, 10U, "UInt32:10"),
new(ModNumericType.UInt32, 11U, "UInt32:11"),
new(ModNumericType.UInt32, 32U, "UInt32:32"),
new(ModNumericType.UInt32, 64U, "UInt32:64"),
new(ModNumericType.UInt32, 128U, "UInt32:128"),
new(ModNumericType.UInt32, 256U, "UInt32:256"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 8191U, "UInt32:8191"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 131071U, "UInt32:131071"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt32, uint.MaxValue - 1024U, "UInt32:Max-1024"),
new(ModNumericType.UInt32, uint.MaxValue - 511U, "UInt32:Max-511"),
new(ModNumericType.UInt32, uint.MaxValue - 255U, "UInt32:Max-255"),
new(ModNumericType.UInt32, uint.MaxValue - 127U, "UInt32:Max-127"),
new(ModNumericType.UInt32, uint.MaxValue - 63U, "UInt32:Max-63"),
new(ModNumericType.UInt32, uint.MaxValue - 31U, "UInt32:Max-31"),
new(ModNumericType.UInt32, uint.MaxValue - 17U, "UInt32:Max-17"),
new(ModNumericType.UInt32, uint.MaxValue, "UInt32:Max"),
new(ModNumericType.UInt64, 5UL, "UInt64:5"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 31UL, "UInt64:Max-31"),
new(ModNumericType.UInt128, 5UL, "UInt128:5"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 31UL, "UInt128:Max-31"),
];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod5(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 5UL;
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
/// <summary>
/// `% 5` baseline; maintained 0.03-0.12 ns on <c>uint</c>, 0.26-0.27 ns on <c>ulong</c>, and 2.84-2.93 ns on
/// <see cref="UInt128"/> values across the unified dataset.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 5U,
ModNumericType.UInt64 => (ulong)data.Value % 5UL,
ModNumericType.UInt128 => (ulong)(data.Value % 5UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Extension helpers; effectively tied with modulo on <c>uint</c>, about 1.6-1.7× slower on <c>ulong</c>, and roughly 6×
/// faster than `%` on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => ((uint)data.Value).Mod5(),
ModNumericType.UInt64 => Mod5((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod5(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod5UInt128Benchmarks
{
[Params(5UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 31UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 5` baseline; ran at 2.84-2.93 ns (2.840 ns at value 5, 2.874 ns at 8191, 2.870 ns at 131071, 2.895 ns at 2147483647,
/// 2.927 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 5UL);
}
/// <summary>
/// Extension helper that folds high and low halves; trimmed execution to 0.47-0.49 ns (0.4706 ns at value 5, 0.4691 ns at
/// 8191, 0.4644 ns at 131071, 0.4847 ns at 2147483647, 0.4669 ns near ulong.Max), about 6x faster.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod5();
}
}
@@ -1,56 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod5UIntBenchmarks
{
[Params(
3U,
5U,
6U,
8U,
10U,
11U,
32U,
64U,
128U,
256U,
2047U,
8191U,
65535U,
131071U,
2147483647U,
uint.MaxValue - 1024U,
uint.MaxValue - 511U,
uint.MaxValue - 255U,
uint.MaxValue - 127U,
uint.MaxValue - 63U,
uint.MaxValue - 31U,
uint.MaxValue - 17U,
uint.MaxValue)]
public uint Value { get; set; }
/// <summary>
/// `% 5` baseline; times hovered around 0.03 ns with the expected zero-measurement noise (0.0296 ns at value 5, 0.0671 ns at 6,
/// 0.0326 ns at 8, 0.0289 ns at 10, 0.1236 ns at 11, dropping back to ~0.03 ns for large values).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 5U;
}
/// <summary>
/// Extension helper; typically within 0.03-0.04 ns (0.0349 ns at value 3, 0.0294 ns at 8, 0.0305 ns at 65535, 0.0270 ns at 131071,
/// 0.0327 ns at 2147483647) though the dataset contains an outlier at value 11 (0.0315 ns) and value 64 (0.0356 ns).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Value.Mod5();
}
}
@@ -1,41 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod5ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod5(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 5UL;
[Params(5UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 31UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 5` baseline; measured 0.2657 ns at value 5, 0.2622 ns at 8191, 0.2640 ns at 131071, 0.2645 ns at 2147483647, and 0.2696 ns
/// near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 5UL;
}
/// <summary>
/// Extension helper; slower at 0.431-0.448 ns (0.4305 ns at value 5, 0.4404 ns at 8191, 0.4359 ns at 131071, 0.4361 ns at
/// 2147483647, 0.4481 ns near ulong.Max), roughly 1.6-1.7x the `%` cost.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod5(Value);
}
}
@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
[MemoryDiagnoser]
public class Mod6Benchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 6U, "UInt32:6"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 1024UL, "UInt64:Max-1024"),
new(ModNumericType.UInt128, 6UL, "UInt128:6"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 63UL, "UInt128:Max-63"),
];
private static readonly ModBenchmarkCase[] UInt64Cases =
[
new(ModNumericType.UInt64, 3UL, "UInt64:3"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 1024UL, "UInt64:Max-1024"),
];
private static readonly Mod6LoopCase[] LoopCases =
[
new(3UL, "Loop:3"),
new(131071UL, "Loop:131071"),
new(ulong.MaxValue - 1024UL, "Loop:Max-1024"),
];
private const int Iterations = 10_000_000;
private const ulong Divisor = 6UL;
private static readonly ulong FastDivMul = (ulong)(((UInt128)1 << 64) / Divisor);
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
public static IEnumerable<ModBenchmarkCase> GetUInt64Cases() => UInt64Cases;
public static IEnumerable<Mod6LoopCase> GetLoopCases() => LoopCases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Mod6(uint value) => Mod6Lookup[(int)(((value % 3U) << 1) | (value & 1U))];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod6(ulong value) => Mod6Lookup[(int)(((value % 3UL) << 1) | (value & 1UL))];
/// <summary>
/// `% 6` baseline across the merged scalar dataset; previously timed at 0.03-0.05 ns on <c>uint</c>, 0.26-0.27 ns on
/// <c>ulong</c>, and 2.87-2.91 ns on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 6U,
ModNumericType.UInt64 => (ulong)data.Value % 6UL,
ModNumericType.UInt128 => (ulong)(data.Value % 6UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Division-based scalar helpers where available; matches the modulo cost on <c>ulong</c> inputs.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetUInt64Cases))]
public ulong DivisionBased(ModBenchmarkCase data)
{
ulong value = (ulong)data.Value;
ulong quotient = value / Divisor;
return value - quotient * Divisor;
}
/// <summary>
/// FastDivHigh scalar helper for <c>ulong</c>; remains 1.0-1.6× slower than the `%` baseline across the dataset.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetUInt64Cases))]
public ulong FastDivHigh(ModBenchmarkCase data)
{
ulong value = (ulong)data.Value;
ulong quotient = value.FastDiv64(Divisor, FastDivMul);
return value - quotient * Divisor;
}
/// <summary>
/// Lookup helpers; significantly slower on <c>uint</c> and <c>ulong</c> but 2.4-2.5× faster on <see cref="UInt128"/>.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => Mod6((uint)data.Value),
ModNumericType.UInt64 => Mod6((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod6(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// `% 6` baseline inside the 10,000,000-iteration loop; measured 1.02-1.04 ns depending on the starting value.
/// </summary>
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
[ArgumentsSource(nameof(GetLoopCases))]
public ulong ModuloOperatorLoop(Mod6LoopCase data)
{
ulong checksum = 0UL;
ulong value = data.StartValue;
for (int i = 0; i < Iterations; i++)
{
checksum += value % Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// Division-based loop helper; mirrors the modulo timings with small deviations across the dataset.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
[ArgumentsSource(nameof(GetLoopCases))]
public ulong DivisionBasedLoop(Mod6LoopCase data)
{
ulong checksum = 0UL;
ulong value = data.StartValue;
for (int i = 0; i < Iterations; i++)
{
ulong quotient = value / Divisor;
checksum += value - quotient * Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// FastDivHigh loop helper; remained ~1.6× slower than the modulo baseline across the looped dataset.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
[ArgumentsSource(nameof(GetLoopCases))]
public ulong FastDivHighLoop(Mod6LoopCase data)
{
ulong checksum = 0UL;
ulong value = data.StartValue;
for (int i = 0; i < Iterations; i++)
{
ulong quotient = value.FastDiv64(Divisor, FastDivMul);
checksum += value - quotient * Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// Lookup-based loop helper; effectively matched the modulo baseline while removing the `%` instruction.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
[ArgumentsSource(nameof(GetLoopCases))]
public ulong ExtensionMethodLoop(Mod6LoopCase data)
{
ulong checksum = 0UL;
ulong value = data.StartValue;
for (int i = 0; i < Iterations; i++)
{
checksum += Mod6(value);
value += (ulong)((i & 15) + 1);
}
return checksum;
}
public readonly struct Mod6LoopCase
{
public Mod6LoopCase(ulong startValue, string display)
{
StartValue = startValue;
Display = display;
}
public ulong StartValue { get; }
private string Display { get; }
public override string ToString()
{
return Display;
}
}
}
@@ -1,100 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
[MemoryDiagnoser]
public class Mod6ComparisonBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod6(ulong value) => Mod6Lookup[(int)(((value % 3UL) << 1) | (value & 1UL))];
private const int Iterations = 10_000_000;
private const ulong Divisor = 6UL;
private static readonly ulong FastDivMul = (ulong)(((UInt128)1 << 64) / Divisor);
[Params(3UL, 131071UL, ulong.MaxValue - 1024UL)]
public ulong StartValue { get; set; }
/// <summary>
/// `% 6` baseline inside the loop; measured 1.02 ns at StartValue 3, 1.035 ns at 131071, and 1.011 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
public ulong ModuloOperator()
{
ulong checksum = 0UL;
ulong value = StartValue;
for (int i = 0; i < Iterations; i++)
{
checksum += value % Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// `value / 6` with a multiply back; varied between 0.962 ns and 1.063 ns (0.9915 ns at StartValue 3, 1.0633 ns at 131071,
/// 0.9617 ns near ulong.Max), roughly matching the baseline.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong DivisionBased()
{
ulong checksum = 0UL;
ulong value = StartValue;
for (int i = 0; i < Iterations; i++)
{
ulong quotient = value / Divisor;
checksum += value - quotient * Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// FastDivHigh helper; landed at 1.67-1.71 ns (1.691 ns at StartValue 3, 1.706 ns at 131071, 1.673 ns near ulong.Max), making it
/// ~1.65x slower than `%`.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong FastDivHigh()
{
ulong checksum = 0UL;
ulong value = StartValue;
for (int i = 0; i < Iterations; i++)
{
ulong quotient = value.FastDiv64(Divisor, FastDivMul);
checksum += value - quotient * Divisor;
value += (ulong)((i & 15) + 1);
}
return checksum;
}
/// <summary>
/// Lookup-based helper argued for adoption; ran in 1.06-1.07 ns (1.0605 ns at StartValue 3, 1.0696 ns at 131071, 1.0604 ns near
/// ulong.Max), essentially matching the baseline with slightly higher stability.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong ExtensionMethod()
{
ulong checksum = 0UL;
ulong value = StartValue;
for (int i = 0; i < Iterations; i++)
{
checksum += Mod6(value);
value += (ulong)((i & 15) + 1);
}
return checksum;
}
}
@@ -1,32 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod6UInt128Benchmarks
{
[Params(6UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 63UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 6` baseline; recorded 2.89-2.91 ns on the smaller values and 2.874 ns at 2147483647, 2.908 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 6UL);
}
/// <summary>
/// Extension helper (<see cref="UInt128Extensions.Mod6"/>); reduced the timings to 1.16-1.24 ns (1.180 ns at value 6, 1.164 ns at
/// 8191, 1.158 ns at 131071, 1.241 ns at 2147483647, 1.167 ns near ulong.Max), roughly 2.4-2.5x faster.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod6();
}
}
@@ -1,38 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod6UIntBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod6(uint value) => Mod6Lookup[(int)(((value % 3U) << 1) | (value & 1U))];
[Params(6U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 6` baseline; BenchmarkDotNet highlighted zero-measurement noise with means of 0.0348 ns at value 6, 0.0277 ns at 2047,
/// 0.0492 ns at 65535, and 0.0385 ns at 2147483647.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 6U;
}
/// <summary>
/// Lookup-based helper; significantly slower at 0.433 ns (value 6), 0.452 ns (2047), 0.454 ns (65535), and 0.459 ns
/// (2147483647), about 11-19x the raw modulo cost.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod6(Value);
}
}
@@ -1,57 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
[MemoryDiagnoser]
public class Mod6ULongBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod6(ulong value) => Mod6Lookup[(int)(((value % 3UL) << 1) | (value & 1UL))];
[Params(3UL, 131071UL, ulong.MaxValue - 1024UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 6` baseline; recorded 0.264 ns at Value 3, 0.262 ns at 131071, and 0.271 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 6UL;
}
/// <summary>
/// Division-based reduction (`value / 6`); timings were 0.277 ns, 0.284 ns, and 0.287 ns across the three start values.
/// </summary>
[Benchmark]
public ulong DivisionBased()
{
ulong quotient = Value / 6UL;
return Value - quotient * 6UL;
}
/// <summary>
/// FastDivHigh helper; significantly slower at 1.24 ns (Value 3), 1.04 ns (131071), and 1.17 ns (near ulong.Max).
/// </summary>
[Benchmark]
public ulong FastDivHigh()
{
ulong quotient = Value.FastDiv64(6UL, (ulong)(((UInt128)1 << 64) / 6UL));
return Value - quotient * 6UL;
}
/// <summary>
/// Lookup-based helper; cost 0.461 ns at Value 3, 0.467 ns at 131071, 0.467 ns near ulong.Max—about 1.7x slower than the modulo.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod6(Value);
}
}
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod7Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 7U, "UInt32:7"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 7UL, "UInt64:7"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 63UL, "UInt64:Max-63"),
new(ModNumericType.UInt128, 7UL, "UInt128:7"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 127UL, "UInt128:Max-127"),
];
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Mod7(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 7U;
value >>= 3;
if (remainder >= 7U)
{
remainder -= 7U;
}
}
return remainder >= 7U ? remainder - 7U : remainder;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod7(ulong value)
{
ulong remainder = ((uint)value % 7U) + (((uint)(value >> 32) % 7U) << 2);
while (remainder >= 7UL)
{
remainder -= 7UL;
}
return remainder;
}
/// <summary>
/// `% 7` baseline across the merged dataset; hovered near the noise floor on <c>uint</c>, 0.27 ns on <c>ulong</c>, and
/// 2.86-2.91 ns on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 7U,
ModNumericType.UInt64 => (ulong)data.Value % 7UL,
ModNumericType.UInt128 => (ulong)(data.Value % 7UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Extension helpers; dramatically slower on <c>uint</c>, modestly slower on <c>ulong</c>, and 3.2-4.2× faster on
/// <see cref="UInt128"/> inputs where the folding helpers shine.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => Mod7((uint)data.Value),
ModNumericType.UInt64 => Mod7((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod7(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod7UInt128Benchmarks
{
[Params(7UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 7` baseline; timings clustered around 2.86-2.91 ns (2.860 ns at value 7, 2.887 ns at 8191, 2.873 ns at 131071,
/// 2.878 ns at 2147483647, 2.915 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 7UL);
}
/// <summary>
/// Extension helper; achieved 0.676 ns (value 7), 0.675 ns (8191), 0.722 ns (131071), 0.882 ns (2147483647), and 0.676 ns near
/// ulong.Max—between 3.2x and 4.2x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod7();
}
}
@@ -1,50 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod7UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod7(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 7U;
value >>= 3;
if (remainder >= 7U)
{
remainder -= 7U;
}
}
return remainder >= 7U ? remainder - 7U : remainder;
}
[Params(7U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 7` baseline; again near the noise floor with means of 0.0354 ns (value 7), 0.0276 ns (2047), 0.0282 ns (65535), and
/// 0.0422 ns (2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 7U;
}
/// <summary>
/// Extension helper; substantially slower at 0.268 ns (value 7), 1.217 ns (2047), 2.334 ns (65535), and 4.579 ns (2147483647)—up to
/// 110x the baseline.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod7(Value);
}
}
@@ -1,44 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod7ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod7(ulong value)
{
ulong remainder = ((uint)value % 7U) + (((uint)(value >> 32) % 7U) << 2);
while (remainder >= 7UL)
{
remainder -= 7UL;
}
return remainder;
}
[Params(7UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 63UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 7` baseline; 0.274 ns at value 7, 0.270 ns at 8191, 0.268 ns at 131071, 0.270 ns at 2147483647, and 0.275 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 7UL;
}
/// <summary>
/// Extension helper; 0.433 ns at value 7, 0.443 ns at 8191, 0.436 ns at 131071, 0.438 ns at 2147483647, but 0.684 ns near
/// ulong.Max—about 1.6-2.5x slower overall.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod7(Value);
}
}
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod8Benchmarks
{
private static readonly ModBenchmarkCase[] Cases =
[
new(ModNumericType.UInt32, 8U, "UInt32:8"),
new(ModNumericType.UInt32, 2047U, "UInt32:2047"),
new(ModNumericType.UInt32, 65535U, "UInt32:65535"),
new(ModNumericType.UInt32, 2147483647U, "UInt32:2147483647"),
new(ModNumericType.UInt64, 8UL, "UInt64:8"),
new(ModNumericType.UInt64, 8191UL, "UInt64:8191"),
new(ModNumericType.UInt64, 131071UL, "UInt64:131071"),
new(ModNumericType.UInt64, 2147483647UL, "UInt64:2147483647"),
new(ModNumericType.UInt64, ulong.MaxValue - 127UL, "UInt64:Max-127"),
new(ModNumericType.UInt128, 8UL, "UInt128:8"),
new(ModNumericType.UInt128, 8191UL, "UInt128:8191"),
new(ModNumericType.UInt128, 131071UL, "UInt128:131071"),
new(ModNumericType.UInt128, 2147483647UL, "UInt128:2147483647"),
new(ModNumericType.UInt128, ulong.MaxValue - 255UL, "UInt128:Max-255"),
];
public static IEnumerable<ModBenchmarkCase> GetCases() => Cases;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(ulong value)
{
return value & 7UL;
}
/// <summary>
/// `% 8` baseline across the merged dataset; effectively at the noise floor on <c>uint</c> and <c>ulong</c>, and 2.86-2.89 ns
/// on <see cref="UInt128"/> inputs.
/// </summary>
[Benchmark(Baseline = true)]
[ArgumentsSource(nameof(GetCases))]
public ulong ModuloOperator(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => (uint)data.Value % 8U,
ModNumericType.UInt64 => (ulong)data.Value % 8UL,
ModNumericType.UInt128 => (ulong)(data.Value % 8UL),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
/// <summary>
/// Bitmask helpers; match `%` on 32-bit inputs, are typically faster on <c>ulong</c>, and deliver 140-280× wins on
/// <see cref="UInt128"/> by avoiding division.
/// </summary>
[Benchmark]
[ArgumentsSource(nameof(GetCases))]
public ulong ExtensionMethod(ModBenchmarkCase data)
{
return data.Type switch
{
ModNumericType.UInt32 => ((uint)data.Value).Mod8(),
ModNumericType.UInt64 => Mod8((ulong)data.Value),
ModNumericType.UInt128 => data.Value.Mod8(),
_ => throw new ArgumentOutOfRangeException(nameof(data)),
};
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod8UInt128Benchmarks
{
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 8` baseline; 2.86-2.89 ns across the values (2.864 ns at value 8, 2.878 ns at 8191, 2.895 ns at 131071, 2.890 ns at
/// 2147483647, 2.894 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 8UL);
}
/// <summary>
/// Bitmask helper; reduced the cost to 0.010-0.020 ns (0.0170 ns at value 8, 0.0144 ns at 8191, 0.0134 ns at 131071, 0.0105 ns at
/// 2147483647, 0.0203 ns near ulong.Max), delivering a 140-280x speedup.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod8();
}
}
@@ -1,33 +0,0 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod8UIntBenchmarks
{
[Params(8U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 8` baseline; means stayed around 0.011-0.015 ns (0.0109 ns at value 8, 0.0151 ns at 2047, 0.0123 ns at 65535, 0.0122 ns at
/// 2147483647), i.e. indistinguishable from the empty-loop costs.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 8U;
}
/// <summary>
/// Bitmask helper (`value & 7U`); returned 0.0032 ns at value 8, 0.0113 ns at 2047, 0.0191 ns at 65535, and 0.0150 ns at
/// 2147483647—roughly matching the baseline within measurement noise.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Value.Mod8();
}
}
@@ -1,41 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Mod8ULongBenchmarks
{
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public ulong Value { get; set; }
/// <summary>
/// Uses the built-in <c>% 8</c>; measured 0.0224 ns on Value=8, ~0 ns within the measurement noise floor for 8,191,
/// 0.0069 ns for 131,071, 0.0117 ns for 2,147,483,647, and 0.107 ns for the near-maximum value, making it the fastest
/// option except when the branch-free mask wins on huge operands.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 8UL;
}
/// <summary>
/// Applies the branch-free mask helper; costs 0.0344 ns on Value=8, 0.0296 ns on 8,191, 0.0114 ns on 131,071,
/// 0.0106 ns on 2,147,483,647, and just 0.0109 ns on the near-maximum sample, overtaking <c>%</c> once the divisor
/// approaches 2<sup>64</sup>.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod8(Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(ulong value)
{
return value & 7UL;
}
}
@@ -0,0 +1,31 @@
using System;
namespace EvenPerfectBitScanner.Benchmarks;
public enum ModNumericType
{
UInt32,
UInt64,
UInt128,
}
public readonly struct ModBenchmarkCase
{
public ModBenchmarkCase(ModNumericType type, UInt128 value, string display)
{
Type = type;
Value = value;
Display = display;
}
public ModNumericType Type { get; }
public UInt128 Value { get; }
private string Display { get; }
public override string ToString()
{
return Display;
}
}
@@ -1,3 +1,4 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -9,12 +10,10 @@ public class MontgomeryMultiplyBenchmarks
{
private const int MaxBatchSize = 1024;
private readonly ulong[] _operandsA = new ulong[MaxBatchSize];
private readonly ulong[] _operandsB = new ulong[MaxBatchSize];
private readonly ulong[] _moduli = new ulong[MaxBatchSize];
private readonly ulong[] _nPrimes = new ulong[MaxBatchSize];
private readonly Random _random = new(1979);
private ulong[] _operandsA = Array.Empty<ulong>();
private ulong[] _operandsB = Array.Empty<ulong>();
private ulong[] _moduli = Array.Empty<ulong>();
private ulong[] _nPrimes = Array.Empty<ulong>();
[Params(64, 256, 1024)]
public int BatchSize { get; set; }
@@ -22,13 +21,70 @@ public class MontgomeryMultiplyBenchmarks
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < MaxBatchSize; i++)
MontgomerySampleData data = MontgomerySampleDataCache.Instance;
_operandsA = data.OperandsA;
_operandsB = data.OperandsB;
_moduli = data.Moduli;
_nPrimes = data.NPrimes;
}
private sealed class MontgomerySampleData
{
public MontgomerySampleData(ulong[] operandsA, ulong[] operandsB, ulong[] moduli, ulong[] nPrimes)
{
ulong modulus = NextOddModulus();
_moduli[i] = modulus;
_nPrimes[i] = ComputeMontgomeryNPrime(modulus);
_operandsA[i] = (ulong)_random.NextInt64(0, (long)modulus);
_operandsB[i] = (ulong)_random.NextInt64(0, (long)modulus);
OperandsA = operandsA;
OperandsB = operandsB;
Moduli = moduli;
NPrimes = nPrimes;
}
public ulong[] OperandsA { get; }
public ulong[] OperandsB { get; }
public ulong[] Moduli { get; }
public ulong[] NPrimes { get; }
}
private static class MontgomerySampleDataCache
{
private static readonly Lazy<MontgomerySampleData> Cache = new(Create);
public static MontgomerySampleData Instance => Cache.Value;
private static MontgomerySampleData Create()
{
Console.WriteLine("Preparing Montgomery multiply benchmark sample data...");
Random random = new(1979);
var operandsA = new ulong[MaxBatchSize];
var operandsB = new ulong[MaxBatchSize];
var moduli = new ulong[MaxBatchSize];
var nPrimes = new ulong[MaxBatchSize];
for (int i = 0; i < MaxBatchSize; i++)
{
ulong modulus = NextOddModulus(random);
moduli[i] = modulus;
nPrimes[i] = ComputeMontgomeryNPrime(modulus);
operandsA[i] = (ulong)random.NextInt64(0, (long)modulus);
operandsB[i] = (ulong)random.NextInt64(0, (long)modulus);
}
MontgomerySampleData data = new MontgomerySampleData(operandsA, operandsB, moduli, nPrimes);
Console.WriteLine("Finished preparing Montgomery multiply benchmark sample data.");
return data;
}
private static ulong NextOddModulus(Random random)
{
ulong modulus = (ulong)random.NextInt64(3, long.MaxValue);
if ((modulus & 1UL) == 0UL)
{
modulus++;
}
return modulus;
}
}
@@ -68,17 +124,6 @@ public class MontgomeryMultiplyBenchmarks
return checksum;
}
private ulong NextOddModulus()
{
ulong modulus = (ulong)_random.NextInt64(3, long.MaxValue);
if ((modulus & 1UL) == 0UL)
{
modulus++;
}
return modulus;
}
private static ulong OriginalMontgomeryMultiply(ulong a, ulong b, ulong modulus, ulong nPrime)
{
ulong tLow = unchecked(a * b);
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
@@ -90,7 +89,7 @@ public class Mul64Benchmarks
{
ulong leftLow = (ulong)left;
ulong rightLow = (ulong)right;
return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHigh(rightLow)) << 64) | (UInt128)(leftLow * rightLow);
return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHighCpu(rightLow)) << 64) | (UInt128)(leftLow * rightLow);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
@@ -37,10 +36,8 @@ public class MulHighBenchmarks
[Benchmark(Baseline = true)]
public ulong CurrentImplementation()
{
// TODO: Replace ULongExtensions.MulHigh with the UInt128-based helper on CPU paths; it was
// 11–14× faster in the latest benchmarks while the GPU-specific variant remains available
// separately.
return ULongExtensions.MulHigh(Input.X, Input.Y);
// Routes through the UInt128-based helper adopted by CPU hot paths after benchmarking.
return ULongExtensions.MulHighCpu(Input.X, Input.Y);
}
/// <summary>
@@ -108,7 +105,7 @@ public class MulHighBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulHighWithGpuUInt128(ulong x, ulong y)
{
return ULongExtensions.MulHighGpuCompatible(x, y);
return ULongExtensions.MulHighGpu(x, y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -0,0 +1,461 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
/// <summary>
/// GPU benchmarks for 64-bit modular multiplication helpers.
/// Windows 11 results ranged from 0.62 ms to 1.63 ms, and MulModGpu led the PrimeSizedModulus case at 0.641 ms.
/// Recommended helper for p ≥ 138,000,000 is MulModGpu.
/// </summary>
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MulMod64BenchmarksGpu : IDisposable
{
private static readonly MulMod64Input[] Inputs =
[
new(0UL, 0UL, 3UL, "ZeroOperands"),
new(ulong.MaxValue, ulong.MaxValue - 1UL, ulong.MaxValue - 58UL, "NearFullRange"),
new(0xFFFF_FFFF_0000_0000UL, 0x0000_0000_FFFF_FFFFUL, 0xFFFF_FFFF_0000_002BUL, "CrossWordBlend"),
new(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL, 0x1FFF_FFFF_FFFF_FFFBUL, "MixedBitPattern"),
new(0x0000_0001_0000_0000UL, 0x0000_0000_0000_0003UL, 0x0000_0000_FFFF_FFC3UL, "SparseOperands"),
new(0x7FFF_FFFF_FFFF_FFA3UL, 0x6FFF_FFFF_FFFF_FF81UL, 0x7FFF_FFFF_FFFF_FFE7UL, "PrimeSizedModulus"),
];
[ParamsSource(nameof(GetInputs))]
public MulMod64Input Input { get; set; }
private Context? _context;
private Accelerator? _accelerator;
private MemoryBuffer1D<MulMod64GpuKernelInput, Stride1D.Dense>? _inputBuffer;
private MemoryBuffer1D<ulong, Stride1D.Dense>? _resultBuffer;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _extensionBaselineKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _gpuCompatibleExtensionKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _gpuCompatibleSimplifiedKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _inlineKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _inlineReductionFirstKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _inlineOperandReductionKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _inlineLocalsKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _inlineLocalsOperandReductionKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _mulHighKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _legacyKernel;
private Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? _legacyDeferredKernel;
public static IEnumerable<MulMod64Input> GetInputs()
{
return Inputs;
}
[GlobalSetup]
public void GlobalSetup()
{
_context = Context.Create(builder => builder.Default().EnableAlgorithms());
_accelerator = _context.GetPreferredDevice(preferCPU: false).CreateAccelerator(_context);
MulMod64GpuKernelInput[] gpuInputs = Inputs
.Select(i => new MulMod64GpuKernelInput(i.Left, i.Right, i.Modulus))
.ToArray();
_inputBuffer = _accelerator.Allocate1D(gpuInputs);
_resultBuffer = _accelerator.Allocate1D<ulong>(gpuInputs.Length);
_extensionBaselineKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(ExtensionBaselineKernel);
_gpuCompatibleExtensionKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(GpuCompatibleExtensionKernel);
_gpuCompatibleSimplifiedKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(GpuCompatibleSimplifiedKernel);
_inlineKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(InlineKernel);
_inlineReductionFirstKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(InlineReductionFirstKernel);
_inlineOperandReductionKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(InlineOperandReductionKernel);
_inlineLocalsKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(InlineLocalsKernel);
_inlineLocalsOperandReductionKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(InlineLocalsOperandReductionKernel);
_mulHighKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(MulHighKernel);
_legacyKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(LegacyKernel);
_legacyDeferredKernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>(LegacyDeferredKernel);
}
/// <summary>
/// Inline wide multiply/reduce baseline: ZeroOperands 0.618 ms, MixedBitPattern 0.640 ms, CrossWordBlend 0.928 ms, NearFullRange 0.875 ms, PrimeSizedModulus 0.759 ms, SparseOperands 0.804 ms.
/// Use primarily as a reference; optimized GPU-specific helpers beat it across all datasets.
/// </summary>
[Benchmark(Baseline = true)]
public void ExtensionBaseline()
{
LaunchKernel(_extensionBaselineKernel);
}
/// <summary>
/// Original GPU-compatible extension path: means from 0.614 ms (MixedBitPattern) to 1.023 ms (CrossWordBlend) with PrimeSizedModulus at 0.697 ms.
/// Retain only when mirroring the CPU extension logic outweighs the speed gap to the simplified helper.
/// </summary>
[Benchmark]
public void GpuCompatibleMulModExtension()
{
LaunchKernel(_gpuCompatibleExtensionKernel);
}
/// <summary>
/// Simplified GPU extension helper: 0.708 ms (ZeroOperands), 0.905 ms (CrossWordBlend), 0.798 ms (NearFullRange), and class-leading 0.641 ms (PrimeSizedModulus).
/// Recommended for production GPU scans, especially for large p ≥ 138,000,000.
/// </summary>
[Benchmark]
public void MulModGpu()
{
LaunchKernel(_gpuCompatibleSimplifiedKernel);
}
/// <summary>
/// Inline UInt128 multiplication without operand reduction: CrossWordBlend 0.838 ms, MixedBitPattern 0.865 ms, NearFullRange 0.786 ms, PrimeSizedModulus 0.709 ms, SparseOperands 0.751 ms, ZeroOperands 0.827 ms.
/// Suitable when explicit UInt128 math is acceptable, though it trails the simplified GPU helper on every dataset.
/// </summary>
[Benchmark]
public void InlineUInt128Operands()
{
LaunchKernel(_inlineKernel);
}
/// <summary>
/// Reduces operands before multiplying: ZeroOperands 0.926 ms, CrossWordBlend 0.847 ms, MixedBitPattern 0.843 ms, NearFullRange 0.689 ms, PrimeSizedModulus 0.809 ms, SparseOperands 0.746 ms.
/// Only competitive on NearFullRange; the extra reductions hurt other scenarios.
/// </summary>
[Benchmark]
public void InlineUInt128OperandsWithReductionFirst()
{
LaunchKernel(_inlineReductionFirstKernel);
}
/// <summary>
/// Performs operand reduction and then multiplies inline: CrossWordBlend 1.041 ms, MixedBitPattern 0.913 ms, NearFullRange 0.847 ms, PrimeSizedModulus 0.812 ms, SparseOperands 0.744 ms, ZeroOperands 0.802 ms.
/// Consistently slower than both the reduction-first and simplified GPU helpers.
/// </summary>
[Benchmark]
public void InlineUInt128OperandsWithOperandReduction()
{
LaunchKernel(_inlineOperandReductionKernel);
}
/// <summary>
/// Uses local temporaries for the wide product: ZeroOperands 0.691 ms, CrossWordBlend 0.914 ms, MixedBitPattern 0.874 ms, NearFullRange 0.702 ms, PrimeSizedModulus 0.817 ms, SparseOperands 0.728 ms.
/// Balanced choice when sticking with manual wide products, but still slower than the simplified GPU path.
/// </summary>
[Benchmark]
public void InlineUInt128WithLocals()
{
LaunchKernel(_inlineLocalsKernel);
}
/// <summary>
/// Combines local temporaries with operand reduction: ZeroOperands 0.698 ms, CrossWordBlend 0.858 ms, MixedBitPattern 0.877 ms, NearFullRange 0.851 ms, PrimeSizedModulus 0.761 ms, SparseOperands 0.779 ms.
/// Adds little benefit over the non-reduced local variant while increasing cost on most datasets.
/// </summary>
[Benchmark]
public void InlineUInt128WithLocalsAndOperandReduction()
{
LaunchKernel(_inlineLocalsOperandReductionKernel);
}
/// <summary>
/// Multiply-high decomposition: ZeroOperands 0.651 ms, CrossWordBlend 0.848 ms, MixedBitPattern 0.758 ms, NearFullRange 0.812 ms, PrimeSizedModulus 0.728 ms, SparseOperands 0.699 ms.
/// Useful when avoiding UInt128 temporaries on sparse inputs, but still behind the simplified GPU helper for prime-sized moduli.
/// </summary>
[Benchmark]
public void MultiplyHighDecomposition()
{
LaunchKernel(_mulHighKernel);
}
/// <summary>
/// Legacy GPU baseline: ZeroOperands 0.759 ms, CrossWordBlend 0.971 ms, MixedBitPattern 0.827 ms, NearFullRange 0.870 ms, PrimeSizedModulus 0.737 ms, SparseOperands 0.620 ms.
/// Retain only for regression comparisons; newer helpers are faster except on the sparse sample.
/// </summary>
[Benchmark]
public void GpuCompatibleBaseline()
{
LaunchKernel(_legacyKernel);
}
/// <summary>
/// Deferred legacy helper: ZeroOperands 1.561 ms, CrossWordBlend 1.631 ms, MixedBitPattern 1.540 ms, NearFullRange 1.438 ms, PrimeSizedModulus 1.598 ms, SparseOperands 1.096 ms.
/// Slowest option in every scenario; keep only for historical context.
/// </summary>
[Benchmark]
public void GpuCompatibleDeferred()
{
LaunchKernel(_legacyDeferredKernel);
}
[GlobalCleanup]
public void GlobalCleanup()
{
_resultBuffer?.Dispose();
_inputBuffer?.Dispose();
_accelerator?.Dispose();
_context?.Dispose();
}
public void Dispose()
{
GlobalCleanup();
GC.SuppressFinalize(this);
}
private void LaunchKernel(Action<Index1D, ArrayView<MulMod64GpuKernelInput>, ArrayView<ulong>>? kernel)
{
if (kernel == null || _accelerator == null || _inputBuffer == null || _resultBuffer == null)
{
throw new InvalidOperationException("Benchmark kernels are not initialized.");
}
kernel(new Index1D((int)_inputBuffer.Length), _inputBuffer.View, _resultBuffer.View);
_accelerator.Synchronize();
}
private static void ExtensionBaselineKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
results[index] = MulMod64Baseline(input.Left, input.Right, input.Modulus);
}
private static void GpuCompatibleExtensionKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
GpuUInt128 state = new(input.Left);
results[index] = state.MulMod(input.Right, input.Modulus);
}
private static void GpuCompatibleSimplifiedKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
GpuUInt128 state = new(input.Left);
results[index] = state.MulMod(input.Right, input.Modulus);
}
private static void InlineKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
results[index] = MulMod64Baseline(input.Left, input.Right, input.Modulus);
}
private static void InlineReductionFirstKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
ulong left = input.Left % input.Modulus;
ulong right = input.Right % input.Modulus;
results[index] = MulMod64Baseline(left, right, input.Modulus);
}
private static void InlineOperandReductionKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
ulong left = input.Left % input.Modulus;
ulong right = input.Right % input.Modulus;
results[index] = MulMod64Baseline(left, right, input.Modulus);
}
private static void InlineLocalsKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
MultiplyWide(input.Left, input.Right, out ulong productHigh, out ulong productLow);
results[index] = ReduceWide(productHigh, productLow, input.Modulus);
}
private static void InlineLocalsOperandReductionKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
ulong left = input.Left % input.Modulus;
ulong right = input.Right % input.Modulus;
MultiplyWide(left, right, out ulong productHigh, out ulong productLow);
results[index] = ReduceWide(productHigh, productLow, input.Modulus);
}
private static void MulHighKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
ulong left = input.Left % input.Modulus;
ulong right = input.Right % input.Modulus;
MultiplyWide(left, right, out ulong productHigh, out ulong productLow);
results[index] = ReduceWide(productHigh, productLow, input.Modulus);
}
private static void LegacyKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
results[index] = MulMod64GpuLegacy(input.Left, input.Right, input.Modulus);
}
private static void LegacyDeferredKernel(Index1D index, ArrayView<MulMod64GpuKernelInput> inputs, ArrayView<ulong> results)
{
if (index >= inputs.Length)
{
return;
}
MulMod64GpuKernelInput input = inputs[index];
results[index] = MulMod64GpuDeferredLegacy(input.Left, input.Right, input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64Baseline(ulong left, ulong right, ulong modulus)
{
MultiplyWide(left, right, out ulong high, out ulong low);
return ReduceWide(high, low, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void MultiplyWide(ulong left, ulong right, out ulong high, out ulong low)
{
ulong leftLow = (uint)left;
ulong leftHigh = left >> 32;
ulong rightLow = (uint)right;
ulong rightHigh = right >> 32;
ulong productLL = leftLow * rightLow;
ulong productLH = leftLow * rightHigh;
ulong productHL = leftHigh * rightLow;
ulong productHH = leftHigh * rightHigh;
ulong carry = (productLL >> 32) + (productLH & 0xFFFF_FFFFUL) + (productHL & 0xFFFF_FFFFUL);
low = (carry << 32) | (uint)productLL;
high = productHH + (productLH >> 32) + (productHL >> 32) + (carry >> 32);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ReduceWide(ulong high, ulong low, ulong modulus)
{
if (modulus <= 1UL)
{
return 0UL;
}
ulong remainder = 0UL;
if (high != 0UL)
{
for (int bit = 63; bit >= 0; --bit)
{
remainder = ShiftLeftAddBit(remainder, (high >> bit) & 1UL, modulus);
}
}
for (int bit = 63; bit >= 0; --bit)
{
remainder = ShiftLeftAddBit(remainder, (low >> bit) & 1UL, modulus);
}
return remainder;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ShiftLeftAddBit(ulong value, ulong bit, ulong modulus)
{
value <<= 1;
if (value >= modulus)
{
value -= modulus;
}
if (bit != 0UL)
{
value += 1UL;
if (value >= modulus)
{
value -= modulus;
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a % modulus);
return state.MulMod(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuDeferredLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a);
return state.MulModWithNativeModulo(b, modulus);
}
public readonly record struct MulMod64Input(ulong Left, ulong Right, ulong Modulus, string Name)
{
public override string ToString()
{
return Name;
}
}
private readonly struct MulMod64GpuKernelInput
{
public readonly ulong Left;
public readonly ulong Right;
public readonly ulong Modulus;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public MulMod64GpuKernelInput(ulong left, ulong right, ulong modulus)
{
Left = left;
Right = right;
Modulus = modulus;
}
}
}
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -59,7 +58,7 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulModSimplified(ulong, ulong, ulong)"/> helper; performance mirrors
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod(ulong, ulong, ulong)"/> helper; performance mirrors
/// the full helper with 5.24–5.95 ns on sparse inputs and 1.99 ns when zeros dominate, but 22.8–59.2 ns on dense blends.
/// </summary>
/// <remarks>
@@ -67,10 +66,10 @@ public class MulMod64Benchmarks
/// PrimeSizedModulus 50.490 ns, SparseOperands 5.239 ns, ZeroOperands 1.986 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleMulModSimplifiedExtension()
public ulong MulModGpu()
{
GpuUInt128 gpuUInt128 = new(Input.Left);
return gpuUInt128.MulModSimplified(Input.Right, Input.Modulus);
return gpuUInt128.MulMod(Input.Right, Input.Modulus);
}
/// <summary>
@@ -158,16 +157,45 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Uses the production <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper shared by CPU and GPU callers.
/// Benchmarks the legacy GPU-oriented shim that previously powered <see cref="ULongExtensions.MulMod64Gpu(ulong, ulong, ulong)"/>;
/// dense inputs still land in the 24.4–61.9 ns range while sparse operands remain closer to 3.6–7.3 ns.
/// </summary>
/// <remarks>
/// Observed means prior to the helper consolidation: CrossWordBlend 24.422 ns, MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// Observed means: CrossWordBlend 24.422 ns (6.86×), MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns.
/// </remarks>
[Benchmark]
public ulong UnifiedMulMod()
public ulong GpuCompatibleBaseline()
{
return Input.Left.MulMod64(Input.Right, Input.Modulus);
return MulMod64GpuLegacy(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Benchmarks the deferred native-modulo shim that now lives only in this suite; tiny operands still excel around 2 ns
/// while dense datasets take 146–295 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 146.559 ns (40.63×), MixedBitPattern 255.555 ns, NearFullRange 21.390 ns,
/// PrimeSizedModulus 294.486 ns, SparseOperands 18.182 ns, ZeroOperands 2.009 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleDeferred()
{
return MulMod64GpuDeferredLegacy(Input.Left, Input.Right, Input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a % modulus);
return state.MulMod(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuDeferredLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a);
return state.MulModWithNativeModulo(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -212,7 +240,7 @@ public class MulMod64Benchmarks
ulong left = a % modulus;
ulong right = b % modulus;
ulong low = left * right;
ulong high = left.MulHigh(right);
ulong high = left.MulHighCpu(right);
UInt128 product = ((UInt128)high << 64) | low;
return (ulong)(product % modulus);
}
@@ -1,75 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128AddBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong temp = source.Low + value.Low;
source.Low = temp;
temp = temp < source.Low ? 1UL : 0UL;
temp += source.High;
temp += value.High;
source.High = temp;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Add(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,100 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128Mul64Benchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mul64(ulong left, ulong right, out ulong low)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong leftLow = left & Mask32;
ulong leftHigh = left >> 32;
ulong rightLow = right & Mask32;
ulong rightHigh = right >> 32;
ulong lowLow = leftLow * rightLow;
ulong lowHigh = leftLow * rightHigh;
ulong highLow = leftHigh * rightLow;
ulong highHigh = leftHigh * rightHigh;
ulong carry = (lowLow >> 32) + (lowHigh & Mask32) + (highLow & Mask32);
ulong high = highHigh + (lowHigh >> 32) + (highLow >> 32) + (carry >> 32);
low = ((carry & Mask32) << 32) | (lowLow & Mask32);
return high;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, ulong value)
{
ulong currentHigh = source.High;
ulong currentLow = source.Low;
ulong carry = Mul64(currentLow, value, out ulong newLow);
Mul64(currentHigh, value, out ulong shiftedHigh);
ulong newHigh = shiftedHigh + carry;
source.Low = newLow;
source.High = newHigh;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.113 ns with mixed operands,
/// 3.128 ns when both limbs and inputs are near their maxima, and 3.131 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.113 ns, NearMaxOperands 3.128 ns, TinyOperands 3.131 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 2.969 ns on mixed operands,
/// 2.912 ns on near-max inputs, and 2.935 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 2.969 ns, NearMaxOperands 2.912 ns, TinyOperands 2.935 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Mul(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,76 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128MultiplyAddBenchmarks
{
private static readonly MultiplyAddInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, 0x5UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, 0x1357_9BDF_2468_ACEEUL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFF7UL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public MultiplyAddInput Input { get; set; }
public static IEnumerable<MultiplyAddInput> GetInputs() => Inputs;
private static UInt128 Create(ulong high, ulong low)
{
return ((UInt128)high << 64) | low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void MultiplyAdd(ref MutableUInt128 source, ulong multiplier, ulong addend)
{
source.Mul(multiplier);
source.Add(addend);
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 MultiplyAddChained()
{
MutableUInt128 value = new(Input.InitialValue);
MultiplyAdd(ref value, Input.Multiplier, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 MultiplyAddFullyInlined()
{
MutableUInt128 value = new(Input.InitialValue);
value.MulAdd(Input.Multiplier, Input.Addend);
return value;
}
public readonly record struct MultiplyAddInput(UInt128 InitialValue, ulong Multiplier, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,75 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128SubMutableBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong low = source.Low;
ulong result = low - value.Low;
ulong borrow = low < value.Low ? 1UL : 0UL;
source.Low = result;
source.High = source.High - value.High - borrow;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Sub(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,9 +1,7 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -41,34 +39,37 @@ public class Pow2MontgomeryModBenchmarks
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = MersenneDivisorCycles.CalculateCycleLength(smallModulus, MontgomeryDivisorData.FromModulus(smallModulus));
Console.WriteLine($"Calculating large cycle {i + 1} with heuristics");
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = largeCycle;
}
}
/// <summary>
/// Baseline right-to-left Montgomery ladder; measured 36.36 μs on the small sample set and 117.32 μs on the large one.
/// </summary>
[Benchmark(Baseline = true)]
public ulong BaselineCpu()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
/// <summary>
/// Baseline right-to-left Montgomery ladder; measured 36.36 μs on the small sample set and 117.32 μs on the large one.
/// </summary>
[Benchmark(Baseline = true)]
public ulong BaselineCpu()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
}
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
}
return checksum;
}
return checksum;
}
/// <summary>
/// Baseline right-to-left Montgomery ladder; measured ??.?? μs on the small sample set and ??.?? μs on the large one.
///
/// This benchmark failed running, hanging up early before reaching workload testing.
/// </summary>
[Benchmark]
// [Benchmark]
public ulong BaselineGpu()
{
GetData(out var exponents, out var divisors, out _);
@@ -261,7 +262,7 @@ public class Pow2MontgomeryModBenchmarks
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiplyCpu(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
@@ -299,22 +300,22 @@ public class Pow2MontgomeryModBenchmarks
if (exponent == 0UL)
{
return result.MontgomeryMultiply(1UL, modulus, nPrime);
return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime);
}
int msbIndex = 63 - BitOperations.LeadingZeroCount(exponent);
for (int bitIndex = msbIndex; bitIndex >= 0; bitIndex--)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(result, modulus, nPrime);
if (((exponent >> bitIndex) & 1UL) != 0UL)
{
result = result.MontgomeryMultiply(baseVal, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(baseVal, modulus, nPrime);
}
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime);
}
private static ulong Pow2MontgomeryModBatched4(ulong exponent, in MontgomeryDivisorData divisor)
@@ -335,40 +336,41 @@ public class Pow2MontgomeryModBenchmarks
ulong nibble = remainingExponent & 0xFUL;
ulong base0 = baseVal;
ulong base1 = base0.MontgomeryMultiply(base0, modulus, nPrime);
ulong base2 = base1.MontgomeryMultiply(base1, modulus, nPrime);
ulong base3 = base2.MontgomeryMultiply(base2, modulus, nPrime);
ulong base1 = base0.MontgomeryMultiplyCpu(base0, modulus, nPrime);
ulong base2 = base1.MontgomeryMultiplyCpu(base1, modulus, nPrime);
ulong base3 = base2.MontgomeryMultiplyCpu(base2, modulus, nPrime);
if ((nibble & 1UL) != 0UL)
{
result = result.MontgomeryMultiply(base0, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(base0, modulus, nPrime);
}
if ((nibble & 2UL) != 0UL)
{
result = result.MontgomeryMultiply(base1, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(base1, modulus, nPrime);
}
if ((nibble & 4UL) != 0UL)
{
result = result.MontgomeryMultiply(base2, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(base2, modulus, nPrime);
}
if ((nibble & 8UL) != 0UL)
{
result = result.MontgomeryMultiply(base3, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(base3, modulus, nPrime);
}
baseVal = base3.MontgomeryMultiply(base3, modulus, nPrime);
baseVal = base3.MontgomeryMultiplyCpu(base3, modulus, nPrime);
remainingExponent >>= 4;
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime);
}
// TODO: Update it with the latest implementations
private static ulong Pow2MontgomeryModSlidingWindow(ulong exponent, in MontgomeryDivisorData divisor)
{
ulong modulus = divisor.Modulus;
ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
@@ -380,16 +382,16 @@ public class Pow2MontgomeryModBenchmarks
if (exponent == 0UL)
{
return result.MontgomeryMultiply(1UL, modulus, nPrime);
return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime);
}
const int WindowSize = 5;
Span<ulong> oddPowers = stackalloc ulong[1 << (WindowSize - 1)];
oddPowers[0] = baseVal;
ulong baseSquared = baseVal.MontgomeryMultiply(baseVal, modulus, nPrime);
ulong baseSquared = baseVal.MontgomeryMultiplyCpu(baseVal, modulus, nPrime);
for (int i = 1; i < oddPowers.Length; i++)
{
oddPowers[i] = oddPowers[i - 1].MontgomeryMultiply(baseSquared, modulus, nPrime);
oddPowers[i] = oddPowers[i - 1].MontgomeryMultiplyCpu(baseSquared, modulus, nPrime);
}
int bitIndex = 63 - BitOperations.LeadingZeroCount(exponent);
@@ -398,7 +400,7 @@ public class Pow2MontgomeryModBenchmarks
{
if (((exponent >> bitIndex) & 1UL) == 0UL)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(result, modulus, nPrime);
bitIndex--;
continue;
}
@@ -415,16 +417,17 @@ public class Pow2MontgomeryModBenchmarks
int squares = bitIndex - windowStart + 1;
for (int i = 0; i < squares; i++)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
result = result.MontgomeryMultiplyCpu(result, modulus, nPrime);
}
result = result.MontgomeryMultiply(oddPowers[(int)(windowValue >> 1)], modulus, nPrime);
result = result.MontgomeryMultiplyCpu(oddPowers[(int)(windowValue >> 1)], modulus, nPrime);
bitIndex = windowStart - 1;
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
return result.MontgomeryMultiplyCpu(1UL, modulus, nPrime);
}
// TODO: Update it with the latest implementations, if it exists in production code. Otherwise just remove TODO
private static ulong Pow2ModBinary(ulong exponent, ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
@@ -1,412 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
/// <remarks>
/// Benchmark results collected on AMD Ryzen 5 5625U (.NET 8.0, launchCount: 1, warmupCount: 1, iterationCount: 5):
/// Small sample (~1e6 random odd moduli)
/// - MontgomeryWithoutCycle: 35.41 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 25.81 µs (0.73× baseline)
/// - MontgomeryWithGpuCycleComputation: 54.09 ms (1,527× baseline, includes GPU cycle calculation)
/// Large sample (Mersenne-like moduli from 2^48-1 to 2^63-1)
/// - MontgomeryWithoutCycle: 117.33 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 9.12 µs (0.08× baseline)
/// - MontgomeryWithGpuCycleComputation: 12.91 µs (0.11× baseline)
/// </remarks>
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModCycleComputationBenchmarks
{
private const int SampleCount = 256;
private const ulong OneShiftedLeft63 = 1UL << 63;
private const long OneShiftedLeft60 = 1L << 60;
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _veryLargeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly UInt128[] _wideModuli = new UInt128[SampleCount];
private readonly ulong[] _smallExponents = new ulong[SampleCount];
private readonly ulong[] _largeExponents = new ulong[SampleCount];
private readonly ulong[] _veryLargeExponents = new ulong[SampleCount];
private readonly ulong[] _smallCycles = new ulong[SampleCount];
private readonly ulong[] _largeCycles = new ulong[SampleCount];
private readonly ulong[] _veryLargeCycles = new ulong[SampleCount];
private readonly UInt128[] _wideCycles = new UInt128[SampleCount];
private readonly Random _random = new(113);
private ulong? _previousPrimeOrder;
private UInt128? _previousWidePrimeOrder;
public enum InputScale
{
Small,
Large,
VeryLarge
}
[ParamsAllValues]
public InputScale Scale { get; set; }
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
#if DEBUG
Console.WriteLine($"Generating sample {i}");
#endif
ulong smallModulus = NextSmallOddModulus();
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = CalculateCycleLengthWithHeuristics(smallModulus);
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = CalculateCycleLengthWithHeuristics(largeModulus, largeCycle);
ulong veryLargeModulus = NextVeryLargeOddModulus();
_veryLargeDivisors[i] = CreateMontgomeryDivisorData(veryLargeModulus);
_veryLargeExponents[i] = NextVeryLargeExponent();
#if DEBUG
Console.WriteLine($"Calculating cycle length {veryLargeModulus}");
#endif
_veryLargeCycles[i] = CalculateCycleLengthWithHeuristics(veryLargeModulus);
Console.WriteLine($"Generating wide sample {i}");
UInt128 wideModulus = NextWideOddModulus();
_wideModuli[i] = wideModulus;
_wideCycles[i] = CalculateCycleLengthWithHeuristics(wideModulus);
}
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycleCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycleGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycleCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycleGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputationCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputationGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputationCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputationGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public UInt128 HeuristicWideCycleComputation()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
UInt128 cycle = CalculateCycleLengthWithHeuristics(_wideModuli[i]);
_wideCycles[i] = cycle;
checksum ^= cycle;
}
return checksum;
}
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
{
switch (Scale)
{
case InputScale.Small:
exponents = _smallExponents;
divisors = _smallDivisors;
cycles = _smallCycles;
break;
case InputScale.Large:
exponents = _largeExponents;
divisors = _largeDivisors;
cycles = _largeCycles;
break;
case InputScale.VeryLarge:
exponents = _veryLargeExponents;
divisors = _veryLargeDivisors;
cycles = _veryLargeCycles;
break;
default:
throw new ArgumentOutOfRangeException(nameof(Scale), Scale, null);
}
}
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
private ulong NextLargeExponent() => (ulong)_random.NextInt64(OneShiftedLeft60, long.MaxValue);
private ulong NextVeryLargeExponent()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = checked(BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63);
return value;
}
private ulong NextSmallOddModulus()
{
ulong value = (ulong)_random.NextInt64(3L, 1_000_000L);
return value | 1UL;
}
private (ulong modulus, ulong cycleLength) NextLargeModulusAndCycle()
{
int bitLength = _random.Next(48, 64);
ulong modulus = (1UL << bitLength) - 1UL;
return (modulus, (ulong)bitLength);
}
private ulong NextVeryLargeOddModulus()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63 | 1UL;
return value;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
Span<byte> highSlice = buffer.Slice(8, 8);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(highSlice) | (1UL << 63);
UInt128 value = ((UInt128)high << 64) | low;
return value | UInt128.One;
}
private static MontgomeryDivisorData CreateMontgomeryDivisorData(ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL, 0UL);
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private ulong CalculateCycleLengthWithHeuristics(ulong modulus, ulong fallbackOrder = 0UL)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(modulus);
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
// When the below is uncommented, the benchmark never completes due to the primality test cost on very large random set.
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(modulus);
// // bool isPrime = PrimeTester.IsPrimeInternal(modulus, CancellationToken.None);
// if (!isPrime)
// {
// return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus);
// }
#if DEBUG
Console.WriteLine("Trying heuristic. Prime order calculation");
#endif
ulong order = PrimeOrderCalculator.Calculate(
modulus,
_previousPrimeOrder,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
if (order != 0UL)
{
_previousPrimeOrder = order;
return order;
}
_previousPrimeOrder = null;
Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation");
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
private UInt128 CalculateCycleLengthWithHeuristics(UInt128 modulus)
{
if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero)
{
return MersenneDivisorCycles.GetCycle(modulus);
}
#if DEBUG
Console.WriteLine("Trying heuristic. Wide prime order calculation");
#endif
UInt128 order = PrimeOrderCalculator.Calculate(
modulus,
_previousWidePrimeOrder,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
if (order != UInt128.Zero)
{
_previousWidePrimeOrder = order;
return order;
}
_previousWidePrimeOrder = null;
Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation");
return MersenneDivisorCycles.GetCycle(modulus);
}
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
}
@@ -1,283 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModUInt128Benchmarks
{
private const int SampleCount = 256;
private const ulong Pow2WindowFallbackThreshold = 32UL;
private readonly UInt128[] _moduli = new UInt128[SampleCount];
private readonly UInt128[] _exponents = new UInt128[SampleCount];
private readonly Random _random = new(19);
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
UInt128 modulus = NextWideOddModulus();
_moduli[i] = modulus;
UInt128 exponent = NextWideExponent();
_exponents[i] = exponent;
}
}
/// <summary>
/// Current UInt128-native windowed Montgomery ladder that avoids GpuUInt128 on CPU paths.
/// </summary>
[Benchmark(Baseline = true)]
public UInt128 UInt128Windowed()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= _exponents[i].Pow2MontgomeryModWindowed(_moduli[i]);
}
return checksum;
}
/// <summary>
/// Previous CPU shim that reused the GPU-oriented GpuUInt128 implementation.
/// </summary>
[Benchmark]
public UInt128 GpuStructShim()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2MontgomeryModWindowedGpuShim(_exponents[i], _moduli[i]);
}
return checksum;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..]);
if (high == 0UL)
{
high = 1UL;
}
UInt128 modulus = ((UInt128)high << 64) | low;
modulus |= UInt128.One;
if (modulus <= UInt128.One)
{
modulus += 2UL;
}
return modulus;
}
private UInt128 NextWideExponent()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..]);
if (high == 0UL)
{
high = 1UL;
}
return ((UInt128)high << 64) | low;
}
private static UInt128 Pow2MontgomeryModWindowedGpuShim(UInt128 exponent, UInt128 modulus)
{
if (modulus == UInt128.One)
{
return UInt128.Zero;
}
if (exponent == UInt128.Zero)
{
return UInt128.One % modulus;
}
GpuUInt128 modulusGpu = new(modulus);
GpuUInt128 exponentGpu = new(exponent);
GpuUInt128 baseValue = new(2UL);
if (baseValue.CompareTo(modulusGpu) >= 0)
{
baseValue.Sub(modulusGpu);
}
if (ShouldUseSingleBit(exponentGpu))
{
GpuUInt128 singleBitResult = Pow2MontgomeryModSingleBit(exponentGpu, modulusGpu, baseValue);
return (UInt128)singleBitResult;
}
int bitLength = exponentGpu.GetBitLength();
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
Span<GpuUInt128> oddPowers = stackalloc GpuUInt128[oddPowerCount];
InitializeOddPowers(baseValue, modulusGpu, oddPowers);
GpuUInt128 result = GpuUInt128.One;
int index = bitLength - 1;
while (index >= 0)
{
if (!IsBitSet(exponentGpu, index))
{
result.MulMod(result, modulusGpu);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (!IsBitSet(exponentGpu, windowStart))
{
windowStart++;
}
int windowBitCount = index - windowStart + 1;
for (int square = 0; square < windowBitCount; square++)
{
result.MulMod(result, modulusGpu);
}
ulong windowValue = ExtractWindowValue(exponentGpu, windowStart, windowBitCount);
int tableIndex = (int)((windowValue - 1UL) >> 1);
GpuUInt128 factor = oddPowers[tableIndex];
result.MulMod(factor, modulusGpu);
index = windowStart - 1;
}
return (UInt128)result;
}
private static bool ShouldUseSingleBit(GpuUInt128 exponent)
{
if (exponent.High == 0UL && exponent.Low <= Pow2WindowFallbackThreshold)
{
return true;
}
return false;
}
private static int GetWindowSize(int bitLength)
{
if (bitLength <= 8)
{
return Math.Max(bitLength, 1);
}
if (bitLength <= 23)
{
return 4;
}
if (bitLength <= 79)
{
return 5;
}
if (bitLength <= 239)
{
return 6;
}
if (bitLength <= 671)
{
return 7;
}
return 8;
}
private static void InitializeOddPowers(GpuUInt128 baseValue, GpuUInt128 modulus, Span<GpuUInt128> oddPowers)
{
oddPowers[0] = baseValue;
if (oddPowers.Length == 1)
{
return;
}
// Reusing baseValue to hold base^2 for the shared odd-power ladder.
baseValue.MulMod(baseValue, modulus);
for (int i = 1; i < oddPowers.Length; i++)
{
oddPowers[i] = oddPowers[i - 1];
oddPowers[i].MulMod(baseValue, modulus);
}
}
private static GpuUInt128 Pow2MontgomeryModSingleBit(GpuUInt128 exponent, GpuUInt128 modulus, GpuUInt128 baseValue)
{
GpuUInt128 result = GpuUInt128.One;
while (!exponent.IsZero)
{
if ((exponent.Low & 1UL) != 0UL)
{
result.MulMod(baseValue, modulus);
}
exponent.ShiftRight(1);
if (exponent.IsZero)
{
break;
}
// Reusing baseValue to store the squared base for the next iteration.
baseValue.MulMod(baseValue, modulus);
}
return result;
}
private static bool IsBitSet(GpuUInt128 value, int bitIndex)
{
if (bitIndex >= 64)
{
return ((value.High >> (bitIndex - 64)) & 1UL) != 0UL;
}
return ((value.Low >> bitIndex) & 1UL) != 0UL;
}
private static ulong ExtractWindowValue(GpuUInt128 exponent, int windowStart, int windowBitCount)
{
if (windowStart != 0)
{
GpuUInt128 shifted = exponent;
shifted.ShiftRight(windowStart);
ulong mask = (1UL << windowBitCount) - 1UL;
return shifted.Low & mask;
}
ulong directMask = (1UL << windowBitCount) - 1UL;
return exponent.Low & directMask;
}
}
@@ -0,0 +1,94 @@
using System;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
internal sealed class Pow2MontgomeryModWindowedBenchmarkInputs
{
public Pow2MontgomeryModWindowedBenchmarkInputs(Pow2MontgomeryModWindowedBenchmarkCase[] cases)
{
Cases = cases;
}
public Pow2MontgomeryModWindowedBenchmarkCase[] Cases { get; }
}
internal readonly struct Pow2MontgomeryModWindowedBenchmarkCase
{
public Pow2MontgomeryModWindowedBenchmarkCase(
ulong exponent,
ulong modulus,
in MontgomeryDivisorData divisor,
ulong cycleLength,
ulong reducedExponent)
{
Exponent = exponent;
Modulus = modulus;
Divisor = divisor;
CycleLength = cycleLength;
ReducedExponent = reducedExponent;
}
public ulong Exponent { get; }
public ulong Modulus { get; }
public MontgomeryDivisorData Divisor { get; }
public ulong CycleLength { get; }
public ulong ReducedExponent { get; }
}
internal static class Pow2MontgomeryModWindowedBenchmarkInputsProvider
{
private static readonly Lazy<Pow2MontgomeryModWindowedBenchmarkInputs> Cache = new(Create);
public static Pow2MontgomeryModWindowedBenchmarkInputs Instance => Cache.Value;
private static Pow2MontgomeryModWindowedBenchmarkInputs Create()
{
Pow2MontgomeryModWindowedBenchmarkSeed[] seeds =
{
new Pow2MontgomeryModWindowedBenchmarkSeed(138_000_001UL, 1UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(150_000_013UL, 2UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(175_000_019UL, 3UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(190_000_151UL, 4UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(210_000_089UL, 5UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(230_000_039UL, 6UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(260_000_111UL, 7UL),
new Pow2MontgomeryModWindowedBenchmarkSeed(300_000_007UL, 8UL)
};
var cases = new Pow2MontgomeryModWindowedBenchmarkCase[seeds.Length];
for (int i = 0; i < seeds.Length; i++)
{
Pow2MontgomeryModWindowedBenchmarkSeed seed = seeds[i];
ulong modulus = 2UL * seed.Multiplier * seed.Exponent + 1UL;
MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(modulus);
ulong cycleLength = MersenneDivisorCycles.CalculateCycleLength(modulus, divisor);
ulong reducedExponent = seed.Exponent % cycleLength;
cases[i] = new Pow2MontgomeryModWindowedBenchmarkCase(
seed.Exponent,
modulus,
divisor,
cycleLength,
reducedExponent);
}
return new Pow2MontgomeryModWindowedBenchmarkInputs(cases);
}
private readonly struct Pow2MontgomeryModWindowedBenchmarkSeed
{
public Pow2MontgomeryModWindowedBenchmarkSeed(ulong exponent, ulong multiplier)
{
Exponent = exponent;
Multiplier = multiplier;
}
public ulong Exponent { get; }
public ulong Multiplier { get; }
}
}
@@ -0,0 +1,94 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
/// <summary>
/// Benchmarks CPU Montgomery exponentiation helpers for exponents at or above 138 million.
/// Reuses the shared benchmark input cache so every method processes identical samples.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModWindowedKeepMontgomeryCpuBenchmarks
{
private Pow2MontgomeryModWindowedBenchmarkInputs _inputs = null!;
[GlobalSetup]
public void Setup()
{
_inputs = Pow2MontgomeryModWindowedBenchmarkInputsProvider.Instance;
}
/// <summary>
/// Baseline CPU windowed Montgomery exponentiation that converts the residue back to standard form.
/// </summary>
[Benchmark(Baseline = true)]
public ulong WindowedMontgomeryConvert()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2MontgomeryModWindowedCpu(current.Divisor, keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// CPU windowed Montgomery exponentiation that keeps the residue in the Montgomery domain.
/// </summary>
[Benchmark]
public ulong WindowedMontgomeryKeep()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2MontgomeryModWindowedCpu(current.Divisor, keepMontgomery: true);
}
return checksum;
}
/// <summary>
/// Uses the known cycle length to fold the exponent before running the windowed Montgomery ladder.
/// </summary>
[Benchmark]
public ulong WindowedMontgomeryWithCycle()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2MontgomeryModWithCycleCpu(current.CycleLength, current.Divisor);
}
return checksum;
}
/// <summary>
/// Runs the windowed Montgomery ladder on the reduced exponent that the divisor cycle exposes.
/// </summary>
[Benchmark]
public ulong WindowedMontgomeryFromCycleRemainder()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.ReducedExponent.Pow2MontgomeryModFromCycleRemainderCpu(current.Divisor);
}
return checksum;
}
}
@@ -0,0 +1,78 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
/// <summary>
/// Benchmarks GPU Montgomery exponentiation helpers for exponents at or above 138 million.
/// The sample set is generated once and reused across all benchmark methods so every variant
/// processes the same data without incurring repeated initialization costs.
/// </summary>
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModWindowedKeepMontgomeryGpuBenchmarks
{
private Pow2MontgomeryModWindowedBenchmarkInputs _inputs = null!;
[GlobalSetup]
public void Setup()
{
_inputs = Pow2MontgomeryModWindowedBenchmarkInputsProvider.Instance;
}
/// <summary>
/// Baseline GPU windowed Montgomery exponentiation that converts back to the standard residue.
/// </summary>
[Benchmark(Baseline = true)]
public ulong WindowedMontgomeryConvert()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2MontgomeryModWindowedGpu(current.Divisor, keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// GPU windowed Montgomery exponentiation that keeps the result in the Montgomery domain.
/// </summary>
[Benchmark]
public ulong WindowedMontgomeryKeep()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2MontgomeryModWindowedGpu(current.Divisor, keepMontgomery: true);
}
return checksum;
}
/// <summary>
/// GPU windowed modular exponentiation without Montgomery reduction for comparison.
/// </summary>
[Benchmark]
public ulong WindowedStandardMod()
{
ulong checksum = 0UL;
Pow2MontgomeryModWindowedBenchmarkCase[] cases = _inputs.Cases;
for (int i = 0; i < cases.Length; i++)
{
ref readonly Pow2MontgomeryModWindowedBenchmarkCase current = ref cases[i];
checksum ^= current.Exponent.Pow2ModWindowedGpu(current.Modulus);
}
return checksum;
}
}
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -0,0 +1,265 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using Open.Numeric.Primes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class PrimeTesterBenchmarks
{
private static readonly HeuristicPrimeTester _heuristicTester = new();
private ulong[] _candidates = [];
[ParamsSource(nameof(GetBenchmarkCases))]
public PrimeBenchmarkCase BenchmarkCase { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
_candidates = BenchmarkCase.Candidates;
if (_candidates.Length == 0)
{
return;
}
_ = _heuristicTester.IsPrimeCpu(_candidates[0]);
_ = _heuristicTester.IsPrimeGpu(_candidates[0]);
_ = Prime.Numbers.IsPrime(_candidates[0]);
}
[Benchmark]
public int HeuristicCpu()
{
HeuristicPrimeTester tester = _heuristicTester;
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (tester.IsPrimeCpu(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark]
public int HeuristicGpu()
{
HeuristicPrimeTester tester = _heuristicTester;
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (tester.IsPrimeGpu(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark]
public int HeuristicCombinedGpu()
{
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (HeuristicCombinedPrimeTester.IsPrimeGpu(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark]
public int HeuristicCombinedCpu()
{
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (HeuristicCombinedPrimeTester.IsPrimeCpu(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark]
public int NonHeuristicGpu()
{
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (PrimeTester.IsPrimeGpu(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark(Baseline = true)]
public int NonHeuristicCpu()
{
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (PrimeTester.IsPrime(values[i]))
{
primeCount++;
}
}
return primeCount;
}
[Benchmark]
public int OpenNumericPrimes()
{
ulong[] values = _candidates;
int primeCount = 0;
for (int i = 0; i < values.Length; i++)
{
if (Prime.Numbers.IsPrime(values[i]))
{
primeCount++;
}
}
return primeCount;
}
public static IEnumerable<PrimeBenchmarkCase> GetBenchmarkCases()
{
yield return PrimeBenchmarkCase.FromRange(
"≤100",
"Admissible odd values ≤ 100 (k ≤ 5, not divisible by 5)",
3UL,
100UL,
int.MaxValue);
yield return PrimeBenchmarkCase.FromRange(
"≤4_000_000",
"Sampled admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
3UL,
4_000_000UL,
4_096);
yield return PrimeBenchmarkCase.FromRange(
"≥138_000_000",
"Sampled admissible odd values ≥ 138,000,000 (k ≤ 5, not divisible by 5)",
138_000_001UL,
138_500_000UL,
4_096);
}
public readonly record struct PrimeBenchmarkCase(string Name, string Description, ulong[] Candidates)
{
public override string ToString()
{
return Name;
}
public static PrimeBenchmarkCase FromRange(
string name,
string description,
ulong startInclusive,
ulong endInclusive,
int maxCount)
{
ulong[] values = CollectRange(startInclusive, endInclusive, maxCount);
return new PrimeBenchmarkCase(name, description, values);
}
}
private static ulong[] CollectRange(ulong startInclusive, ulong endInclusive, int maxCount)
{
maxCount = maxCount < 0 ? 0 : maxCount;
if (maxCount == 0)
{
return [];
}
int estimatedLength = (int)Math.Min((ulong)maxCount, endInclusive - startInclusive + 1UL);
ulong[] buffer = new ulong[estimatedLength];
int count = 0;
for (ulong value = startInclusive; value <= endInclusive && count < maxCount; value++)
{
if (!IsAdmissibleMersenneDivisorCandidate(value))
{
continue;
}
if (count == buffer.Length)
{
Array.Resize(ref buffer, buffer.Length * 2);
}
buffer[count++] = value;
}
if (count == buffer.Length)
{
return buffer;
}
ulong[] result = new ulong[count];
Array.Copy(buffer, result, count);
return result;
}
private static bool IsAdmissibleMersenneDivisorCandidate(ulong value)
{
if ((value & 1UL) == 0UL || value % 5UL == 0UL || value <= 1UL)
{
return false;
}
ulong qMinusOne = value - 1UL;
for (ulong k = 1; k <= 5; k++)
{
ulong denominator = k << 1;
if (qMinusOne % denominator != 0UL)
{
continue;
}
ulong p = qMinusOne / denominator;
if (p < 2UL)
{
continue;
}
if (Prime.Numbers.IsPrime(p))
{
return true;
}
}
return false;
}
}
+2 -9
View File
@@ -1,6 +1,5 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,14 +7,8 @@ public static class Program
{
public static void Main(string[] args)
{
if (args.Length > 0)
{
IConfig config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
return;
}
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
// Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
// {
@@ -11,7 +11,4 @@
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -22,7 +22,4 @@
<ItemGroup>
<ProjectReference Include="../EvenPerfectBitScanner/EvenPerfectBitScanner.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
+106 -105
View File
@@ -4,15 +4,47 @@ using System.Reflection;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using EvenPerfectBitScanner;
using EvenPerfectBitScanner.Candidates;
using EvenPerfectBitScanner.Candidates.Transforms;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
namespace EvenPerfectBitScanner.Tests;
[Trait("Category", "Fast")]
public class ProgramTests
{
private static readonly FieldInfo CliArgumentsField = typeof(Program).GetField("_cliArguments", BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly FieldInfo DivisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly CliArguments DefaultCliArguments = CliArguments.Parse(Array.Empty<string>());
public ProgramTests()
{
ResetCliArguments();
}
private static void ConfigureCliArguments(params string[] args)
{
SetCliArguments(CliArguments.Parse(args));
}
private static void SetCliArguments(CliArguments arguments)
{
CliArgumentsField.SetValue(null, arguments);
}
private static void ResetCliArguments()
{
SetCliArguments(DefaultCliArguments);
DivisorField.SetValue(null, UInt128.Zero);
}
[Fact]
public void CountOnes_returns_correct_count()
{
Program.CountOnes(0b101010UL).Should().Be(3UL);
BitOperations.PopCount(0b101010).Should().Be(3);
}
[Theory]
@@ -23,102 +55,91 @@ public class ProgramTests
[InlineData(81UL, false)]
public void IsPrime_identifies_primes_correctly(ulong n, bool expected)
{
new PrimeTester().IsPrime(n, CancellationToken.None).Should().Be(expected);
PrimeTester.IsPrime(n).Should().Be(expected);
}
[Fact]
public void TransformPAdd_moves_to_next_candidate()
{
ulong remainder = 3UL;
Program.TransformPAdd(3UL, ref remainder).Should().Be(5UL);
Program.TransformPAdd(5UL, ref remainder).Should().Be(7UL);
Program.TransformPAdd(7UL, ref remainder).Should().Be(11UL);
bool limit = false;
CandidateAddTransform.Transform(3UL, ref remainder, ref limit).Should().Be(5UL);
CandidateAddTransform.Transform(5UL, ref remainder, ref limit).Should().Be(7UL);
CandidateAddTransform.Transform(7UL, ref remainder, ref limit).Should().Be(11UL);
limit.Should().BeFalse();
}
[Fact]
public void TransformPBit_appends_one_bit_and_skips_to_candidate()
{
ulong remainder = 5UL;
Program.TransformPBit(5UL, ref remainder).Should().Be(13UL);
bool limit = false;
CandidateBitTransform.Transform(5UL, ref remainder, ref limit).Should().Be(13UL);
limit.Should().BeFalse();
}
[Fact]
public void TransformPBit_detects_overflow_and_stops()
{
typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)!
.SetValue(null, false);
bool limit = false;
ulong start = (ulong.MaxValue >> 1) + 1UL;
ulong remainder = start % 6UL;
Program.TransformPBit(start, ref remainder);
bool limit = (bool)typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)!
.GetValue(null)!;
CandidateBitTransform.Transform(start, ref remainder, ref limit);
limit.Should().BeTrue();
typeof(Program).GetField("_limitReached", BindingFlags.NonPublic | BindingFlags.Static)!
.SetValue(null, false);
}
[Fact]
public void Gcd_filter_detects_some_composites()
{
Program.IsCompositeByGcd(15UL).Should().BeTrue();
Program.IsCompositeByGcd(5UL).Should().BeFalse();
15UL.IsCompositeByGcd().Should().BeTrue();
5UL.IsCompositeByGcd().Should().BeFalse();
}
[Fact]
public void Divisor_mode_scans_dynamic_cycles_when_none_specified()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var divisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=divisor");
var testerField = typeof(Program).GetField("_divisorTester", BindingFlags.NonPublic | BindingFlags.Static)!;
var candidatesField = typeof(MersenneNumberDivisorGpuTester).GetField("_divisorCandidates", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, true);
divisorField.SetValue(null, UInt128.Zero);
testerField.SetValue(null, new MersenneNumberDivisorGpuTester());
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
candidatesField.SetValue(null, new (ulong, uint)[] { (7UL, 3U), (23UL, 11U) });
forceCpuProp!.SetValue(null, true);
try
{
Program.IsEvenPerfectCandidate(11UL, 32UL).Should().BeFalse();
Program.IsEvenPerfectCandidate(5UL, 32UL).Should().BeTrue();
Program.IsEvenPerfectCandidate(127UL, 32UL).Should().BeTrue();
Program.IsEvenPerfectCandidate(11UL, 32UL, out _, out _).Should().BeFalse();
Program.IsEvenPerfectCandidate(5UL, 32UL, out _, out _).Should().BeTrue();
Program.IsEvenPerfectCandidate(127UL, 32UL, out _, out _).Should().BeTrue();
}
finally
{
useDivisorField.SetValue(null, false);
testerField.SetValue(null, null);
candidatesField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Divisor_mode_sets_detailedCheck_based_on_exhaustion()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var divisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=divisor");
var testerField = typeof(Program).GetField("_divisorTester", BindingFlags.NonPublic | BindingFlags.Static)!;
var candidatesField = typeof(MersenneNumberDivisorGpuTester).GetField("_divisorCandidates", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, true);
divisorField.SetValue(null, UInt128.Zero);
testerField.SetValue(null, new MersenneNumberDivisorGpuTester());
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
candidatesField.SetValue(null, Array.Empty<(ulong, uint)>());
forceCpuProp!.SetValue(null, true);
@@ -130,30 +151,28 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
testerField.SetValue(null, null);
candidatesField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Divisor_mode_accepts_large_search_limit()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var divisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=divisor");
DivisorField.SetValue(null, (UInt128)7);
var testerField = typeof(Program).GetField("_divisorTester", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, true);
divisorField.SetValue(null, (UInt128)7);
testerField.SetValue(null, new MersenneNumberDivisorGpuTester());
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, true);
try
@@ -164,29 +183,27 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
testerField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Divisor_mode_checks_divisibility_on_gpu()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
var divisorField = typeof(Program).GetField("_divisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=divisor");
DivisorField.SetValue(null, (UInt128)23);
var testerField = typeof(Program).GetField("_divisorTester", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, true);
divisorField.SetValue(null, (UInt128)23);
testerField.SetValue(null, new MersenneNumberDivisorGpuTester());
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -201,28 +218,26 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
divisorField.SetValue(null, UInt128.Zero);
testerField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Residue_mode_checks_mersenne_primes_on_gpu()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(useIncremental: true, useResidue: true, maxK: 1_000UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -237,30 +252,29 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Residue_mode_with_large_divisor_cycle_limits_accepts_known_primes()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useIncremental: true,
useResidue: true,
maxK: 1_024UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -275,11 +289,11 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
@@ -287,21 +301,18 @@ public class ProgramTests
[Trait("Category", "Fast")]
public void Residue_mode_accepts_known_primes_in_parallel()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var gcdFilterField = typeof(Program).GetField("_useGcdFilter", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
gcdFilterField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useIncremental: true,
useResidue: true,
maxK: 5_000_000UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -316,30 +327,29 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
[Fact]
public void Residue_mode_with_default_limits_accepts_large_known_primes()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useIncremental: true,
useResidue: true,
maxK: 5_000_000UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -354,11 +364,11 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
@@ -366,15 +376,12 @@ public class ProgramTests
[MemberData(nameof(KnownMersennePrimeExponents))]
public void IsEvenPerfectCandidate_accepts_known_small_mersenne_primes(ulong exponent)
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var gcdFilterField = typeof(Program).GetField("_useGcdFilter", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
gcdFilterField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useIncremental: true,
useGpuScan: false,
@@ -382,7 +389,7 @@ public class ProgramTests
useResidue: true,
maxK: 1_024UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, exponent, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, exponent, true), trackAllValues: true));
forceCpuProp!.SetValue(null, true);
try
@@ -392,12 +399,11 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
gcdFilterField.SetValue(null, false);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
@@ -407,15 +413,12 @@ public class ProgramTests
ulong exponent,
MersenneGpuCandidateConfig configuration)
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var gcdFilterField = typeof(Program).GetField("_useGcdFilter", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
gcdFilterField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(
useIncremental: false,
kernelType: configuration.KernelType,
@@ -424,7 +427,7 @@ public class ProgramTests
useResidue: true,
maxK: configuration.ResidueMaxK), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, exponent, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, exponent, true), trackAllValues: true));
forceCpuProp!.SetValue(null, false);
try
@@ -434,12 +437,11 @@ public class ProgramTests
}
finally
{
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
gcdFilterField.SetValue(null, false);
CandidatesCalculator.ResetResidueTrackers();
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
GpuContextPool.DisposeAll();
}
}
@@ -499,17 +501,16 @@ public class ProgramTests
[Fact]
public void Composite_candidates_flagged_for_residue_output_skip()
{
var useDivisorField = typeof(Program).GetField("_useDivisor", BindingFlags.NonPublic | BindingFlags.Static)!;
ConfigureCliArguments("--mersenne=residue");
var mersenneField = typeof(Program).GetField("MersenneTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var primeField = typeof(Program).GetField("PrimeTesters", BindingFlags.NonPublic | BindingFlags.Static)!;
var residueField = typeof(Program).GetField("PResidue", BindingFlags.NonPublic | BindingFlags.Static)!;
var compositeField = typeof(Program).GetField("_lastCompositeP", BindingFlags.NonPublic | BindingFlags.Static)!;
var forceCpuProp = typeof(GpuContextPool).GetProperty("ForceCpu");
useDivisorField.SetValue(null, false);
mersenneField.SetValue(null, new ThreadLocal<MersenneNumberTester>(() => new MersenneNumberTester(useIncremental: true, useResidue: false, maxK: 1_000UL), trackAllValues: true));
primeField.SetValue(null, new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true));
residueField.SetValue(null, new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
CandidatesCalculator.OverrideResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, 2UL, true), trackAllValues: true));
forceCpuProp!.SetValue(null, true);
try
@@ -529,9 +530,10 @@ public class ProgramTests
{
mersenneField.SetValue(null, null);
primeField.SetValue(null, null);
residueField.SetValue(null, null);
CandidatesCalculator.ResetResidueTrackers();
compositeField.SetValue(null, false);
forceCpuProp!.SetValue(null, false);
ResetCliArguments();
}
}
@@ -540,7 +542,7 @@ public class ProgramTests
public void Main_displays_help_when_requested()
{
var main = typeof(Program).GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static)!;
var writer = new StringWriter();
using var writer = new StringWriter();
TextWriter original = Console.Out;
Console.SetOut(writer);
@@ -554,7 +556,6 @@ public class ProgramTests
}
string output = writer.ToString();
writer.Dispose();
output.Should().Contain("Usage:");
output.Should().Contain("--mersenne-device=cpu|gpu");
output.Should().Contain("--primes-device=cpu|gpu");
@@ -1,8 +1,13 @@
using System.Reflection;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using EvenPerfectBitScanner.IO;
namespace EvenPerfectBitScanner.Tests;
[Trait("Category", "Fast")]
@@ -11,41 +16,26 @@ public class ResultsFileNameTests
[Fact]
public void BuildResultsFileName_contains_reduction_and_devices()
{
var programType = typeof(global::EvenPerfectBitScanner.Program);
var method = programType.GetMethod(
"BuildResultsFileName",
BindingFlags.NonPublic | BindingFlags.Static,
binder: null,
types: new[]
{
typeof(bool), typeof(int), typeof(int), typeof(global::PerfectNumbers.Core.GpuKernelType),
typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(bool), typeof(NttBackend), typeof(int), typeof(int), typeof(int), typeof(ulong), typeof(ModReductionMode), typeof(string), typeof(string), typeof(string)
},
modifiers: null)!;
string name = (string)method.Invoke(null, new object[]
{
true, // bitInc
8, // threads
1, // block
global::PerfectNumbers.Core.GpuKernelType.Incremental,
false, // useLucasFlag
false, // useDivisorFlag
false, // useByDivisorFlag
true, // mersenneOnGpu (unused inside name content, devices passed explicitly below)
false, // useOrder
false, // useModWorkaround
false, // useGcd
NttBackend.Staged,
1, // gpuPrimeThreads
32, // llSlice
1024, // gpuScanBatch
1000UL, // warmupLimit
ModReductionMode.Barrett128,
"gpu", // mersenneDevice
"gpu", // primesDevice
"gpu" // orderDevice
})!;
string name = CalculationResultsFile.BuildFileName(
bitInc: true,
threads: 8,
block: 1,
kernelType: GpuKernelType.Incremental,
useLucasFlag: false,
useDivisorFlag: false,
useByDivisorFlag: false,
mersenneOnGpu: true,
useOrder: false,
useGcd: false,
nttBackend: NttBackend.Staged,
gpuPrimeThreads: 1,
llSlice: 32,
gpuScanBatch: 1024,
warmupLimit: 1000UL,
reduction: ModReductionMode.Barrett128,
mersenneDevice: "gpu",
primesDevice: "gpu",
orderDevice: "gpu");
name.Should().Contain("ntt-staged");
name.Should().Contain("red-barrett128");
@@ -53,4 +43,63 @@ public class ResultsFileNameTests
name.Should().Contain("primesdev-gpu");
name.Should().Contain("orderdev-gpu");
}
[Fact]
public void LoadCandidatesWithinRange_skips_comments_and_applies_limit()
{
string path = Path.GetTempFileName();
try
{
File.WriteAllLines(path, new[]
{
"# header",
"5 7",
string.Empty,
"11",
"17",
"19"
});
List<ulong> candidates = CalculationResultsFile.LoadCandidatesWithinRange(path, (UInt128)13, maxPrimeConfigured: true, out int skipped);
candidates.Should().Equal(5UL, 7UL, 11UL);
skipped.Should().Be(2);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void EnumerateCandidates_reads_valid_rows_and_skips_invalid_entries()
{
string path = Path.GetTempFileName();
try
{
File.WriteAllLines(path, new[]
{
"p,searched,detailed,passed",
"5,true,true,true",
string.Empty,
"11,false,false,true",
"invalid",
"13,true,true,false"
});
List<(ulong P, bool Detailed, bool Passed)> rows = new();
CalculationResultsFile.EnumerateCandidates(path, (p, detailed, passed) => rows.Add((p, detailed, passed)));
rows.Should().Equal(new (ulong, bool, bool)[]
{
(5UL, true, true),
(11UL, false, true),
(13UL, true, false)
});
}
finally
{
File.Delete(path);
}
}
}
@@ -0,0 +1,340 @@
using System.Buffers.Text;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner;
internal static class CalculationResultHandler
{
private const string ResultsHeader = "p,searchedMersenne,detailedCheck,passedAllTests";
private const string PrimeFoundSuffix = " (FOUND VALID CANDIDATES)";
internal const int DefaultWriteBatchSize = 100;
private static readonly UTF8Encoding Utf8Encoding = new(false, true);
private static readonly object Sync = new();
private static readonly object FileWriteSync = new();
private static StringBuilder? _outputBuilder;
private static TextFileWriter? _resultsWriter;
private static int _consoleCounter;
private static int _writeIndex;
private static int _writeBatchSize;
private static string _resultsFileName = string.Empty;
private static int _primeCount;
private static bool _primeFoundAfterInit;
internal static string ResultsFileName => _resultsFileName;
internal static void Initialize(string resultsFileName, int writeBatchSize)
{
Dispose();
_outputBuilder = null;
_resultsWriter = null;
_consoleCounter = 0;
_writeIndex = 0;
_writeBatchSize = writeBatchSize > 0 ? writeBatchSize : DefaultWriteBatchSize;
_resultsFileName = resultsFileName;
_primeCount = 0;
_primeFoundAfterInit = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void InitializeOutputBuffer()
{
// Program flow ensures Initialize(...) runs before this call. Do not add null guards here.
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ReleaseOutputBuffer()
{
// Program flow ensures InitializeOutputBuffer() populated the builder. Do not add null checks here.
StringBuilder builder = _outputBuilder!;
// Program flow guarantees the output builder is populated when releasing. Do not add guard checks here.
_outputBuilder = null;
_writeIndex = 0;
StringBuilderPool.Return(builder);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void CreateResultsFileWithHeader()
{
// Program flow guarantees FileWriteSync is available here. Do not add guard checks for the lock object.
lock (FileWriteSync)
{
_resultsWriter?.Dispose();
var writer = new TextFileWriter(_resultsFileName, FileMode.Create);
writer.WriteLine(ResultsHeader.AsSpan());
_resultsWriter = writer;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void RegisterExistingResult(bool detailedCheck, bool passedAllTests)
{
if (!detailedCheck || !passedAllTests)
{
return;
}
int newCount = _primeCount + 1;
_primeCount = newCount;
if (newCount >= 2)
{
_primeFoundAfterInit = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void HandleResult(ulong currentP, bool searchedMersenne, bool detailedCheck, bool passedAllTests, bool lastWasComposite)
{
// Program flow ensures Initialize(...) and InitializeOutputBuffer() ran before this call. Do not add initialization guards here.
Span<byte> stackBuffer = stackalloc byte[128];
byte[]? rentedBuffer = null;
Span<byte> utf8Span = stackBuffer;
int byteCount = TryFormatResultLine(utf8Span, currentP, searchedMersenne, detailedCheck, passedAllTests);
if (byteCount < 0)
{
rentedBuffer = ThreadStaticPools.BytePool.Rent(256);
utf8Span = rentedBuffer;
byteCount = TryFormatResultLine(utf8Span, currentP, searchedMersenne, detailedCheck, passedAllTests);
if (byteCount < 0)
{
ThreadStaticPools.BytePool.Return(rentedBuffer);
throw new InvalidOperationException("Result buffer too small.");
}
}
ReadOnlySpan<byte> utf8Line = utf8Span[..byteCount];
Span<char> charBuffer = stackalloc char[128];
int charCount = Utf8Encoding.GetChars(utf8Line, charBuffer);
ReadOnlySpan<char> recordSpan = charBuffer[..charCount];
bool primeCandidate = UpdatePrimeTracking(passedAllTests, lastWasComposite);
bool printToConsole = TryAcquireConsoleSlot(primeCandidate, out bool primeFlag);
StringBuilder? builderToFlush = AppendToOutputBuffer(recordSpan);
if (builderToFlush is not null)
{
FlushBuffer(builderToFlush);
}
if (printToConsole)
{
ReadOnlySpan<char> consoleSpan = recordSpan[..^1];
TextWriter consoleWriter = Console.Out;
if (primeFlag)
{
consoleWriter.Write(consoleSpan);
consoleWriter.Write(PrimeFoundSuffix);
consoleWriter.WriteLine();
}
else
{
consoleWriter.WriteLine(consoleSpan);
}
}
if (rentedBuffer is not null)
{
ThreadStaticPools.BytePool.Return(rentedBuffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void FlushBuffer()
{
// Program flow ensures InitializeOutputBuffer() prepared the buffer. Do not add null checks here.
StringBuilder? builderToFlush;
lock (Sync)
{
if (_writeIndex == 0 || _outputBuilder is null || _outputBuilder.Length == 0)
{
return;
}
builderToFlush = _outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
FlushBuffer(builderToFlush!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Dispose()
{
// Program flow guarantees FileWriteSync is ready when disposing. Do not add guard checks for the lock object.
lock (FileWriteSync)
{
_resultsWriter?.Dispose();
_resultsWriter = null;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryAcquireConsoleSlot(bool primeCandidate, out bool primeFlag)
{
int consoleInterval = PerfectNumberConstants.ConsoleInterval;
int counterValue = Interlocked.Increment(ref _consoleCounter);
if (counterValue >= consoleInterval)
{
int previous = Interlocked.Exchange(ref _consoleCounter, 0);
if (previous >= consoleInterval)
{
primeFlag = primeCandidate;
return true;
}
}
primeFlag = false;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StringBuilder? AppendToOutputBuffer(ReadOnlySpan<char> text)
{
StringBuilder? builderToFlush = null;
lock (Sync)
{
StringBuilder outputBuilder = _outputBuilder!;
_ = outputBuilder.Append(text);
int nextIndex = _writeIndex + 1;
if (nextIndex >= _writeBatchSize)
{
builderToFlush = outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
else
{
_writeIndex = nextIndex;
}
}
return builderToFlush;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int TryFormatResultLine(Span<byte> destination, ulong currentP, bool searchedMersenne, bool detailedCheck, bool passedAllTests)
{
int offset = 0;
if (!Utf8Formatter.TryFormat(currentP, destination[offset..], out int written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(searchedMersenne, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(detailedCheck, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(passedAllTests, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)'\n';
return offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void FlushBuffer(StringBuilder builderToFlush)
{
// Program flow guarantees FileWriteSync is ready here as well. Do not add guard checks for the lock object.
try
{
lock (FileWriteSync)
{
TextFileWriter writer = EnsureResultsWriter();
writer.Write(builderToFlush);
}
}
finally
{
builderToFlush.Clear();
StringBuilderPool.Return(builderToFlush);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TextFileWriter EnsureResultsWriter()
{
TextFileWriter? writer = _resultsWriter;
if (writer is not null)
{
return writer;
}
writer = new TextFileWriter(_resultsFileName, FileMode.Append);
_resultsWriter = writer;
return writer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool UpdatePrimeTracking(bool passedAllTests, bool lastWasComposite)
{
if (passedAllTests)
{
int newCount = Interlocked.Increment(ref _primeCount);
if (newCount >= 2)
{
Volatile.Write(ref _primeFoundAfterInit, true);
}
}
return passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
}
}
@@ -0,0 +1,78 @@
using System.Globalization;
using System.IO;
namespace EvenPerfectBitScanner.Candidates;
internal static class CalculationTestTime
{
private const string TestTimeFileName = "even_perfect_test_time.txt";
internal static void Report(TimeSpan elapsed, string? resultsDirectory)
{
double elapsedSeconds = elapsed.TotalSeconds;
double elapsedMilliseconds = elapsed.TotalMilliseconds;
Console.WriteLine($"Test elapsed time: {elapsedSeconds:F3} s");
string filePath = GetFilePath(resultsDirectory);
bool hasPrevious = TryRead(filePath, out double previousMilliseconds);
string elapsedText = elapsedMilliseconds.ToString("F3", CultureInfo.InvariantCulture);
if (!hasPrevious)
{
Write(filePath, elapsedMilliseconds);
Console.WriteLine($"FIRST TIME: {elapsedText} ms");
return;
}
string previousText = previousMilliseconds.ToString("F3", CultureInfo.InvariantCulture);
if (elapsedMilliseconds < previousMilliseconds)
{
Write(filePath, elapsedMilliseconds);
Console.WriteLine($"BETTER TIME: {elapsedText} ms (previous {previousText} ms)");
}
else
{
Console.WriteLine($"WORSE TIME: {elapsedText} ms (best {previousText} ms)");
}
}
private static string GetFilePath(string? resultsDirectory)
{
if (string.IsNullOrEmpty(resultsDirectory))
{
return TestTimeFileName;
}
return Path.Combine(resultsDirectory!, TestTimeFileName);
}
private static bool TryRead(string filePath, out double milliseconds)
{
if (!File.Exists(filePath))
{
milliseconds = 0.0;
return false;
}
string content = File.ReadAllText(filePath).Trim();
if (content.Length == 0)
{
milliseconds = 0.0;
return false;
}
return double.TryParse(content, NumberStyles.Float, CultureInfo.InvariantCulture, out milliseconds);
}
private static void Write(string filePath, double milliseconds)
{
string? directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string formatted = milliseconds.ToString("F6", CultureInfo.InvariantCulture);
File.WriteAllText(filePath, formatted);
}
}
@@ -0,0 +1,211 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Open.Numeric.Primes;
using PerfectNumbers.Core;
using EvenPerfectBitScanner.Candidates.Transforms;
namespace EvenPerfectBitScanner.Candidates;
internal static class CandidatesCalculator
{
private static readonly Optimized PrimeIterator = new();
private static long _state;
private static PrimeTransformMode _transformMode;
private static ThreadLocal<ModResidueTracker>? _residueTrackers;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Configure(PrimeTransformMode transformMode)
{
_transformMode = transformMode;
}
internal static void Initialize(ulong initialNumber)
{
SetResidueTrackers(new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, initialNumber: initialNumber, initialized: true), trackAllValues: true));
}
internal static void OverrideResidueTrackers(ThreadLocal<ModResidueTracker> trackers)
{
if (trackers is null)
{
throw new ArgumentNullException(nameof(trackers));
}
SetResidueTrackers(trackers);
}
internal static void ResetResidueTrackers()
{
ThreadLocal<ModResidueTracker>? previous = _residueTrackers;
_residueTrackers = null;
previous?.Dispose();
}
private static void SetResidueTrackers(ThreadLocal<ModResidueTracker> trackers)
{
ThreadLocal<ModResidueTracker>? previous = _residueTrackers;
_residueTrackers = trackers;
previous?.Dispose();
}
private static ModResidueTracker AcquireResidueTracker()
{
var trackers = _residueTrackers ?? throw new InvalidOperationException("CandidatesCalculator.Initialize must be called before residue checks.");
return trackers.Value!;
}
internal static bool IsCompositeByResidues(ulong p)
{
ModResidueTracker tracker = AcquireResidueTracker();
// Use ModResidueTracker with a small set of primes to pre-filter composite p.
tracker.BeginMerge(p);
// TODO: Integrate the divisor-cycle cache here so the small-prime sweep reuses precomputed remainders instead of
// running MergeOrAppend for every candidate and missing the cycle-accelerated early exits.
// Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p)
var primes = PrimesGenerator.SmallPrimes;
var primesPow2 = PrimesGenerator.SmallPrimesPow2;
int len = primes.Length;
for (int primeIndex = 0; primeIndex < len; primeIndex++)
{
if (primesPow2[primeIndex] > p)
{
break;
}
if (tracker.MergeOrAppend(p, primes[primeIndex], out bool divisible) && divisible)
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void InitializeState(ulong startPrime, ulong remainder)
{
Volatile.Write(ref _state, ((long)startPrime << 3) | (long)remainder);
}
internal static List<ulong> BuildTestPrimeCandidates(int targetCount)
{
if (targetCount <= 0)
{
return [];
}
List<ulong> candidates = new(targetCount);
ulong previous = 29UL;
int remaining = targetCount;
ulong nextPrime;
while (remaining > 0)
{
try
{
nextPrime = PrimeIterator.Next(in previous);
}
catch (InvalidOperationException)
{
break;
}
if (nextPrime < 31UL)
{
previous = nextPrime;
continue;
}
candidates.Add(nextPrime);
remaining--;
previous = nextPrime;
}
if (remaining > 0)
{
Console.WriteLine("Unable to populate the requested number of test primes before reaching the 64-bit limit.");
}
return candidates;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong AdvancePrime(ulong value, ref ulong remainder, ref bool limitReached)
{
return _transformMode switch
{
PrimeTransformMode.Bit => CandidateBitTransform.Transform(value, ref remainder, ref limitReached),
PrimeTransformMode.Add => CandidateAddTransform.Transform(value, ref remainder, ref limitReached),
_ => CandidateAddPrimesTransform.Transform(value, ref remainder, ref limitReached),
};
}
internal static int ReserveBlock(ulong[] buffer, int blockSize, ref bool limitReached)
{
Span<ulong> bufferSpan = new(buffer);
while (true)
{
long stateSnapshot = Volatile.Read(ref _state);
ulong p = (ulong)stateSnapshot >> 3;
ulong remainder = ((ulong)stateSnapshot) & 7UL;
int count = 0;
switch (_transformMode)
{
case PrimeTransformMode.Bit:
while (count < blockSize && !Volatile.Read(ref limitReached))
{
bufferSpan[count++] = p;
p = CandidateBitTransform.Transform(p, ref remainder, ref limitReached);
}
break;
case PrimeTransformMode.Add:
while (count < blockSize && !Volatile.Read(ref limitReached))
{
bufferSpan[count++] = p;
p = CandidateAddTransform.Transform(p, ref remainder, ref limitReached);
}
break;
default:
while (count < blockSize && !Volatile.Read(ref limitReached))
{
bufferSpan[count++] = p;
p = CandidateAddPrimesTransform.Transform(p, ref remainder, ref limitReached);
}
break;
}
if (count == 0)
{
return 0;
}
long newState = ((long)p << 3) | (long)remainder;
long original = Interlocked.CompareExchange(ref _state, newState, stateSnapshot);
if (original == stateSnapshot)
{
return count;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong GetNextAddDiff(this ulong remainder)
{
return remainder switch
{
0UL => 1UL,
1UL => 4UL,
2UL => 3UL,
3UL => 2UL,
4UL => 1UL,
_ => 2UL,
};
}
}
@@ -0,0 +1,8 @@
namespace EvenPerfectBitScanner.Candidates;
internal enum PrimeTransformMode
{
Bit,
Add,
AddPrimes,
}
@@ -0,0 +1,79 @@
using System.Runtime.CompilerServices;
using System.Threading;
using EvenPerfectBitScanner.Candidates;
using Open.Numeric.Primes;
namespace EvenPerfectBitScanner.Candidates.Transforms;
internal static class CandidateAddPrimesTransform
{
private static readonly Optimized PrimeIterator = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong Transform(ulong value, ref ulong remainder, ref bool limitReached)
{
ulong originalRemainder = remainder;
ulong addRemainder = remainder;
ulong candidate = value;
ulong primeCandidate = value;
bool advanceAdd = true;
bool advancePrime = true;
ulong diff;
ulong nextPrime;
while (true)
{
if (advanceAdd)
{
diff = addRemainder.GetNextAddDiff();
if (candidate > ulong.MaxValue - diff)
{
Volatile.Write(ref limitReached, true);
remainder = originalRemainder;
return candidate;
}
candidate += diff;
addRemainder += diff;
// Skip the Mod6 lookup: benchmarked `%` stays ahead for these prime increments.
// benchmarked fastest remainder updates instead of looping subtraction.
while (addRemainder >= 6UL)
{
addRemainder -= 6UL;
}
}
if (advancePrime)
{
try
{
nextPrime = PrimeIterator.Next(in primeCandidate);
}
catch (InvalidOperationException)
{
Volatile.Write(ref limitReached, true);
remainder = originalRemainder;
return value;
}
if (nextPrime <= primeCandidate)
{
Volatile.Write(ref limitReached, true);
remainder = originalRemainder;
return value;
}
primeCandidate = nextPrime;
}
if (candidate == primeCandidate)
{
remainder = addRemainder;
return candidate;
}
advanceAdd = candidate < primeCandidate;
advancePrime = candidate > primeCandidate;
}
}
}
@@ -0,0 +1,30 @@
using System.Runtime.CompilerServices;
using System.Threading;
using EvenPerfectBitScanner.Candidates;
namespace EvenPerfectBitScanner.Candidates.Transforms;
internal static class CandidateAddTransform
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong Transform(ulong value, ref ulong remainder, ref bool limitReached)
{
ulong next = value;
ulong diff = remainder.GetNextAddDiff();
if (next > ulong.MaxValue - diff)
{
Volatile.Write(ref limitReached, true);
return next;
}
remainder += diff;
// Retain direct modulo because the Mod6 helper underperforms on 64-bit operands.
while (remainder >= 6UL)
{
remainder -= 6UL;
}
return next + diff;
}
}
@@ -0,0 +1,51 @@
using System.Runtime.CompilerServices;
using System.Threading;
namespace EvenPerfectBitScanner.Candidates.Transforms;
internal static class CandidateBitTransform
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong Transform(ulong value, ref ulong remainder, ref bool limitReached)
{
ulong original = value;
if (value > (ulong.MaxValue >> 1))
{
Volatile.Write(ref limitReached, true);
return value;
}
ulong next = (value << 1) | 1UL;
remainder = (remainder << 1) + 1UL;
// Mod6 lookup loses to `%` in the benches; stick with subtraction + modulo for correctness and speed.
while (remainder >= 6UL)
{
remainder -= 6UL;
}
value = remainder switch
{
0UL => 1UL,
1UL => 0UL,
2UL => 5UL,
3UL => 4UL,
4UL => 3UL,
_ => 2UL,
}; // 'value' now holds diff
if (next > ulong.MaxValue - value)
{
Volatile.Write(ref limitReached, true);
return original;
}
remainder += value;
// `% 6` remains faster per Mod6ComparisonBenchmarks; keep this modulo fold and continue using subtraction.
while (remainder >= 6UL)
{
remainder -= 6UL;
}
return next + value;
}
}
+616
View File
@@ -0,0 +1,616 @@
using System;
using System.Globalization;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner;
internal readonly struct CliArguments
{
private const string DefaultCyclesPath = "divisor_cycles.bin";
internal readonly ulong StartPrime;
internal readonly bool StartPrimeProvided;
internal readonly UInt128 MaxPrimeLimit;
internal readonly bool MaxPrimeConfigured;
internal readonly int ThreadCount;
internal readonly int BlockSize;
internal readonly int GpuPrimeThreads;
internal readonly int GpuPrimeBatch;
internal readonly GpuKernelType KernelType;
internal readonly bool UseOrder;
internal readonly bool ShowHelp;
internal readonly bool UseBitTransform;
internal readonly bool UseLucas;
internal readonly bool UseResidue;
internal readonly bool UseDivisor;
internal readonly bool UseByDivisor;
internal readonly bool UseGcdFilter;
internal readonly bool TestMode;
internal readonly bool UseGpuCycles;
internal readonly bool UseMersenneOnGpu;
internal readonly bool UseOrderOnGpu;
internal readonly int ScanBatchSize;
internal readonly int SliceSize;
internal readonly ulong ResidueKMax;
internal readonly string FilterFile;
internal readonly bool ForcePrimeKernelsOnCpu;
internal readonly ModReductionMode ModReductionMode;
internal readonly NttBackend NttBackend;
internal readonly string? ResultsDirectory;
internal readonly string? ResultsPrefix;
internal readonly int WriteBatchSize;
internal readonly string CyclesPath;
internal readonly bool ContinueCyclesGeneration;
internal readonly int CyclesBatchSize;
internal readonly ulong DivisorCyclesSearchLimit;
internal readonly ulong? OrderWarmupLimitOverride;
internal readonly string? RleBlacklistPath;
internal readonly ulong RleHardMaxP;
internal readonly bool RleOnlyLast7;
internal readonly double ZeroFractionHard;
internal readonly double ZeroFractionConjecture;
internal readonly int MaxZeroConjecture;
private CliArguments(
ulong startPrime,
bool startPrimeProvided,
UInt128 maxPrimeLimit,
bool maxPrimeConfigured,
int threadCount,
int blockSize,
int gpuPrimeThreads,
int gpuPrimeBatch,
GpuKernelType kernelType,
bool useOrder,
bool showHelp,
bool useBitTransform,
bool useLucas,
bool useResidue,
bool useDivisor,
bool useByDivisor,
bool useGcdFilter,
bool testMode,
bool useGpuCycles,
bool useMersenneOnGpu,
bool useOrderOnGpu,
int scanBatchSize,
int sliceSize,
ulong residueKMax,
string filterFile,
bool forcePrimeKernelsOnCpu,
ModReductionMode modReductionMode,
NttBackend nttBackend,
string? resultsDirectory,
string? resultsPrefix,
int writeBatchSize,
string cyclesPath,
bool continueCyclesGeneration,
int cyclesBatchSize,
ulong divisorCyclesSearchLimit,
ulong? orderWarmupLimitOverride,
string? rleBlacklistPath,
ulong rleHardMaxP,
bool rleOnlyLast7,
double zeroFractionHard,
double zeroFractionConjecture,
int maxZeroConjecture)
{
StartPrime = startPrime;
StartPrimeProvided = startPrimeProvided;
MaxPrimeLimit = maxPrimeLimit;
MaxPrimeConfigured = maxPrimeConfigured;
ThreadCount = threadCount;
BlockSize = blockSize;
GpuPrimeThreads = gpuPrimeThreads;
GpuPrimeBatch = gpuPrimeBatch;
KernelType = kernelType;
UseOrder = useOrder;
ShowHelp = showHelp;
UseBitTransform = useBitTransform;
UseLucas = useLucas;
UseResidue = useResidue;
UseDivisor = useDivisor;
UseByDivisor = useByDivisor;
UseGcdFilter = useGcdFilter;
TestMode = testMode;
UseGpuCycles = useGpuCycles;
UseMersenneOnGpu = useMersenneOnGpu;
UseOrderOnGpu = useOrderOnGpu;
ScanBatchSize = scanBatchSize;
SliceSize = sliceSize;
ResidueKMax = residueKMax;
FilterFile = filterFile;
ForcePrimeKernelsOnCpu = forcePrimeKernelsOnCpu;
ModReductionMode = modReductionMode;
NttBackend = nttBackend;
ResultsDirectory = resultsDirectory;
ResultsPrefix = resultsPrefix;
WriteBatchSize = writeBatchSize;
CyclesPath = cyclesPath;
ContinueCyclesGeneration = continueCyclesGeneration;
CyclesBatchSize = cyclesBatchSize;
DivisorCyclesSearchLimit = divisorCyclesSearchLimit;
OrderWarmupLimitOverride = orderWarmupLimitOverride;
RleBlacklistPath = rleBlacklistPath;
RleHardMaxP = rleHardMaxP;
RleOnlyLast7 = rleOnlyLast7;
ZeroFractionHard = zeroFractionHard;
ZeroFractionConjecture = zeroFractionConjecture;
MaxZeroConjecture = maxZeroConjecture;
}
internal bool UseFilter => !TestMode && !string.IsNullOrEmpty(FilterFile);
internal static CliArguments Parse(string[] args)
{
ulong startPrime = PerfectNumberConstants.BiggestKnownEvenPerfectP;
bool startPrimeProvided = false;
UInt128 maxPrimeLimit = UInt128.MaxValue;
bool maxPrimeConfigured = false;
int threadCount = Environment.ProcessorCount;
int blockSize = 1;
int gpuPrimeThreads = 1;
int gpuPrimeBatch = 262_144;
GpuKernelType kernelType = GpuKernelType.Incremental;
bool useOrder = false;
bool showHelp = false;
bool useBitTransform = false;
bool useLucas = false;
bool useResidue = false;
bool useDivisor = false;
bool useByDivisor = false;
bool useGcdFilter = false;
bool testMode = false;
bool useGpuCycles = true;
bool useMersenneOnGpu = true;
bool useOrderOnGpu = true;
int scanBatchSize = 2_097_152;
int sliceSize = 32;
ulong residueKMax = 5_000_000UL;
string filterFile = string.Empty;
bool forcePrimeKernelsOnCpu = false;
ModReductionMode modReductionMode = ModReductionMode.Auto;
NttBackend nttBackend = NttBackend.Reference;
string? resultsDirectory = null;
string? resultsPrefix = null;
int writeBatchSize = CalculationResultHandler.DefaultWriteBatchSize;
string cyclesPath = DefaultCyclesPath;
bool continueCyclesGeneration = false;
int cyclesBatchSize = 512;
ulong divisorCyclesSearchLimit = PerfectNumberConstants.ExtraDivisorCycleSearchLimit;
ulong? orderWarmupLimitOverride = null;
string? rleBlacklistPath = null;
ulong rleHardMaxP = ulong.MaxValue;
bool rleOnlyLast7 = true;
double zeroFractionHard = -1.0;
double zeroFractionConjecture = -1.0;
int maxZeroConjecture = -1;
foreach (string argument in args)
{
if (argument.Equals("--?", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("-?", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("--help", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("-help", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("/?", StringComparison.OrdinalIgnoreCase))
{
showHelp = true;
break;
}
if (argument.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> primeValue = argument.AsSpan("--prime=".Length);
startPrime = Utf8CliParser.ParseUInt64(primeValue);
startPrimeProvided = true;
continue;
}
if (argument.StartsWith("--max-prime=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--max-prime=".Length);
if (UInt128.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out UInt128 parsedMaxPrime))
{
maxPrimeLimit = parsedMaxPrime;
maxPrimeConfigured = true;
}
else
{
Console.WriteLine("Invalid value for --max-prime.");
return default;
}
continue;
}
if (argument.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = true;
continue;
}
if (argument.Equals("--increment=add", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = false;
continue;
}
if (argument.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
{
threadCount = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--threads=".Length)));
continue;
}
if (argument.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--mersenne=".Length);
if (value.Equals("pow2mod", StringComparison.OrdinalIgnoreCase))
{
kernelType = GpuKernelType.Pow2Mod;
}
else if (value.Equals("lucas", StringComparison.OrdinalIgnoreCase))
{
useLucas = true;
}
else if (value.Equals("residue", StringComparison.OrdinalIgnoreCase))
{
useResidue = true;
useLucas = false;
}
else if (value.Equals("divisor", StringComparison.OrdinalIgnoreCase))
{
useDivisor = true;
useLucas = false;
useResidue = false;
}
else if (value.Equals("bydivisor", StringComparison.OrdinalIgnoreCase))
{
useByDivisor = true;
useLucas = false;
useResidue = false;
useDivisor = false;
}
else
{
kernelType = GpuKernelType.Incremental;
}
continue;
}
if (argument.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan("--divisor-cycles-limit=".Length), out ulong parsedLimit))
{
divisorCyclesSearchLimit = parsedLimit;
}
continue;
}
if (argument.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan("--residue-max-k=".Length), out ulong parsedResidueMax))
{
residueKMax = parsedResidueMax;
}
continue;
}
if (argument.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase))
{
useMersenneOnGpu = !argument.AsSpan("--mersenne-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.Equals("--test", StringComparison.OrdinalIgnoreCase))
{
testMode = true;
continue;
}
if (argument.Equals("--use-order", StringComparison.OrdinalIgnoreCase))
{
useOrder = true;
continue;
}
if (argument.StartsWith("--order-warmup-limit=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan("--order-warmup-limit=".Length), out ulong parsedLimit))
{
orderWarmupLimitOverride = parsedLimit;
}
continue;
}
if (argument.Equals("--gcd-filter", StringComparison.OrdinalIgnoreCase))
{
useGcdFilter = true;
continue;
}
if (argument.StartsWith("--ntt=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--ntt=".Length);
nttBackend = value.Equals("staged", StringComparison.OrdinalIgnoreCase) ? NttBackend.Staged : NttBackend.Reference;
continue;
}
if (argument.StartsWith("--mod-reduction=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--mod-reduction=".Length);
if (value.Equals("mont64", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.Mont64;
}
else if (value.Equals("barrett128", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.Barrett128;
}
else if (value.Equals("uint128", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.GpuUInt128;
}
else
{
modReductionMode = ModReductionMode.Auto;
}
continue;
}
if (argument.StartsWith("--primes-device=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan("--primes-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase))
{
useOrderOnGpu = !argument.AsSpan("--order-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--rle-blacklist=", StringComparison.OrdinalIgnoreCase))
{
rleBlacklistPath = argument["--rle-blacklist=".Length..];
continue;
}
if (argument.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan("--rle-hard-max=".Length), out ulong parsedMax))
{
rleHardMaxP = parsedMax;
}
continue;
}
if (argument.StartsWith("--rle-only-last7=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--rle-only-last7=".Length);
rleOnlyLast7 = !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseDouble(argument.AsSpan("--zero-hard=".Length), out double zeroFraction))
{
zeroFractionHard = zeroFraction;
}
continue;
}
if (argument.StartsWith("--zero-conj=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan("--zero-conj=".Length);
int colon = value.IndexOf(':');
if (colon > 0)
{
if (Utf8CliParser.TryParseDouble(value[..colon], out double zeroFrac) &&
Utf8CliParser.TryParseInt32(value[(colon + 1)..], out int maxZero))
{
zeroFractionConjecture = zeroFrac;
maxZeroConjecture = maxZero;
}
}
continue;
}
if (argument.StartsWith("--lucas=", StringComparison.OrdinalIgnoreCase))
{
useMersenneOnGpu = !argument.AsSpan("--lucas=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--primes=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan("--primes=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--gpu-kernels=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan("--gpu-kernels=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--accelerator=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan("--accelerator=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--results-dir=", StringComparison.OrdinalIgnoreCase))
{
resultsDirectory = argument["--results-dir=".Length..];
continue;
}
if (argument.StartsWith("--results-prefix=", StringComparison.OrdinalIgnoreCase))
{
resultsPrefix = argument["--results-prefix=".Length..];
continue;
}
if (argument.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeThreads = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--gpu-prime-threads=".Length)));
continue;
}
if (argument.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeBatch = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--gpu-prime-batch=".Length)));
continue;
}
if (argument.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
{
sliceSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--ll-slice=".Length)));
continue;
}
if (argument.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
{
scanBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--gpu-scan-batch=".Length)));
continue;
}
if (argument.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
{
blockSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--block-size=".Length)));
continue;
}
if (argument.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
{
filterFile = argument["--filter-p=".Length..];
continue;
}
if (argument.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
{
writeBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--write-batch-size=".Length)));
continue;
}
if (argument.StartsWith("--divisor-cycles=", StringComparison.OrdinalIgnoreCase))
{
cyclesPath = argument["--divisor-cycles=".Length..];
continue;
}
if (argument.StartsWith("--divisor-cycles-device=", StringComparison.OrdinalIgnoreCase))
{
useGpuCycles = !argument.AsSpan("--divisor-cycles-device=".Length).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
{
cyclesBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan("--divisor-cycles-batch=".Length)));
continue;
}
if (argument.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
{
continueCyclesGeneration = true;
}
}
return new CliArguments(
startPrime,
startPrimeProvided,
maxPrimeLimit,
maxPrimeConfigured,
threadCount,
blockSize,
gpuPrimeThreads,
gpuPrimeBatch,
kernelType,
useOrder,
showHelp,
useBitTransform,
useLucas,
useResidue,
useDivisor,
useByDivisor,
useGcdFilter,
testMode,
useGpuCycles,
useMersenneOnGpu,
useOrderOnGpu,
scanBatchSize,
sliceSize,
residueKMax,
filterFile,
forcePrimeKernelsOnCpu,
modReductionMode,
nttBackend,
resultsDirectory,
resultsPrefix,
writeBatchSize,
cyclesPath,
continueCyclesGeneration,
cyclesBatchSize,
divisorCyclesSearchLimit,
orderWarmupLimitOverride,
rleBlacklistPath,
rleHardMaxP,
rleOnlyLast7,
zeroFractionHard,
zeroFractionConjecture,
maxZeroConjecture);
}
internal static void PrintHelp()
{
Console.WriteLine("Usage: EvenPerfectBitScanner [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" --prime=<value> starting exponent (p)");
Console.WriteLine(" --max-prime=<value> inclusive upper bound for primes from filter files");
Console.WriteLine(" --increment=bit|add exponent increment method");
Console.WriteLine(" --threads=<value> number of worker threads");
Console.WriteLine(" --block-size=<value> values processed per thread batch");
Console.WriteLine(" --mersenne=pow2mod|incremental|lucas|residue|divisor|bydivisor Mersenne test method");
Console.WriteLine(" --residue-max-k=<value> max k for residue Mersenne test (q = 2*p*k + 1)");
Console.WriteLine(" --mersenne-device=cpu|gpu Device for Mersenne method (default gpu)");
Console.WriteLine(" --primes-device=cpu|gpu Device for prime-scan kernels (default gpu)");
Console.WriteLine(" --gpu-prime-batch=<n> Batch size for GPU primality sieve (default 262144)");
Console.WriteLine(" --order-device=cpu|gpu Device for order computations (default gpu)");
Console.WriteLine(" --ntt=reference|staged GPU NTT backend (default staged)");
Console.WriteLine(" --mod-reduction=auto|uint128|mont64|barrett128 staged NTT reduction (default auto)");
Console.WriteLine(" --gpu-prime-threads=<value> max concurrent GPU prime checks (default 1)");
Console.WriteLine(" --ll-slice=<value> Lucas–Lehmer iterations per slice (default 32)");
Console.WriteLine(" --gpu-scan-batch=<value> GPU q-scan batch size (default 2_097_152)");
Console.WriteLine(" --order-warmup-limit=<value> Warm-up order candidates (default 5_000_000)");
Console.WriteLine(" --rle-blacklist=<path> enable RLE blacklist for p (hard filter up to --rle-hard-max)");
Console.WriteLine(" --rle-hard-max=<p> apply RLE blacklist only for p <= this (default ulong.MaxValue = no limit)");
Console.WriteLine(" --rle-only-last7=true|false apply RLE only when p % 10 == 7 (default true)");
Console.WriteLine(" --zero-hard=<f> hard reject if zero_fraction(p) > f (default off)");
Console.WriteLine(" --zero-conj=<f>:<r> hard reject if zero_fraction(p) > f AND max_zero_block >= r (default off)");
Console.WriteLine(" --results-dir=<path> directory for results file");
Console.WriteLine(" --results-prefix=<text> prefix to prepend to results filename");
Console.WriteLine(" --divisor-cycles=<path> divisor cycles data file");
Console.WriteLine(" --divisor-cycles-device=cpu|gpu device for cycles generation (default gpu)");
Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)");
Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor");
Console.WriteLine(" --use-order test primality via q order");
Console.WriteLine(" --filter-p=<path> process only p from previous run results (required for --mersenne=bydivisor)");
Console.WriteLine(" --write-batch-size=<value> overwrite frequency of disk writes (default 100 lines)");
Console.WriteLine(" --gcd-filter enable early sieve based on GCD");
Console.WriteLine(" --help, -help, --?, -?, /? show this help message");
}
}
@@ -16,7 +16,4 @@
<PackageReference Include="Open.Numeric" Version="3.0.1" />
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -0,0 +1,228 @@
using Open.Collections;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using EvenPerfectBitScanner;
namespace EvenPerfectBitScanner.IO;
internal static class CalculationResultsFile
{
internal static List<ulong> LoadCandidatesWithinRange(string candidateFile, UInt128 maxPrimeLimit, bool maxPrimeConfigured, out int skippedByLimit)
{
List<ulong> candidates = [];
skippedByLimit = 0;
using FileStream readStream = new(candidateFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader reader = new(readStream);
string? line;
ReadOnlySpan<char> trimmed;
while ((line = reader.ReadLine()) is not null)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
trimmed = line.AsSpan().Trim();
if (trimmed.IsEmpty || trimmed[0] == '#')
{
continue;
}
int length = trimmed.Length;
int index = 0;
if (!maxPrimeConfigured)
{
while (index < length)
{
char current = trimmed[index];
uint digit = (uint)(current - '0');
if (digit > 9U)
{
index++;
continue;
}
int start = index++;
while (index < length && (uint)(trimmed[index] - '0') <= 9U)
{
index++;
}
if (Utf8CliParser.TryParseUInt64(trimmed[start..index], out ulong parsed))
{
candidates.Add(parsed);
}
}
}
else
{
UInt128 limit = maxPrimeLimit;
while (index < length)
{
char current = trimmed[index];
uint digit = (uint)(current - '0');
if (digit > 9U)
{
index++;
continue;
}
int start = index++;
while (index < length && (uint)(trimmed[index] - '0') <= 9U)
{
index++;
}
if (Utf8CliParser.TryParseUInt64(trimmed[start..index], out ulong parsed))
{
if ((UInt128)parsed <= limit)
{
candidates.Add(parsed);
}
else
{
skippedByLimit++;
}
}
}
}
}
return candidates;
}
internal static void EnumerateCandidates(string resultsFileName, Action<ulong, bool, bool> lineProcessorAction)
{
using FileStream readStream = new(resultsFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader reader = new(readStream);
string? line;
bool headerSkipped = false;
ReadOnlySpan<char> span;
int first = 0;
int second = 0;
int third = 0;
ulong parsedP = 0UL;
ReadOnlySpan<char> detailedSpan = default;
ReadOnlySpan<char> passedAllTestsSpan = default;
bool detailed = false;
bool passedAllTests = false;
while ((line = reader.ReadLine()) != null)
{
if (!headerSkipped)
{
headerSkipped = true;
continue;
}
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
span = line.AsSpan();
first = span.IndexOf(',');
if (first < 0)
{
continue;
}
parsedP = Utf8CliParser.ParseUInt64(span[..first]);
span = span[(first + 1)..];
second = span.IndexOf(',');
if (second < 0)
{
continue;
}
span = span[(second + 1)..];
third = span.IndexOf(',');
if (third < 0)
{
continue;
}
detailedSpan = span[..third];
passedAllTestsSpan = span[(third + 1)..];
if (Utf8CliParser.TryParseBoolean(detailedSpan, out detailed) && Utf8CliParser.TryParseBoolean(passedAllTestsSpan, out passedAllTests))
{
lineProcessorAction(parsedP, detailed, passedAllTests);
}
}
}
internal static string BuildFileName(
bool bitInc,
int threads,
int block,
GpuKernelType kernelType,
bool useLucasFlag,
bool useDivisorFlag,
bool useByDivisorFlag,
bool mersenneOnGpu,
bool useOrder,
bool useGcd,
NttBackend nttBackend,
int gpuPrimeThreads,
int llSlice,
int gpuScanBatch,
ulong warmupLimit,
ModReductionMode reduction,
string mersenneDevice,
string primesDevice,
string orderDevice)
{
string inc = bitInc ? "bit" : "add";
string mers = useDivisorFlag
? "divisor"
: (useLucasFlag
? "lucas"
: (useByDivisorFlag
? "bydivisor"
: (kernelType == GpuKernelType.Pow2Mod ? "pow2mod" : "incremental")));
string ntt = nttBackend == NttBackend.Staged ? "staged" : "reference";
string red = reduction switch { ModReductionMode.Mont64 => "mont64", ModReductionMode.Barrett128 => "barrett128", ModReductionMode.GpuUInt128 => "uint128", _ => "auto" };
string order = useOrder ? "order-on" : "order-off";
string gcd = useGcd ? "gcd-on" : "gcd-off";
Span<char> initialBuffer = stackalloc char[256];
var builder = new PooledValueStringBuilder(initialBuffer);
try
{
builder.Append("even_perfect_bit_scan_inc-");
builder.Append(inc);
builder.Append("_thr-");
builder.Append(threads);
builder.Append("_blk-");
builder.Append(block);
builder.Append("_mers-");
builder.Append(mers);
builder.Append("_mersdev-");
builder.Append(mersenneDevice);
builder.Append("_ntt-");
builder.Append(ntt);
builder.Append("_red-");
builder.Append(red);
builder.Append("_primesdev-");
builder.Append(primesDevice);
builder.Append('_');
builder.Append(order);
builder.Append("_orderdev-");
builder.Append(orderDevice);
builder.Append('_');
builder.Append(gcd);
builder.Append("_gputh-");
builder.Append(gpuPrimeThreads);
builder.Append("_llslice-");
builder.Append(llSlice);
builder.Append("_scanb-");
builder.Append(gpuScanBatch);
builder.Append("_warm-");
builder.Append(warmupLimit);
builder.Append(".csv");
return builder.ToString();
}
finally
{
builder.Dispose();
}
}
}
File diff suppressed because it is too large. Load diff
+124
View File
@@ -0,0 +1,124 @@
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Text;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner;
internal static class Utf8CliParser
{
private const int StackAllocThreshold = 128;
public static ulong ParseUInt64(ReadOnlySpan<char> value)
{
if (!TryParseUInt64(value, out ulong result))
{
ThrowFormatException(value);
}
return result;
}
public static int ParseInt32(ReadOnlySpan<char> value)
{
if (!TryParseInt32(value, out int result))
{
ThrowFormatException(value);
}
return result;
}
public static bool TryParseUInt64(ReadOnlySpan<char> value, out ulong result)
{
return TryParse(value, static (ReadOnlySpan<byte> source, out ulong parsed, out int consumed) => Utf8Parser.TryParse(source, out parsed, out consumed), out result);
}
public static bool TryParseInt32(ReadOnlySpan<char> value, out int result)
{
return TryParse(value, static (ReadOnlySpan<byte> source, out int parsed, out int consumed) => Utf8Parser.TryParse(source, out parsed, out consumed), out result);
}
public static bool TryParseDouble(ReadOnlySpan<char> value, out double result)
{
return TryParse(value, static (ReadOnlySpan<byte> source, out double parsed, out int consumed) => Utf8Parser.TryParse(source, out parsed, out consumed), out result);
}
public static bool TryParseBoolean(ReadOnlySpan<char> value, out bool result)
{
value = value.Trim();
if (value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
result = true;
return true;
}
if (value.Equals("false", StringComparison.OrdinalIgnoreCase))
{
result = false;
return true;
}
if (value.Equals("1", StringComparison.Ordinal))
{
result = true;
return true;
}
if (value.Equals("0", StringComparison.Ordinal))
{
result = false;
return true;
}
return TryParse(value, static (ReadOnlySpan<byte> source, out bool parsed, out int consumed) => Utf8Parser.TryParse(source, out parsed, out consumed), out result);
}
private static bool TryParse<T>(ReadOnlySpan<char> value, Utf8ParserDelegate<T> parser, out T result)
where T : struct
{
value = value.Trim();
if (value.IsEmpty)
{
result = default;
return false;
}
Span<byte> stackBuffer = stackalloc byte[StackAllocThreshold];
if (value.Length <= stackBuffer.Length)
{
int bytesWritten = Encoding.UTF8.GetBytes(value, stackBuffer);
ReadOnlySpan<byte> utf8 = stackBuffer[..bytesWritten];
if (!parser(utf8, out result, out int consumed) || consumed != bytesWritten)
{
result = default;
return false;
}
return true;
}
ArrayPool<byte> pool = ThreadStaticPools.BytePool;
byte[] rented = pool.Rent(value.Length);
int rentedBytesWritten = Encoding.UTF8.GetBytes(value, rented);
ReadOnlySpan<byte> rentedUtf8 = rented.AsSpan(0, rentedBytesWritten);
bool parsed = parser(rentedUtf8, out result, out int rentedConsumed) && rentedConsumed == rentedBytesWritten;
pool.Return(rented);
if (!parsed)
{
result = default;
return false;
}
return true;
}
private static void ThrowFormatException(ReadOnlySpan<char> value)
{
throw new FormatException($"Invalid value '{value.ToString()}'.");
}
private delegate bool Utf8ParserDelegate<T>(ReadOnlySpan<byte> source, out T value, out int bytesConsumed)
where T : struct;
}
@@ -9,88 +9,88 @@ namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class GpuContextPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_cpu_returns_cpu_accelerator()
{
GpuContextPool.DisposeAll();
// [Fact]
// [Trait("Category", "Fast")]
// public void RentPreferred_cpu_returns_cpu_accelerator()
// {
// GpuContextPool.DisposeAll();
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
}
finally
{
lease.Dispose();
}
// GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
// }
// finally
// {
// lease.Dispose();
// }
GpuContextPool.DisposeAll();
}
// GpuContextPool.DisposeAll();
// }
[Fact]
[Trait("Category", "Fast")]
public void RentPreferred_reuses_cpu_context()
{
GpuContextPool.DisposeAll();
// [Fact]
// [Trait("Category", "Fast")]
// public void RentPreferred_reuses_cpu_context()
// {
// GpuContextPool.DisposeAll();
Context firstContext;
Accelerator firstAccelerator;
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
firstContext = lease.Context;
firstAccelerator = lease.Accelerator;
}
finally
{
lease.Dispose();
}
// Context firstContext;
// Accelerator firstAccelerator;
// GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// firstContext = lease.Context;
// firstAccelerator = lease.Accelerator;
// }
// finally
// {
// lease.Dispose();
// }
lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
lease.Context.Should().BeSameAs(firstContext);
lease.Accelerator.Should().BeSameAs(firstAccelerator);
}
finally
{
lease.Dispose();
}
// lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// lease.Context.Should().BeSameAs(firstContext);
// lease.Accelerator.Should().BeSameAs(firstAccelerator);
// }
// finally
// {
// lease.Dispose();
// }
GpuContextPool.DisposeAll();
}
// GpuContextPool.DisposeAll();
// }
[Fact]
[Trait("Category", "Fast")]
public void DisposeAll_clears_pooled_contexts()
{
GpuContextPool.DisposeAll();
// [Fact]
// [Trait("Category", "Fast")]
// public void DisposeAll_clears_pooled_contexts()
// {
// GpuContextPool.DisposeAll();
Context firstContext;
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
firstContext = lease.Context;
}
finally
{
lease.Dispose();
}
// Context firstContext;
// GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// firstContext = lease.Context;
// }
// finally
// {
// lease.Dispose();
// }
GpuContextPool.DisposeAll();
// GpuContextPool.DisposeAll();
lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
lease.Context.Should().NotBeSameAs(firstContext);
}
finally
{
lease.Dispose();
}
// lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// lease.Context.Should().NotBeSameAs(firstContext);
// }
// finally
// {
// lease.Dispose();
// }
GpuContextPool.DisposeAll();
}
// GpuContextPool.DisposeAll();
// }
}
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -77,18 +74,6 @@ public class GpuKernelCompilationTests
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetKernel not found.");
private static readonly MethodInfo DivisorByDivisorGetExponentKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetKernelByPrimeExponent", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetKernelByPrimeExponent not found.");
private static readonly MethodInfo DivisorByDivisorGetRemainderDeltaKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetRemainderDeltaKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetRemainderDeltaKernel not found.");
private static readonly MethodInfo DivisorByDivisorGetRemainderScanKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetRemainderScanKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetRemainderScanKernel not found.");
private static readonly MethodInfo DivisorTesterGetKernelMethod =
typeof(MersenneNumberDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorGpuTester.GetKernel not found.");
@@ -134,22 +119,13 @@ public class GpuKernelCompilationTests
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPow2ModWideKernel not found.");
private static readonly Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> MersenneDivisorCyclesKernel =
(Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>),
typeof(MersenneDivisorCycles).GetMethod("GpuDivisorCycleKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("MersenneDivisorCycles.GpuDivisorCycleKernel not found."));
DivisorCycleKernels.GpuDivisorCycleKernel;
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>> PrimeTesterSmallPrimeKernel =
(Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>),
typeof(PrimeTester).GetMethod("SmallPrimeSieveKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeTester.SmallPrimeSieveKernel not found."));
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSmallPrimeKernel =
PrimeTesterKernels.SmallPrimeSieveKernel;
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSharesFactorKernel =
(Action<Index1D, ArrayView<ulong>, ArrayView<byte>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView<ulong>, ArrayView<byte>>),
typeof(PrimeTester).GetMethod("SharesFactorKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeTester.SharesFactorKernel not found."));
PrimeTesterKernels.SharesFactorKernel;
public static IEnumerable<object[]> KernelLoaders()
{
@@ -169,9 +145,6 @@ public class GpuKernelCompilationTests
yield return Loader("NttGpuMath.ForwardKernel", CompileNttForwardKernel);
yield return Loader("NttGpuMath.InverseKernel", CompileNttInverseKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.CheckKernel", CompileDivisorByDivisorCheckKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.ComputeMontgomeryExponentKernel", CompileDivisorByDivisorExponentKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.ComputeRemainderDeltasKernel", CompileDivisorByDivisorRemainderDeltaKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.AccumulateRemaindersKernel", CompileDivisorByDivisorRemainderScanKernel);
yield return Loader("MersenneNumberDivisorGpuTester.Kernel", CompileDivisorTesterKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.Kernel", CompileLucasLehmerKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.AddSmallKernel", CompileLucasLehmerAddSmallKernel);
@@ -183,9 +156,9 @@ public class GpuKernelCompilationTests
yield return Loader("PrimeOrderGpuHeuristics.CalculateOrderKernel", CompilePrimeOrderKernel);
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernel", CompilePrimeOrderPow2ModKernel);
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernelWide", CompilePrimeOrderPow2ModWideKernel);
yield return Loader("MersenneDivisorCycles.GpuDivisorCycleKernel", CompileMersenneDivisorCyclesKernel);
yield return Loader("PrimeTester.SmallPrimeSieveKernel", CompilePrimeTesterSmallPrimeKernel);
yield return Loader("PrimeTester.SharesFactorKernel", CompilePrimeTesterSharesFactorKernel);
yield return Loader("DivisorCycleKernels.GpuDivisorCycleKernel", CompileMersenneDivisorCyclesKernel);
yield return Loader("PrimeTesterKernels.SmallPrimeSieveKernel", CompilePrimeTesterSmallPrimeKernel);
yield return Loader("PrimeTesterKernels.SharesFactorKernel", CompilePrimeTesterSharesFactorKernel);
yield return Loader("GpuKernelPool.KernelLeaseKernels", CompileGpuKernelPoolKernels);
}
@@ -194,23 +167,23 @@ public class GpuKernelCompilationTests
return new object[] { name, loader };
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(KernelLoaders))]
public void Kernel_compiles_without_internal_compiler_errors(string name, Action<Accelerator> compile)
{
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
var accelerator = lease.Accelerator;
Action action = () => compile(accelerator);
action.Should().NotThrow($"kernel {name} should compile");
}
finally
{
lease.Dispose();
}
}
// [Theory]
// [Trait("Category", "Fast")]
// [MemberData(nameof(KernelLoaders))]
// public void Kernel_compiles_without_internal_compiler_errors(string name, Action<Accelerator> compile)
// {
// GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// var accelerator = lease.Accelerator;
// Action action = () => compile(accelerator);
// action.Should().NotThrow($"kernel {name} should compile");
// }
// finally
// {
// lease.Dispose();
// }
// }
private static void CompileDivisorCycleCacheKernel(Accelerator accelerator)
{
@@ -293,24 +266,6 @@ public class GpuKernelCompilationTests
_ = DivisorByDivisorGetKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorExponentKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetExponentKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorRemainderDeltaKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetRemainderDeltaKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorRemainderScanKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetRemainderScanKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorTesterKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorGpuTester();
@@ -381,7 +336,7 @@ public class GpuKernelCompilationTests
private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator)
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel);
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<ulong>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel);
_ = kernel;
}
@@ -18,23 +18,33 @@ public class GpuPrimeLimiterTests
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
var first = GpuPrimeWorkLimiter.Acquire();
GpuPrimeWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
var second = GpuPrimeWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
second.Dispose();
GpuPrimeWorkLimiter.Acquire();
try
{
startedSecond.TrySetResult(true);
}
finally
{
GpuPrimeWorkLimiter.Release();
}
});
// Give secondTask a moment to attempt acquire (it should block).
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
// Release first after short delay, allowing second to continue.
// (Dispose at scope end, but we want to end now.)
first.Dispose();
try
{
// Give secondTask a moment to attempt acquire (it should block).
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
}
finally
{
// Release first after the initial wait so the second task can proceed.
GpuPrimeWorkLimiter.Release();
}
await secondTask.WaitAsync(TimeSpan.FromSeconds(1));
sw.Stop();
@@ -84,28 +84,6 @@ public class GpuUInt128Tests
}
}
[Fact]
public void MulModBigInteger_matches_UInt128()
{
var random = new Random(13);
for (var i = 0; i < 20; i++)
{
var high1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low1 = ((ulong)random.Next() << 32) | (uint)random.Next();
var high2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var low2 = ((ulong)random.Next() << 32) | (uint)random.Next();
var modHigh = ((ulong)random.Next() << 32) | (uint)random.Next();
var modLow = ((ulong)random.Next() << 32) | (uint)random.Next() | 1UL;
var modulus = new GpuUInt128(modHigh, modLow);
var a = new GpuUInt128((UInt128)new GpuUInt128(high1, low1) % (UInt128)modulus);
var b = new GpuUInt128((UInt128)new GpuUInt128(high2, low2) % (UInt128)modulus);
var expected = MulModReference((UInt128)a, (UInt128)b, (UInt128)modulus);
var result = new GpuUInt128(a.High, a.Low);
result.MulModBigInteger(b, modulus);
((UInt128)result).Should().Be(expected);
}
}
[Fact]
public void SquareMod_matches_UInt128()
{
@@ -14,7 +14,7 @@ public class LucasLehmerPrimeTesterTests
[Trait("Category", "Fast")]
public void IsMersennePrime_gpu_returns_expected_results(ulong p, bool expected)
{
new MersenneNumberLucasLehmerGpuTester().IsMersennePrime(p).Should().Be(expected);
new MersenneNumberLucasLehmerGpuTester().IsPrime(p, true).Should().Be(expected);
}
[Fact]
@@ -26,7 +26,7 @@ public class LucasLehmerPrimeTesterTests
const ulong exponent = 128UL;
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
tester.IsPrime(exponent, true).Should().BeFalse();
}
[Fact]
@@ -49,7 +49,7 @@ public class LucasLehmerPrimeTesterTests
{
var tester = new MersenneNumberLucasLehmerGpuTester();
tester.WarmUpNttParameters(exponent);
tester.IsMersennePrime(exponent).Should().BeFalse();
tester.IsPrime(exponent, true).Should().BeFalse();
}
[Theory]
@@ -34,19 +34,23 @@ public class NttMontgomeryTests
// Device execution
GpuContextPool.GpuContextLease context = GpuContextPool.Rent();
var accelerator = context.Accelerator;
var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
buffer.View.CopyFromCPU(ref gpu[0], gpu.Length);
NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
for (int i = 0; i < cpu.Length; i++)
try
{
gpu[i].Should().Be(cpu[i]);
}
var accelerator = context.Accelerator;
using var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
buffer.View.CopyFromCPU(ref gpu[0], gpu.Length);
NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
buffer.Dispose();
context.Dispose();
for (int i = 0; i < cpu.Length; i++)
{
gpu[i].Should().Be(cpu[i]);
}
}
finally
{
context.Dispose();
}
}
[Fact]
@@ -1,8 +1,6 @@
using System;
using System.Numerics;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -40,9 +38,10 @@ public class PrimeOrderGpuHeuristicsTests
[Fact]
public void TryPow2Mod_returns_success_for_supported_prime()
{
const ulong prime = 193UL;
const ulong exponent = 138_000_001UL;
const ulong prime = 138_000_001UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder, divisorData);
status.Should().Be(GpuPow2ModStatus.Success);
remainder.Should().Be(2UL);
@@ -120,14 +119,19 @@ public class PrimeOrderGpuHeuristicsTests
public void TryPow2ModBatch_returns_success_for_supported_prime()
{
ulong[] remainders = { 123UL, 456UL };
ulong[] exponents = { 1UL, 2UL };
const ulong prime = 257UL;
ulong[] exponents = { 138_000_001UL, 138_000_011UL };
const ulong prime = 138_000_001UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
ulong expectedFirst = (ulong)BigInteger.ModPow(2, exponents[0], prime);
ulong expectedSecond = (ulong)BigInteger.ModPow(2, exponents[1], prime);
status.Should().Be(GpuPow2ModStatus.Success);
remainders[0].Should().Be(2UL);
remainders[1].Should().Be(4UL);
expectedFirst.Should().Be(2UL);
expectedSecond.Should().Be(2048UL);
remainders[0].Should().Be(expectedFirst);
remainders[1].Should().Be(expectedSecond);
}
[Fact]
@@ -10,19 +10,17 @@ public class PrimeTesterGpuTests
[Fact]
public void IsPrimeGpu_accepts_known_primes()
{
GpuContextPool.ForceCpu = false;
try
{
var tester = new PrimeTester();
ulong[] primes = [5UL, 31UL, 61UL, 89UL, 107UL, 127UL];
ulong[] primes = [31UL, 61UL, 89UL, 107UL, 127UL, 521UL];
foreach (ulong prime in primes)
{
tester.IsPrimeGpu(prime, CancellationToken.None).Should().BeTrue();
PrimeTester.IsPrimeGpu(prime).Should().BeTrue();
}
}
finally
{
GpuContextPool.ForceCpu = false;
GpuContextPool.DisposeAll();
}
}
@@ -30,19 +28,17 @@ public class PrimeTesterGpuTests
[Fact]
public void IsPrimeGpu_rejects_composites()
{
GpuContextPool.ForceCpu = false;
try
{
var tester = new PrimeTester();
ulong[] composites = [21UL, 25UL, 27UL, 35UL, 55UL];
ulong[] composites = [33UL, 39UL, 51UL, 77UL, 91UL];
foreach (ulong composite in composites)
{
tester.IsPrimeGpu(composite, CancellationToken.None).Should().BeFalse();
PrimeTester.IsPrimeGpu(composite).Should().BeFalse();
}
}
finally
{
GpuContextPool.ForceCpu = false;
GpuContextPool.DisposeAll();
}
}
@@ -0,0 +1,64 @@
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
public class ULongExtensionsGpuTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData(1UL, 3UL)]
[InlineData(2UL, 5UL)]
[InlineData(3UL, 7UL)]
[InlineData(63UL, 97UL)]
[InlineData(512UL, 12289UL)]
[InlineData(1024UL, 196613UL)]
[InlineData(4096UL, 999983UL)]
[InlineData(65535UL, 2147483647UL)]
public void Pow2ModWindowedGpu_matches_montgomery_cpu_for_known_values(ulong exponent, ulong modulus)
{
MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(modulus);
ulong expected = exponent.Pow2MontgomeryModWindowedCpu(divisor, keepMontgomery: false);
exponent.Pow2ModWindowedGpu(modulus).Should().Be(expected);
}
[Fact]
[Trait("Category", "Fast")]
public void Pow2ModWindowedGpu_matches_cpu_for_sampled_inputs()
{
ulong[] moduli =
{
3UL,
5UL,
97UL,
12289UL,
196613UL,
4294967291UL,
9223372036854775783UL,
};
ulong[] exponents =
{
1UL,
2UL,
7UL,
32UL,
63UL,
2048UL,
65535UL,
};
foreach (ulong modulus in moduli)
{
MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(modulus);
foreach (ulong exponent in exponents)
{
ulong expected = exponent.Pow2MontgomeryModWindowedCpu(divisor, keepMontgomery: false);
exponent.Pow2ModWindowedGpu(modulus).Should().Be(expected);
}
}
}
}
+18 -18
View File
@@ -7,26 +7,26 @@ namespace PerfectNumbers.Core.Tests;
[Collection("GpuNtt")]
public class GpuKernelPoolTests
{
[Fact]
[Trait("Category", "Fast")]
public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls()
{
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
var accelerator = lease.Accelerator;
// [Fact]
// [Trait("Category", "Fast")]
// public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls()
// {
// GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
// try
// {
// var accelerator = lease.Accelerator;
var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
// var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
// var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
second.Equals(first).Should().BeTrue();
second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
}
finally
{
lease.Dispose();
}
}
// second.Equals(first).Should().BeTrue();
// second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
// }
// finally
// {
// lease.Dispose();
// }
// }
[Fact]
[Trait("Category", "Fast")]
@@ -23,14 +23,25 @@ public class GpuWorkLimiterTests
var secondTask = Task.Run(() =>
{
var second = GpuWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
second.Dispose();
try
{
startedSecond.TrySetResult(true);
}
finally
{
second.Dispose();
}
});
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
first.Dispose();
try
{
await Task.Delay(50);
startedSecond.Task.IsCompleted.Should().BeFalse();
}
finally
{
first.Dispose();
}
await secondTask.WaitAsync(TimeSpan.FromSeconds(1));
sw.Stop();
@@ -0,0 +1,239 @@
using System;
using System.Numerics;
using System.Collections.Generic;
using PerfectNumbers.Core;
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class HeuristicCombinedPrimeTesterTests
{
[Theory]
[Trait("Category", "Fast")]
[InlineData((byte)1)]
[InlineData((byte)7)]
[InlineData((byte)9)]
public void Enumerator_uses_one_a_one_b_schedule_by_default(byte lastDigit)
{
Span<HeuristicCombinedPrimeTester.HeuristicGroupBSequenceState> groupBState =
stackalloc HeuristicCombinedPrimeTester.HeuristicGroupBSequenceState[4];
var enumerator = HeuristicCombinedPrimeTester.CreateHeuristicDivisorEnumerator(500UL * 500UL, lastDigit, groupBState);
var results = new List<HeuristicCombinedPrimeTester.HeuristicDivisorCandidate>(32);
while (results.Count < 20 && enumerator.TryGetNext(out var candidate))
{
results.Add(candidate);
}
const int segmentsToCheck = 6;
results.Count.Should().BeGreaterThanOrEqualTo(4 + segmentsToCheck * 2);
results[0].Value.Should().Be(3UL);
results[0].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant);
results[1].Value.Should().Be(7UL);
results[1].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant);
results[2].Value.Should().Be(11UL);
results[2].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant);
results[3].Value.Should().Be(13UL);
results[3].Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupAConstant);
int index = 4;
for (int segment = 0; segment < segmentsToCheck; segment++)
{
HeuristicCombinedPrimeTester.HeuristicDivisorCandidate groupACandidate = results[index++];
groupACandidate.Value.Should().BeGreaterThan(13UL);
(groupACandidate.Value % 10UL).Should().Be(3UL);
groupACandidate.Group.Should().NotBe(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupB);
HeuristicCombinedPrimeTester.HeuristicDivisorCandidate groupBCandidate = results[index++];
groupBCandidate.Value.Should().BeGreaterThan(13UL);
(groupBCandidate.Value % 10UL).Should().NotBe(3UL);
groupBCandidate.Group.Should().Be(HeuristicCombinedPrimeTester.HeuristicDivisorGroup.GroupB);
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData((byte)1)]
[InlineData((byte)7)]
[InlineData((byte)9)]
public void One_a_one_b_pattern_interleaves_group_a_and_group_b_values(byte lastDigit)
{
ReadOnlySpan<ulong> combined = HeuristicCombinedPrimeTester.GetCombinedDivisors(
lastDigit,
HeuristicCombinedPrimeTester.CombinedDivisorPattern.OneAOneB);
combined.Length.Should().BeGreaterThan(18);
combined[0].Should().Be(3UL);
combined[1].Should().Be(7UL);
combined[2].Should().Be(11UL);
combined[3].Should().Be(13UL);
int index = 4;
const int segmentsToCheck = 6;
for (int segment = 0; segment < segmentsToCheck; segment++)
{
ulong groupAValue = combined[index++];
groupAValue.Should().BeGreaterThan(13UL);
(groupAValue % 10UL).Should().Be(3UL);
ulong groupBValue = combined[index++];
groupBValue.Should().BeGreaterThan(13UL);
(groupBValue % 10UL).Should().NotBe(3UL);
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData((byte)1)]
[InlineData((byte)7)]
[InlineData((byte)9)]
public void Two_a_one_b_pattern_interleaves_two_group_a_values_before_group_b(byte lastDigit)
{
ReadOnlySpan<ulong> combined = HeuristicCombinedPrimeTester.GetCombinedDivisors(
lastDigit,
HeuristicCombinedPrimeTester.CombinedDivisorPattern.TwoAOneB);
combined.Length.Should().BeGreaterThan(20);
combined[0].Should().Be(3UL);
combined[1].Should().Be(7UL);
combined[2].Should().Be(11UL);
combined[3].Should().Be(13UL);
int index = 4;
const int segmentsToCheck = 5;
for (int segment = 0; segment < segmentsToCheck; segment++)
{
for (int i = 0; i < 2; i++)
{
ulong groupAValue = combined[index++];
groupAValue.Should().BeGreaterThan(13UL);
(groupAValue % 10UL).Should().Be(3UL);
}
ulong groupBValue = combined[index++];
groupBValue.Should().BeGreaterThan(13UL);
(groupBValue % 10UL).Should().NotBe(3UL);
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData((byte)1)]
[InlineData((byte)7)]
[InlineData((byte)9)]
public void Three_a_two_b_pattern_interleaves_three_group_a_values_before_two_group_b_values(byte lastDigit)
{
ReadOnlySpan<ulong> combined = HeuristicCombinedPrimeTester.GetCombinedDivisors(
lastDigit,
HeuristicCombinedPrimeTester.CombinedDivisorPattern.ThreeATwoB);
combined.Length.Should().BeGreaterThan(28);
combined[0].Should().Be(3UL);
combined[1].Should().Be(7UL);
combined[2].Should().Be(11UL);
combined[3].Should().Be(13UL);
int index = 4;
const int segmentsToCheck = 4;
for (int segment = 0; segment < segmentsToCheck; segment++)
{
for (int i = 0; i < 3; i++)
{
ulong groupAValue = combined[index++];
groupAValue.Should().BeGreaterThan(13UL);
(groupAValue % 10UL).Should().Be(3UL);
}
for (int i = 0; i < 2; i++)
{
ulong groupBValue = combined[index++];
groupBValue.Should().BeGreaterThan(13UL);
(groupBValue % 10UL).Should().NotBe(3UL);
}
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData((byte)1)]
[InlineData((byte)7)]
[InlineData((byte)9)]
public void Three_a_one_b_pattern_interleaves_three_group_a_values_before_group_b(byte lastDigit)
{
ReadOnlySpan<ulong> combined = HeuristicCombinedPrimeTester.GetCombinedDivisors(
lastDigit,
HeuristicCombinedPrimeTester.CombinedDivisorPattern.ThreeAOneB);
combined.Length.Should().BeGreaterThan(24);
combined[0].Should().Be(3UL);
combined[1].Should().Be(7UL);
combined[2].Should().Be(11UL);
combined[3].Should().Be(13UL);
int index = 4;
const int segmentsToCheck = 4;
for (int segment = 0; segment < segmentsToCheck; segment++)
{
for (int i = 0; i < 3; i++)
{
ulong groupAValue = combined[index++];
groupAValue.Should().BeGreaterThan(13UL);
(groupAValue % 10UL).Should().Be(3UL);
}
ulong groupBValue = combined[index++];
groupBValue.Should().BeGreaterThan(13UL);
(groupBValue % 10UL).Should().NotBe(3UL);
}
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(1UL)]
[InlineData(2UL)]
[InlineData(3UL)]
[InlineData(4UL)]
[InlineData(5UL)]
[InlineData(8UL)]
[InlineData(9UL)]
[InlineData(15UL)]
[InlineData(16UL)]
[InlineData(17UL)]
[InlineData(24UL)]
[InlineData(25UL)]
[InlineData(26UL)]
[InlineData(255UL)]
[InlineData(256UL)]
[InlineData(257UL)]
[InlineData(1_000UL)]
[InlineData(1_048_575UL)]
[InlineData(1_048_576UL)]
[InlineData(1_048_577UL)]
[InlineData(4_000_000UL)]
[InlineData(4_000_001UL)]
[InlineData(4_000_004UL)]
[InlineData(65_535_999UL)]
[InlineData(65_536_000UL)]
[InlineData(65_536_001UL)]
[InlineData(4_294_967_295UL)]
[InlineData(4_294_967_296UL)]
[InlineData(4_294_967_297UL)]
[InlineData(18_446_744_073_709_551_615UL)]
[InlineData(18_446_744_073_709_551_614UL)]
[InlineData(18_446_744_073_709_551_613UL)]
public void ComputeHeuristicDivisorSquareLimit_returns_input_value(ulong value)
{
ulong limit = HeuristicCombinedPrimeTester.ComputeHeuristicDivisorSquareLimit(value);
limit.Should().Be(value);
}
}
@@ -14,8 +14,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
{
MersenneDivisorCycles.GenerateGpu(path, maxDivisor: 50UL, batchSize: 16, skipCount: 0, nextPosition: 0);
var pairs = new List<(ulong divisor, ulong cycle)>();
var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var reader = new BinaryReader(stream);
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
ulong d = reader.ReadUInt64();
@@ -41,9 +41,6 @@ public class MersenneDivisorCyclesGenerateGpuTests
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
}
reader.Dispose();
stream.Dispose();
}
finally
{
@@ -64,8 +61,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4);
var pairs = new List<(ulong divisor, ulong cycle)>();
var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var reader = new BinaryReader(stream);
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
ulong divisor = reader.ReadUInt64();
@@ -98,9 +95,6 @@ public class MersenneDivisorCyclesGenerateGpuTests
var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
completeCount.Should().Be(expected.Count);
reader.Dispose();
stream.Dispose();
}
finally
{
Loaded 100 of 218 files, more files were not shown because too many files have changed in this diff. Show more