Compare commits
512
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0466d98434 | ||
|
|
1bd3d5c163 | ||
|
|
9a0e328995 | ||
|
|
e2f68cedf2 | ||
|
|
0e1212aa1c | ||
|
|
3d458fc6da | ||
|
|
794e092e57 | ||
|
|
ba439f0f0f | ||
|
|
6458f6740d | ||
|
|
f2ce40ade9 | ||
|
|
5c9fb47a3d | ||
|
|
3758524450 | ||
|
|
1e5eadac34 | ||
|
|
21d645fcf0 | ||
|
|
55554d9565 | ||
|
|
a7fc34e37a | ||
|
|
9bc8fd5043 | ||
|
|
c01abbd03a | ||
|
|
a83ae56caf | ||
|
|
43c7bafd5d | ||
|
|
ff1860a7b3 | ||
|
|
1ed5afe689 | ||
|
|
a747f29ac2 | ||
|
|
6e632ba03a | ||
|
|
1f7680a465 | ||
|
|
15c229665d | ||
|
|
4ee6464130 | ||
|
|
5276d9d27b | ||
|
|
74ab4e91e6 | ||
|
|
8e5a1cd4c7 | ||
|
|
2d103a9662 | ||
|
|
32f9f25e9b | ||
|
|
3735a1cf0f | ||
|
|
6b1c2d0665 | ||
|
|
2b00a53c78 | ||
|
|
04d4d18f37 | ||
|
|
4b13bc6497 | ||
|
|
03194650c9 | ||
|
|
3e081141e0 | ||
|
|
d0ba08dd24 | ||
|
|
e566339f09 | ||
|
|
2888a1f9b3 | ||
|
|
8795b460c3 | ||
|
|
ad19096bab | ||
|
|
4e9d57df4b | ||
|
|
868c7a4fd2 | ||
|
|
fab9ba0413 | ||
|
|
bbe0c02069 | ||
|
|
46a2178d65 | ||
|
|
69acd3dad9 | ||
|
|
cc281ab423 | ||
|
|
557bc5d06f | ||
|
|
d20362a2fc | ||
|
|
1e5a9128cb | ||
|
|
035956cdb6 | ||
|
|
fbd6235316 | ||
|
|
cf9d143ca2 | ||
|
|
8637ee8875 | ||
|
|
8a258f7536 | ||
|
|
1075ab1ea1 | ||
|
|
40d3b4b3ac | ||
|
|
ab1a069e57 | ||
|
|
6af0ae900e | ||
|
|
7feeb08943 | ||
|
|
0d7df2e45b | ||
|
|
8e3e990499 | ||
|
|
c03aa53689 | ||
|
|
c697a7e127 | ||
|
|
ff9b0122c0 | ||
|
|
f0b4cdf6e2 | ||
|
|
f672ff520c | ||
|
|
dda53581c0 | ||
|
|
ea01585892 | ||
|
|
faab60c2cd | ||
|
|
4d9158e8f3 | ||
|
|
ea0770a84a | ||
|
|
586eb8d303 | ||
|
|
add62df697 | ||
|
|
bae39ad95e | ||
|
|
5b973fb513 | ||
|
|
4d8ea9dccd | ||
|
|
6c3efb90c9 | ||
|
|
bfc5712d81 | ||
|
|
ab4388dfba | ||
|
|
00341fbb8c | ||
|
|
43cd067e09 | ||
|
|
4baca1ba70 | ||
|
|
24eaec7a21 | ||
|
|
f12919e220 | ||
|
|
b08175dadf | ||
|
|
f2070a0f64 | ||
|
|
865a88fac0 | ||
|
|
fa6f75c158 | ||
|
|
27739acd02 | ||
|
|
3e7606025d | ||
|
|
ebb87d6513 | ||
|
|
3afd9399db | ||
|
|
40ba538587 | ||
|
|
7631b2d4cb | ||
|
|
819cb7b00b | ||
|
|
5dd9eb636d | ||
|
|
9bcf1bd9a1 | ||
|
|
a226ae10cb | ||
|
|
7baf3b619f | ||
|
|
6adf78faf6 | ||
|
|
b418d79fb3 | ||
|
|
c15405e3b1 | ||
|
|
31a0b8d706 | ||
|
|
ea11db6d0f | ||
|
|
8dc7751084 | ||
|
|
1b3a89ca53 | ||
|
|
8acccce471 | ||
|
|
1a4818c794 | ||
|
|
28ff9292b8 | ||
|
|
20f30dc182 | ||
|
|
33f9b51593 | ||
|
|
da6ea25729 | ||
|
|
481d1010e8 | ||
|
|
360a31bd54 | ||
|
|
82e44e3f3f | ||
|
|
7cbc27d6c3 | ||
|
|
4831fc84d1 | ||
|
|
798ae455b1 | ||
|
|
5d5fb28765 | ||
|
|
c726d6de6b | ||
|
|
9540a9935d | ||
|
|
ecc718bc69 | ||
|
|
de8e769f48 | ||
|
|
50d2e462d7 | ||
|
|
d0c9389bcc | ||
|
|
1809ba95b3 | ||
|
|
73c9272ce4 | ||
|
|
5fbbfd7590 | ||
|
|
9c08cf48a7 | ||
|
|
ea2476c749 | ||
|
|
0f51454496 | ||
|
|
c892e1c30c | ||
|
|
94323c2e8c | ||
|
|
4e553c951b | ||
|
|
34240ab3e9 | ||
|
|
bfc6605e97 | ||
|
|
a7ed762428 | ||
|
|
e9cff7bfcd | ||
|
|
d99610098a | ||
|
|
752c07c57e | ||
|
|
bad5c28fdf | ||
|
|
e4f6cbbe25 | ||
|
|
43e2894661 | ||
|
|
0fbd635828 | ||
|
|
b7c187bbc5 | ||
|
|
4917f690fc | ||
|
|
627d2e9b14 | ||
|
|
324080d265 | ||
|
|
eda3c2128a | ||
|
|
9cb50d77ca | ||
|
|
653da2ccc3 | ||
|
|
5799630919 | ||
|
|
79e8706644 | ||
|
|
6ff033a568 | ||
|
|
da7a6cb23d | ||
|
|
37ab77e5db | ||
|
|
75f815567d | ||
|
|
4fcf6254e6 | ||
|
|
f4a3685707 | ||
|
|
ee52920ad9 | ||
|
|
c0b8fa07a2 | ||
|
|
9e5123d3cc | ||
|
|
62577a271b | ||
|
|
1e738f9e0d | ||
|
|
691a0d20b7 | ||
|
|
405436988f | ||
|
|
2c6e9f5460 | ||
|
|
2dcc51bfc0 | ||
|
|
ef5f91ddd0 | ||
|
|
4ef7f636d2 | ||
|
|
1b16e5d2f0 | ||
|
|
56c8e84c19 | ||
|
|
7f468de485 | ||
|
|
e78150b242 | ||
|
|
8108d973ca | ||
|
|
98afb3ff9e | ||
|
|
4ac2ef1e6e | ||
|
|
e338497c2f | ||
|
|
194f10d00b | ||
|
|
fcefaad29f | ||
|
|
e6f1f87d3b | ||
|
|
ca07ed3388 | ||
|
|
2ba193bd65 | ||
|
|
8adeb59acc | ||
|
|
9232778173 | ||
|
|
6928d43aee | ||
|
|
e87488a93e | ||
|
|
473d4efcdf | ||
|
|
fbb1167433 | ||
|
|
6217ea7888 | ||
|
|
14b72a6a2a | ||
|
|
1e25ea0ccf | ||
|
|
d1f3de07d7 | ||
|
|
44f65b1c8f | ||
|
|
84b759e802 | ||
|
|
5fbd5bc20c | ||
|
|
9f05dfb44d | ||
|
|
c6ad53fffb | ||
|
|
7a22fbbba7 | ||
|
|
c4dfbf0fb2 | ||
|
|
b2ed0d5832 | ||
|
|
a1bbd23594 | ||
|
|
d968f553af | ||
|
|
3412834f7d | ||
|
|
0d048ac078 | ||
|
|
7adea979fb | ||
|
|
b8031c5993 | ||
|
|
1be1fd5925 | ||
|
|
83bcd8a0de | ||
|
|
4cca19b1c2 | ||
|
|
be7c2b7ae5 | ||
|
|
e525484b48 | ||
|
|
c0f38fa104 | ||
|
|
8428e6c5bd | ||
|
|
43cf4a978f | ||
|
|
5fcd082fff | ||
|
|
70637c3fba | ||
|
|
c4e56bf753 | ||
|
|
0613cb9d99 | ||
|
|
efb93cace0 | ||
|
|
fada9ff098 | ||
|
|
e5d57364b5 | ||
|
|
964e748498 | ||
|
|
fddec17b3f | ||
|
|
301e715179 | ||
|
|
f5da594240 | ||
|
|
ee4787d244 | ||
|
|
1e1ac8e0ca | ||
|
|
b2689529fa | ||
|
|
67ad36420a | ||
|
|
f5c79c8807 | ||
|
|
b76ef0b11a | ||
|
|
3b759c724e | ||
|
|
534141e5a7 | ||
|
|
36a4617efe | ||
|
|
b985a46ed6 | ||
|
|
56e413064a | ||
|
|
495b3918c1 | ||
|
|
a0154dd587 | ||
|
|
ca0f498d0d | ||
|
|
06b08708d9 | ||
|
|
26894db181 | ||
|
|
b81fe3de75 | ||
|
|
d6a1fa0b51 | ||
|
|
0a2e65e190 | ||
|
|
e63fba4df1 | ||
|
|
14fcd715c1 | ||
|
|
1b03fb311a | ||
|
|
1ce1622388 | ||
|
|
140c291344 | ||
|
|
b5f8ed786c | ||
|
|
ab86fc16fe | ||
|
|
806dc96f8c | ||
|
|
d2e7e4905a | ||
|
|
8d9b8b1829 | ||
|
|
e8c1069a77 | ||
|
|
4c81f9e5a2 | ||
|
|
10751bf841 | ||
|
|
aa4ff799f3 | ||
|
|
07395830a5 | ||
|
|
e53bb6ba43 | ||
|
|
fb08a61c66 | ||
|
|
949b5f891c | ||
|
|
3e7ee6ab46 | ||
|
|
ff724732f8 | ||
|
|
03751c27be | ||
|
|
9c474fc241 | ||
|
|
3e7bffc812 | ||
|
|
0a23502441 | ||
|
|
51730bcba3 | ||
|
|
571b5a9938 | ||
|
|
9ab87a8b9b | ||
|
|
64c3721db4 | ||
|
|
c99aebdf68 | ||
|
|
763714307f | ||
|
|
b0ce7152f7 | ||
|
|
34fcd62c23 | ||
|
|
c8f8132db7 | ||
|
|
e44662413a | ||
|
|
a70113a99d | ||
|
|
0b831b5a23 | ||
|
|
77bc2601f1 | ||
|
|
0055be366d | ||
|
|
2f85636a2a | ||
|
|
d3695a3dc3 | ||
|
|
7c71d5165e | ||
|
|
e9373dd8c9 | ||
|
|
313286f70d | ||
|
|
5a4a5e4d98 | ||
|
|
ee2df92a15 | ||
|
|
38120fba13 | ||
|
|
c9ae56a5ec | ||
|
|
64c08f9b49 | ||
|
|
79d821c475 | ||
|
|
adac9ff5f9 | ||
|
|
4ab70ec4a8 | ||
|
|
9a81f5f685 | ||
|
|
686d482417 | ||
|
|
85a98eb009 | ||
|
|
5a66abe672 | ||
|
|
fbe9a98b42 | ||
|
|
4afe0e356e | ||
|
|
c45b3951c3 | ||
|
|
632957e4f4 | ||
|
|
6dc26336c6 | ||
|
|
6a4ee0e2c5 | ||
|
|
85634da6eb | ||
|
|
26abc2dfa8 | ||
|
|
6918bd28ff | ||
|
|
cd1debe9d6 | ||
|
|
2eb622030e | ||
|
|
762b9bd9f3 | ||
|
|
b27c8bffa7 | ||
|
|
c87f88fd88 | ||
|
|
2dd5a679e2 | ||
|
|
e0a5eea62d | ||
|
|
48127845b5 | ||
|
|
31e95e5a8b | ||
|
|
16181903a1 | ||
|
|
bd8686b86b | ||
|
|
894a3d8af2 | ||
|
|
ac846df7d9 | ||
|
|
581da24b2f | ||
|
|
0543306f31 | ||
|
|
20120f5bc0 | ||
|
|
ce1355c749 | ||
|
|
4f80baf0bc | ||
|
|
6b1ebf01a6 | ||
|
|
b4c384247a | ||
|
|
abab307da4 | ||
|
|
a98d5a7c1a | ||
|
|
26cc574020 | ||
|
|
b6b372bc59 | ||
|
|
3cddbe22a0 | ||
|
|
0bc97937aa | ||
|
|
7ffd438eee | ||
|
|
386b2dd54d | ||
|
|
72dd69bd53 | ||
|
|
b2a7190c14 | ||
|
|
75586d07b8 | ||
|
|
38df505f4f | ||
|
|
ea08d3f71f | ||
|
|
85c6eaece7 | ||
|
|
6adac6b7a7 | ||
|
|
84ebbed98b | ||
|
|
40dff45a81 | ||
|
|
df71823c8f | ||
|
|
9dc6569a05 | ||
|
|
8068a62f55 | ||
|
|
07dad89fdf | ||
|
|
e6a80bb213 | ||
|
|
58e93763f4 | ||
|
|
210c049d11 | ||
|
|
c27673ed38 | ||
|
|
4f18a02b02 | ||
|
|
8527a43c97 | ||
|
|
9941e65488 | ||
|
|
05defebce5 | ||
|
|
9eb09a0901 | ||
|
|
448ccdd4f7 | ||
|
|
2d1b6813d1 | ||
|
|
4cb88ee08b | ||
|
|
4ce653d19a | ||
|
|
192516e2b5 | ||
|
|
d3cd408d3d | ||
|
|
fb6009ff1c | ||
|
|
40bf897b19 | ||
|
|
a21dcb5d70 | ||
|
|
3ebca194e4 | ||
|
|
414272256e | ||
|
|
83cf33fe77 | ||
|
|
596ee76a2d | ||
|
|
16a79f33a9 | ||
|
|
726fd3f50b | ||
|
|
f84a6a1b33 | ||
|
|
dddaa13658 | ||
|
|
2101e19e31 | ||
|
|
941b51ebcf | ||
|
|
56e3a7695f | ||
|
|
874ea10a0e | ||
|
|
27a001001c | ||
|
|
d131ad9ecc | ||
|
|
435ca7588f | ||
|
|
55b04a293a | ||
|
|
37861d1ffd | ||
|
|
ebe48d5f2f | ||
|
|
f9e548279c | ||
|
|
507b9c91f4 | ||
|
|
b8aa845c5c | ||
|
|
8b467c27f3 | ||
|
|
36d316d19c | ||
|
|
513fc7acb0 | ||
|
|
e14cb5df52 | ||
|
|
604e91b3e3 | ||
|
|
4bbdebc874 | ||
|
|
ddca13892b | ||
|
|
7fc9f7320d | ||
|
|
c4ba758359 | ||
|
|
34817bec21 | ||
|
|
3cb2acb3af | ||
|
|
8d44b75ca5 | ||
|
|
93a82f756f | ||
|
|
8c877cecb3 | ||
|
|
bc5e98450a | ||
|
|
ebe3b306b9 | ||
|
|
daab18504b | ||
|
|
28ee92b1b9 | ||
|
|
e6c8cb35e7 | ||
|
|
0bdf85730d | ||
|
|
79cb8823ad | ||
|
|
f63254f0f0 | ||
|
|
b677e6cdec | ||
|
|
1ba04047c1 | ||
|
|
e9270d16f3 | ||
|
|
4d2c70da8b | ||
|
|
fc74f2b59b | ||
|
|
da4dec2369 | ||
|
|
06061862db | ||
|
|
7ef8a304e9 | ||
|
|
ab3f39ec0d | ||
|
|
9958338820 | ||
|
|
699612f4f9 | ||
|
|
9b7611fe62 | ||
|
|
35691ff710 | ||
|
|
9b9c6959be | ||
|
|
d91507c52e | ||
|
|
0f3b354a93 | ||
|
|
b40c61b527 | ||
|
|
05077f7e90 | ||
|
|
5bfc9b1c30 | ||
|
|
c28e36cbe5 | ||
|
|
5b2995c8a5 | ||
|
|
0959b471b9 | ||
|
|
b792eb90e0 | ||
|
|
756be9a040 | ||
|
|
a225cbd117 | ||
|
|
74747adc7c | ||
|
|
845ce41dd8 | ||
|
|
8b95a42f27 | ||
|
|
d7f2dbbec6 | ||
|
|
8c3e9cb76f | ||
|
|
62e4b31f35 | ||
|
|
d95195e0c6 | ||
|
|
34681b9738 | ||
|
|
49fcd93747 | ||
|
|
a171e57353 | ||
|
|
26567bbdaf | ||
|
|
3fb0524259 | ||
|
|
e876a9f2eb | ||
|
|
b58e2be2b0 | ||
|
|
45b0d60a71 | ||
|
|
6717005529 | ||
|
|
b486d429e9 | ||
|
|
ad3effa5fa | ||
|
|
211df684ce | ||
|
|
b798970ce9 | ||
|
|
bc4e526946 | ||
|
|
ed8b093472 | ||
|
|
84c3c23997 | ||
|
|
d72cfc9748 | ||
|
|
a8588d0ec5 | ||
|
|
fc73205507 | ||
|
|
1a3b75a52f | ||
|
|
854e8aaf34 | ||
|
|
760b941c06 | ||
|
|
a1abe71279 | ||
|
|
ae782232f8 | ||
|
|
c2b57ae13d | ||
|
|
f8867205bf | ||
|
|
e316c0055f | ||
|
|
cc7cb12f86 | ||
|
|
d4dcd0a534 | ||
|
|
e92bca197d | ||
|
|
3f010ce9ad | ||
|
|
b69a1101c4 | ||
|
|
283913375b | ||
|
|
436d3856d6 | ||
|
|
a39f7dda59 | ||
|
|
eeab8deea7 | ||
|
|
74b41aad85 | ||
|
|
df69dcbadb | ||
|
|
82a0fb1dd7 | ||
|
|
1296e94843 | ||
|
|
0a2fe73f0b | ||
|
|
a4bda0f0ee | ||
|
|
d472d48a84 | ||
|
|
615ae29fd6 | ||
|
|
2484d506d6 | ||
|
|
58a4473dde | ||
|
|
4d16cfffa2 | ||
|
|
cc834687fd | ||
|
|
01bb731220 | ||
|
|
15bc2686cc | ||
|
|
c1ad705a4a | ||
|
|
1978f7dfcb | ||
|
|
63f3d71ce4 | ||
|
|
773848c577 | ||
|
|
7079d108e7 | ||
|
|
4e5dd35a15 | ||
|
|
5ce67fa1c5 | ||
|
|
dc22545adb | ||
|
|
6e99ee39e3 | ||
|
|
9becb8df67 | ||
|
|
68851bf5ba | ||
|
|
b5cad8251e | ||
|
|
aa5e358878 | ||
|
|
dbd36c4ee8 |
No files matched your search
Vendored
+16
-3
@@ -20,9 +20,22 @@
|
||||
"request": "launch",
|
||||
"preLaunchTask": "Build Debug",
|
||||
"program": "${workspaceFolder}/${input:projectName}/bin/Debug/net8.0/${input:projectName}.dll",
|
||||
"args": [
|
||||
"--increment=add --mersenne=residue --threads=128 --gpu-scan-batch=512000 --primes-device=gpu --mersenne-device=gpu --mod-reduction=barrett128 --block-size=10000000 --gpu-prime-threads=32 --write-batch-size=10000 --divisor-cycles-device=gpu"
|
||||
],
|
||||
"args": [
|
||||
"--increment=add",
|
||||
"--mersenne=bydivisor",
|
||||
"--threads=1",
|
||||
"--gpu-scan-batch=256000",
|
||||
"--primes-device=gpu",
|
||||
"--mersenne-device=gpu",
|
||||
"--mod-reduction=barrett128",
|
||||
"--block-size=1000000",
|
||||
"--gpu-prime-threads=64",
|
||||
"--write-batch-size=10000",
|
||||
"--divisor-cycles-device=gpu",
|
||||
"--prime=31",
|
||||
"--results-prefix=test-",
|
||||
"--filter-p=sorted-primes-passed-residue-_even_perfect_bit_scan_inc-add_thr-16_blk-10000000_mers-incremental_mersdev-gpu_ntt-reference_red-barrett128_primesdev-gpu_order-off_orderdev-gpu_gcd-off_modfix-off_gputh-64_llslice-32_scanb-512000_warm-5000000.csv"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"stopAtEntry": false,
|
||||
"console": "internalConsole"
|
||||
|
||||
Vendored
+7
-1
@@ -2,6 +2,12 @@
|
||||
"dotnet.defaultSolution": "FlexiCoder.sln",
|
||||
"chatgpt.openOnStartup": true,
|
||||
"cSpell.words": [
|
||||
"ILGPU"
|
||||
"CCCCCCCDUL",
|
||||
"cofactor",
|
||||
"ILGPU",
|
||||
"mers",
|
||||
"Mersenne",
|
||||
"primality",
|
||||
"suff"
|
||||
]
|
||||
}
|
||||
@@ -1,94 +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.
|
||||
## Test Execution Time Policy
|
||||
|
||||
### Test execution time policy
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
#### 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 "FullyQualifiedName~SomeTests"`
|
||||
- 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. Only run tests with category "Fast", when you want to validate if you haven't broken anything. If there are no categories assigned to the tests, when you attempt running them always watch out the 2 mins execution time, to prevent hanging up on test execution. In such case, marked all timeout out tests with category Slow and tests which executed within time limit, even if they failed, mark with category Fast, so that we can recognize them in the future.
|
||||
## 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 4 spaces and / or 1 tab in source code files.
|
||||
## apply_patch.cs Profiling Snapshot (2025-10-04)
|
||||
|
||||
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
|
||||
- `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.
|
||||
- Next focus: stream the patch reader and reuse pooled buffers to reduce parse/apply cost.
|
||||
|
||||
## Performance implementation checklist
|
||||
## Mersenne Divisor Guidance
|
||||
|
||||
- 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.
|
||||
- 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`.
|
||||
|
||||
## GPU-specific notes
|
||||
## Practical Kernel Guidance
|
||||
|
||||
The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerators:
|
||||
- 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.
|
||||
|
||||
- [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).
|
||||
## Heuristic Multiply-Shift Benchmarks (2025-10-04)
|
||||
|
||||
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.
|
||||
- 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.
|
||||
+3124
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
@@ -10,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; }
|
||||
@@ -23,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,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)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuPow2ModBenchmarks
|
||||
{
|
||||
private static readonly Pow2ModInput[] Inputs = new Pow2ModInput[]
|
||||
{
|
||||
new(63UL, new GpuUInt128(0UL, 97UL), "SmallExponentSmallModulus"),
|
||||
new(1_048_575UL, new GpuUInt128(0UL, 0xFFFF_FFFBUL), "MediumExponentPrimeModulus"),
|
||||
new(6_710_886_400UL, new GpuUInt128(0x0000_0000_0000_0001UL, 0xFFFF_FFFF_FFFF_FFC3UL), "LargeExponentHighWordModulus"),
|
||||
new(ulong.MaxValue, new GpuUInt128(0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFC5UL), "FullWidthExponentFullWidthModulus"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public Pow2ModInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<Pow2ModInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Windowed (8-bit) exponentiation that dominated every dataset: 21.5 μs for the full-width modulus, 7.87 μs for the large
|
||||
/// set, 265 ns for medium, and 82.97 ns for the small modulus sample.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: FullWidthExponentFullWidthModulus 21,481.71 ns (1.00×), LargeExponentHighWordModulus 7,874.85 ns,
|
||||
/// MediumExponentPrimeModulus 264.97 ns, SmallExponentSmallModulus 82.97 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 ProcessEightBitWindows()
|
||||
{
|
||||
return GpuUInt128.Pow2Mod(Input.Exponent, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit-by-bit fallback; competitive only on medium/small moduli (266 ns / 85.39 ns) while trailing badly on the widest
|
||||
/// modulus (50.97 μs, 2.37× slower) and large input (8.85 μs, 1.12× slower).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: FullWidthExponentFullWidthModulus 50,967.07 ns (2.37×), LargeExponentHighWordModulus 8,851.95 ns,
|
||||
/// MediumExponentPrimeModulus 265.98 ns, SmallExponentSmallModulus 85.39 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 ProcessSingleBits()
|
||||
{
|
||||
return Pow2ModSingleBit(Input.Exponent, Input.Modulus);
|
||||
}
|
||||
|
||||
private static GpuUInt128 Pow2ModSingleBit(ulong exponent, GpuUInt128 modulus)
|
||||
{
|
||||
if (modulus.IsZero || modulus == GpuUInt128.One)
|
||||
{
|
||||
return new GpuUInt128();
|
||||
}
|
||||
|
||||
GpuUInt128 result = new(1UL);
|
||||
GpuUInt128 baseVal = new(2UL);
|
||||
|
||||
ulong e = exponent;
|
||||
while (e != 0UL)
|
||||
{
|
||||
if ((e & 1UL) != 0UL)
|
||||
{
|
||||
result.MulMod(baseVal, modulus);
|
||||
}
|
||||
|
||||
baseVal.MulMod(baseVal, modulus);
|
||||
e >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public readonly record struct Pow2ModInput(ulong Exponent, GpuUInt128 Modulus, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class GpuRationalBenchmarks
|
||||
{
|
||||
private static readonly GpuRational Left = new(new GpuUInt128(3UL), new GpuUInt128(4UL));
|
||||
private static readonly GpuRational Right = new(new GpuUInt128(5UL), new GpuUInt128(6UL));
|
||||
private static readonly GpuUInt128 IntegerLeft = new(3UL);
|
||||
private static readonly GpuUInt128 IntegerRight = new(5UL);
|
||||
|
||||
[Benchmark]
|
||||
public GpuRational AddRational()
|
||||
{
|
||||
GpuRational value = Left;
|
||||
value = value + Right;
|
||||
return value;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public GpuUInt128 AddInteger()
|
||||
{
|
||||
GpuUInt128 value = IntegerLeft;
|
||||
value.Add(IntegerRight);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128AddBenchmarks
|
||||
{
|
||||
private static readonly AddInput[] Inputs = new AddInput[]
|
||||
{
|
||||
new(new GpuUInt128(0UL, 0UL), new GpuUInt128(0UL, 1UL), "IncrementLow"),
|
||||
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, 1UL), "CarryIntoHigh"),
|
||||
new(new GpuUInt128(1UL, ulong.MaxValue), new GpuUInt128(3UL, 7UL), "MixedOperands"),
|
||||
new(new GpuUInt128(ulong.MaxValue, ulong.MaxValue), new GpuUInt128(ulong.MaxValue, ulong.MaxValue), "AllBitsSet"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public AddInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<AddInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Materializes the carry in a local before updating the struct; stayed between 0.459 ns and 0.474 ns across all operand
|
||||
/// patterns, giving it the edge on carry-heavy cases.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.4737 ns (1.00×), CarryIntoHigh 0.4586 ns, IncrementLow 0.4653 ns, MixedOperands 0.4628 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 CarryMaterialisedInLocal()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
value.Add(Input.Right);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the carry inline within the expression; matches the baseline on balanced additions (0.474 ns mixed) but trails
|
||||
/// slightly when the carry bubbles into the high word (0.491 ns) or chains through increments (0.508 ns).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.4676 ns (0.98×), CarryIntoHigh 0.4907 ns, IncrementLow 0.5082 ns, MixedOperands 0.4741 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 CarryInExpression()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
AddInline(ref value, Input.Right);
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AddInline(ref GpuUInt128 left, GpuUInt128 right)
|
||||
{
|
||||
ulong low = left.Low + right.Low;
|
||||
left.High = left.High + right.High + (low < left.Low ? 1UL : 0UL);
|
||||
left.Low = low;
|
||||
}
|
||||
|
||||
public readonly record struct AddInput(GpuUInt128 Left, GpuUInt128 Right, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128BinaryGcdBenchmarks
|
||||
{
|
||||
private static readonly BinaryGcdInput[] Inputs = new BinaryGcdInput[]
|
||||
{
|
||||
new(new GpuUInt128(0UL, 48UL), new GpuUInt128(0UL, 18UL), "SmallOperands"),
|
||||
new(
|
||||
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
|
||||
new GpuUInt128(0x0FED_CBA9_8765_4321UL, 0x0123_4567_89AB_CDEFUL),
|
||||
"HighEntropy"),
|
||||
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, ulong.MaxValue - 1UL), "LowWordHeavy"),
|
||||
new(new GpuUInt128(0x8000_0000_0000_0000UL, 0UL), new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0UL), "HighWordOnly"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public BinaryGcdInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<BinaryGcdInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Baseline that reuses locals between iterations; measured 195–196 ns on high-entropy/high-word-only inputs, 157.8 ns on
|
||||
/// low-word-heavy pairs, and 7.32 ns on tiny operands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighEntropy 195.593 ns (1.00×), HighWordOnly 196.218 ns, LowWordHeavy 157.787 ns, SmallOperands 7.317 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 ReusedVariables()
|
||||
{
|
||||
return BinaryGcdWithReusedVariables(Input.Left, Input.Right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a temporary struct per loop iteration; trims 4–9% off the large inputs (186–186 ns) and 18% off small operands
|
||||
/// (5.99 ns), making it the better all-around choice when GC pressure is acceptable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighEntropy 186.615 ns (0.95×), HighWordOnly 185.849 ns, LowWordHeavy 143.686 ns, SmallOperands 5.987 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 TemporaryStructPerIteration()
|
||||
{
|
||||
return GpuUInt128.BinaryGcd(Input.Left, Input.Right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static GpuUInt128 BinaryGcdWithReusedVariables(GpuUInt128 u, GpuUInt128 v)
|
||||
{
|
||||
if (u.IsZero)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
if (v.IsZero)
|
||||
{
|
||||
return u;
|
||||
}
|
||||
|
||||
ulong combinedLow = u.Low | v.Low;
|
||||
ulong combinedHigh = u.High | v.High;
|
||||
int shift = combinedLow != 0UL
|
||||
? BitOperations.TrailingZeroCount(combinedLow)
|
||||
: 64 + BitOperations.TrailingZeroCount(combinedHigh);
|
||||
|
||||
int zu = GpuUInt128.TrailingZeroCount(u);
|
||||
u >>= zu;
|
||||
|
||||
do
|
||||
{
|
||||
int zv = GpuUInt128.TrailingZeroCount(v);
|
||||
v >>= zv;
|
||||
if (u > v)
|
||||
{
|
||||
(u, v) = (v, u);
|
||||
}
|
||||
|
||||
v -= u;
|
||||
}
|
||||
while (!v.IsZero);
|
||||
|
||||
return u << shift;
|
||||
}
|
||||
|
||||
public readonly record struct BinaryGcdInput(GpuUInt128 Left, GpuUInt128 Right, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128Montgomery64Benchmarks
|
||||
{
|
||||
private static readonly MontgomeryInput[] Inputs = new MontgomeryInput[]
|
||||
{
|
||||
Create(0x0000_0000_0000_0065UL, 0x0000_0000_0000_0037UL, 0x0000_0000_0000_007FUL, "TinyOperands"),
|
||||
Create(0x7FFF_FFFF_FFFF_FFABUL, 0x7FFF_FFFF_FFFF_FFC5UL, 0xFFFF_FFFF_FFFF_FFC5UL, "NearModulus"),
|
||||
Create(0x0123_4567_89AB_CDEFUL, 0x0FED_CBA9_8765_4321UL, 0xFFFF_FFFF_FFFF_FF4FUL, "MixedMagnitude"),
|
||||
Create(0xFFFF_FFFF_FFFF_FF01UL, 0xFFFF_FFFF_FFFF_FDCBUL, 0xFFFF_FFFF_FFFF_FFCFUL, "DenseOperands"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public MontgomeryInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<MontgomeryInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Extension-based Montgomery multiply that ran in 3.64 ns across every operand mix (dense, mixed, near-modulus, tiny),
|
||||
/// making it the reference CPU helper.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: DenseOperands 3.663 ns (1.00×), MixedMagnitude 3.636 ns, NearModulus 3.651 ns, TinyOperands 3.639 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ExtensionMontgomeryMultiply()
|
||||
{
|
||||
return Input.Left.MontgomeryMultiply(Input.Right, Input.Modulus, Input.NPrime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct-based GPU-style multiply; matches device arithmetic but costs 24.9 ns regardless of the operand pattern,
|
||||
/// approximately 6.8× slower than the extension helper.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: DenseOperands 24.904 ns (6.80×), MixedMagnitude 24.886 ns, NearModulus 24.893 ns, TinyOperands 24.869 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong GpuStructMontgomeryMultiply()
|
||||
{
|
||||
// TODO: Migrate remaining callers of MulModMontgomery64 to the extension helper; the struct
|
||||
// emulation is ~6.8× slower but kept for GPU parity benchmarks.
|
||||
GpuUInt128 state = new(Input.Left);
|
||||
return state.MulModMontgomery64(Input.Right, Input.Modulus, Input.NPrime, Input.R2);
|
||||
}
|
||||
|
||||
private static MontgomeryInput Create(ulong left, ulong right, ulong modulus, string name)
|
||||
{
|
||||
ulong adjustedModulus = (modulus & 1UL) == 0UL ? modulus | 1UL : modulus;
|
||||
ulong nPrime = ComputeMontgomeryNPrime(adjustedModulus);
|
||||
ulong r2 = ComputeMontgomeryR2(adjustedModulus);
|
||||
return new MontgomeryInput(left, right, adjustedModulus, nPrime, r2, name);
|
||||
}
|
||||
|
||||
private static ulong ComputeMontgomeryR2(ulong modulus)
|
||||
{
|
||||
UInt128 r = UInt128.One << 64;
|
||||
return (ulong)((r * r) % 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);
|
||||
}
|
||||
|
||||
public readonly record struct MontgomeryInput(
|
||||
ulong Left,
|
||||
ulong Right,
|
||||
ulong Modulus,
|
||||
ulong NPrime,
|
||||
ulong R2,
|
||||
string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128Mul64Benchmarks
|
||||
{
|
||||
private static readonly Mul64Input[] Inputs = new Mul64Input[]
|
||||
{
|
||||
new(new GpuUInt128(0UL, 1UL), new GpuUInt128(0UL, 1UL), "TinyOperands"),
|
||||
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, ulong.MaxValue), "LowWordMax"),
|
||||
new(new GpuUInt128(1UL, 0UL), new GpuUInt128(0UL, ulong.MaxValue), "HighByLow"),
|
||||
new(new GpuUInt128(ulong.MaxValue, ulong.MaxValue), new GpuUInt128(ulong.MaxValue, ulong.MaxValue), "AllBitsSet"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public Mul64Input Input { get; set; }
|
||||
|
||||
public static IEnumerable<Mul64Input> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Baseline that materializes the high product in a local; measured 1.53–1.55 ns across all operand patterns, keeping it in
|
||||
/// front of the inline variant.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 1.551 ns (1.00×), HighByLow 1.532 ns, LowWordMax 1.548 ns, TinyOperands 1.529 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 HighProductMaterializedInLocal()
|
||||
{
|
||||
GpuUInt128 value = Input.Multiplicand;
|
||||
GpuUInt128.Mul64(ref value, Input.Multiplier.High, Input.Multiplier.Low);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inline variant that avoids the temporary struct field update, but costs roughly 4–5% extra work with 1.59–1.63 ns means.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 1.631 ns (1.05×), HighByLow 1.594 ns, LowWordMax 1.623 ns, TinyOperands 1.602 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 HighProductInline()
|
||||
{
|
||||
GpuUInt128 value = Input.Multiplicand;
|
||||
Mul64Inline(ref value, Input.Multiplier);
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void Mul64Inline(ref GpuUInt128 left, GpuUInt128 right)
|
||||
{
|
||||
ulong operand = left.Low;
|
||||
left.Low = operand * right.Low;
|
||||
left.High = operand * right.High + GpuUInt128.MulHigh(operand, right.Low);
|
||||
}
|
||||
|
||||
public readonly record struct Mul64Input(GpuUInt128 Multiplicand, GpuUInt128 Multiplier, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128MulModBenchmarks
|
||||
{
|
||||
private static readonly GpuMulModCase[] GeneralCases =
|
||||
[
|
||||
new(
|
||||
new GpuUInt128(0UL, 5UL),
|
||||
new GpuUInt128(0UL, 7UL),
|
||||
new GpuUInt128(0UL, 97UL),
|
||||
"TinyOperands"),
|
||||
new(
|
||||
new GpuUInt128(0UL, ulong.MaxValue - 3UL),
|
||||
new GpuUInt128(0UL, ulong.MaxValue - 7UL),
|
||||
new GpuUInt128(0UL, ulong.MaxValue - 11UL),
|
||||
"LowWordHeavy"),
|
||||
new(
|
||||
new GpuUInt128(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL),
|
||||
new GpuUInt128(0x0ACE_BDF0_1357_9BDFUL, 0x0246_8ACE_ECA8_6421UL),
|
||||
new GpuUInt128(0x1FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF7UL),
|
||||
"HighWordModulus"),
|
||||
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),
|
||||
"MixedMagnitude"),
|
||||
];
|
||||
|
||||
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<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
|
||||
/// large moduli (193–201 ns).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordModulus 192.794 ns (1.00×), LowWordHeavy 6.883 ns, MixedMagnitude 201.468 ns, TinyOperands 7.146 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
[ArgumentsSource(nameof(GetGeneralCases))]
|
||||
public GpuUInt128 InPlaceMulMod(GpuMulModCase input)
|
||||
{
|
||||
GpuUInt128 value = input.Left;
|
||||
value.MulMod(input.Right, input.Modulus);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates fresh temporaries per iteration; pays off on every dataset with 4.66 ns on tiny operands, 117 ns on
|
||||
/// low-word-heavy inputs, 138 ns on mixed magnitude, and 184 ns on high-word moduli.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordModulus 184.322 ns (0.96×), LowWordHeavy 116.993 ns, MixedMagnitude 138.684 ns, TinyOperands 4.659 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
[ArgumentsSource(nameof(GetGeneralCases))]
|
||||
public GpuUInt128 AllocatePerIteration(GpuMulModCase input)
|
||||
{
|
||||
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)]
|
||||
private static GpuUInt128 MulModInline(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
|
||||
{
|
||||
// Reuse the variables to new instances and lower registry pressure, if possible.
|
||||
GpuUInt128 a = left;
|
||||
GpuUInt128 b = right;
|
||||
GpuUInt128 result = new();
|
||||
|
||||
while (!b.IsZero)
|
||||
{
|
||||
if ((b.Low & 1UL) != 0UL)
|
||||
{
|
||||
result.Add(a);
|
||||
if (result.CompareTo(modulus) >= 0)
|
||||
{
|
||||
result.Sub(modulus);
|
||||
}
|
||||
}
|
||||
|
||||
a <<= 1;
|
||||
if (a.CompareTo(modulus) >= 0)
|
||||
{
|
||||
a.Sub(modulus);
|
||||
}
|
||||
|
||||
if (b.High == 0UL)
|
||||
{
|
||||
b = new GpuUInt128(b.Low >> 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong low = (b.Low >> 1) | (b.High << 63);
|
||||
ulong high = b.High >> 1;
|
||||
b = new GpuUInt128(high, low);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128NativeModuloBenchmarks
|
||||
{
|
||||
private static readonly NativeModuloInput[] Inputs = new NativeModuloInput[]
|
||||
{
|
||||
new(0x0000_0000_0000_0003UL, 0x0000_0000_0000_0005UL, 0x0000_0000_0000_007FUL, "TinyOperands"),
|
||||
new(0x7FFF_FFFF_FFFF_FFC3UL, 0x7FFF_FFFF_FFFF_FF21UL, 0x7FFF_FFFF_FFFF_FFC5UL, "NearModulus"),
|
||||
new(0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFCFUL, 0xFFFF_FFFF_FFFF_FFF5UL, "DenseOperands"),
|
||||
new(0x0123_4567_89AB_CDEFUL, 0x0FED_CBA9_8765_4321UL, 0x1FFF_FFFF_FFFF_FFFBUL, "MixedMagnitude"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public NativeModuloInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<NativeModuloInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Performs the modulo immediately after loading the left operand; runs in 7.28 ns on tiny inputs and 38–58 ns on dense or
|
||||
/// mixed workloads, keeping it well ahead of the deferred path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: DenseOperands 38.127 ns (1.00×), MixedMagnitude 58.555 ns, NearModulus 54.691 ns, TinyOperands 7.278 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ImmediateModulo()
|
||||
{
|
||||
GpuUInt128 state = new(Input.Left % Input.Modulus);
|
||||
return state.MulMod(Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defers the modulo until after the multiply; only helps on tiny operands (18.15 ns) but is 4–8× slower on dense or
|
||||
/// near-modulus workloads at 256–299 ns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: DenseOperands 298.926 ns (7.83×), MixedMagnitude 255.624 ns, NearModulus 293.280 ns, TinyOperands 18.149 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong DeferredNativeModulo()
|
||||
{
|
||||
// TODO: Drop MulModWithNativeModulo from production once all callers use the immediate
|
||||
// reduction path; benchmarks show it is 4–8× slower on large 128-bit operands.
|
||||
GpuUInt128 state = new(Input.Left);
|
||||
return state.MulModWithNativeModulo(Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
public readonly record struct NativeModuloInput(ulong Left, ulong Right, ulong Modulus, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128ScalarMulModBenchmarks
|
||||
{
|
||||
private static readonly ScalarMulModInput[] Inputs = new ScalarMulModInput[]
|
||||
{
|
||||
new(
|
||||
new GpuUInt128(0x0000_0000_0000_0000UL, 0x0000_0000_0000_0011UL),
|
||||
0x0000_0000_0000_0013UL,
|
||||
new GpuUInt128(0x0000_0000_0000_0001UL, 0x0000_0000_0000_00FFUL),
|
||||
"TinyOperands"),
|
||||
new(
|
||||
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
|
||||
0x0F1E_2D3C_4B5A_6978UL,
|
||||
new GpuUInt128(0x0FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFC3UL),
|
||||
"MixedMagnitude"),
|
||||
new(
|
||||
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
|
||||
0x7FFF_FFFF_FFFF_FFFBUL,
|
||||
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
|
||||
"HighWordDominant"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public ScalarMulModInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<ScalarMulModInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a temporary <see cref="GpuUInt128"/> for the scalar operand; landed at 164–194 ns across the three workloads,
|
||||
/// making it the slower option.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordDominant 193.7 ns (1.00×), MixedMagnitude 180.7 ns, TinyOperands 164.0 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 StructAllocating()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
value.MulMod(new GpuUInt128(Input.Right), Input.Modulus);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized scalar path that multiplies directly by the 64-bit factor; saves 6–13% by avoiding the temporary (154–192 ns).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordDominant 192.1 ns (0.99×), MixedMagnitude 168.5 ns, TinyOperands 154.2 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 SpecializedScalar()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
value.MulMod(Input.Right, Input.Modulus);
|
||||
return value;
|
||||
}
|
||||
|
||||
public readonly record struct ScalarMulModInput(GpuUInt128 Left, ulong Right, GpuUInt128 Modulus, string Name)
|
||||
{
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class GpuUInt128SubModScalarBenchmarks
|
||||
{
|
||||
private static readonly SubModInput[] Inputs = new SubModInput[]
|
||||
{
|
||||
new(new GpuUInt128(0UL, 3UL), 5UL, new GpuUInt128(0UL, 11UL), "UnderflowAddsModulus"),
|
||||
new(new GpuUInt128(0UL, ulong.MaxValue - 3UL), 7UL, new GpuUInt128(0UL, ulong.MaxValue - 11UL), "LargeLowWord"),
|
||||
new(new GpuUInt128(17UL, 2UL), 1UL, new GpuUInt128(3UL, 7UL), "HighWordBorrow"),
|
||||
};
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public SubModInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<SubModInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// In-place subtraction that hits 0.509–0.717 ns across the scenarios, edging out the legacy helper especially when a borrow
|
||||
/// is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordBorrow 0.5112 ns (1.00×), LargeLowWord 0.5092 ns, UnderflowAddsModulus 0.7169 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public GpuUInt128 CurrentInPlace()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
value.SubMod(Input.Scalar, Input.Modulus);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy version that stages intermediates in temporaries; nearly matches the in-place path but remains 1–4% slower on every
|
||||
/// case (0.514–0.722 ns).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: HighWordBorrow 0.5308 ns (1.04×), LargeLowWord 0.5144 ns, UnderflowAddsModulus 0.7224 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public GpuUInt128 LegacyTemporaries()
|
||||
{
|
||||
GpuUInt128 value = Input.Left;
|
||||
LegacySubMod(ref value, Input.Scalar, Input.Modulus);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void LegacySubMod(ref GpuUInt128 value, ulong scalar, GpuUInt128 modulus)
|
||||
{
|
||||
ulong low;
|
||||
ulong high;
|
||||
if (value.High == 0UL && value.Low < scalar)
|
||||
{
|
||||
low = value.Low + modulus.Low;
|
||||
ulong carry = low < value.Low ? 1UL : 0UL;
|
||||
high = value.High + modulus.High + carry;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = value.High;
|
||||
low = value.Low;
|
||||
}
|
||||
|
||||
ulong borrow = low < scalar ? 1UL : 0UL;
|
||||
value.High = high - borrow;
|
||||
value.Low = low - scalar;
|
||||
}
|
||||
|
||||
public readonly record struct SubModInput(GpuUInt128 Left, ulong Scalar, GpuUInt128 Modulus, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
|
||||
[MemoryDiagnoser]
|
||||
public class HeuristicMultiplyShiftBenchmarks
|
||||
{
|
||||
private static readonly MultiplyShiftInput[] Inputs =
|
||||
[
|
||||
new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"),
|
||||
new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"),
|
||||
new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"),
|
||||
new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"),
|
||||
];
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public MultiplyShiftInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<MultiplyShiftInput> GetInputs()
|
||||
{
|
||||
return Inputs;
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong SafeMultiplyShift()
|
||||
{
|
||||
return ULongExtensions.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ShiftFirstMultiplyShift()
|
||||
{
|
||||
return ULongExtensions.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong NaiveMultiplyShift()
|
||||
{
|
||||
return (Input.Value * Input.Multiplier) >> Input.Shift;
|
||||
}
|
||||
|
||||
public readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
|
||||
[MemoryDiagnoser]
|
||||
public class MersenneDivisorCycleLengthGpuBenchmarks
|
||||
{
|
||||
private const int Iterations = 64;
|
||||
private static readonly ulong[] StepOffsets = [0UL, 2UL, 4UL, 6UL, 8UL, 10UL, 12UL, 14UL];
|
||||
private const int StepMask = 7;
|
||||
|
||||
private static readonly ulong[] Divisors =
|
||||
[
|
||||
17UL,
|
||||
31UL,
|
||||
8191UL,
|
||||
131071UL,
|
||||
524287UL,
|
||||
2097151UL,
|
||||
8388607UL,
|
||||
];
|
||||
|
||||
[ParamsSource(nameof(GetDivisors))]
|
||||
public ulong Divisor { get; set; }
|
||||
|
||||
public IEnumerable<ulong> GetDivisors() => Divisors;
|
||||
|
||||
/// <summary>
|
||||
/// Baseline single-step GPU loop; spans from 6.74 ns at divisor 17 to 4.95 ms at divisor 8,388,607 and serves as the ratio
|
||||
/// reference for the other variants.
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
|
||||
public ulong CurrentImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_Current(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do-while style loop; nearly matches the baseline (5.88 ns at 17, 9.41 ns at 31) and trims ~5% off the largest divisor with
|
||||
/// 4.95 ms at 8,388,607.
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong DoWhileImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_DoWhile(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Double-subtract variant retained for comparison; ranges from 6.30 ns at divisor 17 to 4.92 ms at 8,388,607, generally matching
|
||||
/// or slightly trailing the baseline.
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong DoubleSubtractImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_DoubleSubtract(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UInt128 modulo helper used for comparison; incurs 42.36 ns at divisor 17 and stretches to 6.17 ms at 8,388,607, making it the
|
||||
/// slowest option across the board.
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong UInt128ModuloImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_UInt128Modulo(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two-step unrolled loop; improves small divisors to 6.79 ns (17) and large ones to 4.78 ms (8,388,607), shaving ~3–5% off the
|
||||
/// baseline at the top end.
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong UnrolledPairImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_UnrolledPair(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Four-step unrolled loop; posts 5.83 ns at divisor 17 and 4.75 ms at 8,388,607, outperforming the pair loop especially on large
|
||||
/// divisors (52.18 μs at 131,071 vs. 52.65 μs).
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong UnrolledQuadImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_UnrolledQuad(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eight-step unrolled loop; 5.45 ns at divisor 17, 582 ns at 8,191, 50.45 μs at 131,071, and 4.67 ms at 8,388,607—slightly behind the
|
||||
/// hex loop on the largest case but faster on mid-range divisors.
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong UnrolledOctImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_UnrolledOct(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sixteen-step unrolled loop (hex); leads the pack at the largest divisor with 4.59 ms and remains competitive elsewhere (4.83 ns at 17,
|
||||
/// 5.13 ns at 31, 50.52 μs at 131,071).
|
||||
/// </summary>
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong UnrolledHexImplementation()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong baseDivisor = Divisor | 1UL;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
|
||||
checksum ^= CalculateCycleLengthGpu_UnrolledHex(candidate);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_Current(ulong divisor)
|
||||
{
|
||||
// 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 (pow != 1UL)
|
||||
{
|
||||
pow <<= 1;
|
||||
if (pow >= divisor)
|
||||
{
|
||||
pow -= divisor;
|
||||
}
|
||||
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_DoWhile(ulong divisor)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
pow <<= 1;
|
||||
if (pow >= divisor)
|
||||
{
|
||||
pow -= divisor;
|
||||
}
|
||||
|
||||
order++;
|
||||
|
||||
if (pow == 1UL)
|
||||
{
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_DoubleSubtract(ulong divisor)
|
||||
{
|
||||
// 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 (pow != 1UL)
|
||||
{
|
||||
pow += pow;
|
||||
if (pow >= divisor)
|
||||
{
|
||||
pow -= divisor;
|
||||
if (pow >= divisor)
|
||||
{
|
||||
pow -= divisor;
|
||||
}
|
||||
}
|
||||
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_UInt128Modulo(ulong divisor)
|
||||
{
|
||||
// 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 (pow != 1UL)
|
||||
{
|
||||
System.UInt128 doubled = (System.UInt128)pow << 1;
|
||||
pow = (ulong)(doubled % divisor);
|
||||
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_UnrolledPair(ulong divisor)
|
||||
{
|
||||
// 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 (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_UnrolledQuad(ulong divisor)
|
||||
{
|
||||
// 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 (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_UnrolledOct(ulong divisor)
|
||||
{
|
||||
// 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 (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong CalculateCycleLengthGpu_UnrolledHex(ulong divisor)
|
||||
{
|
||||
// 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 (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
if (AdvanceMersenneDivisorCycleStep(ref pow, divisor, ref order))
|
||||
{
|
||||
return order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool AdvanceMersenneDivisorCycleStep(ref ulong pow, ulong divisor, ref ulong order)
|
||||
{
|
||||
pow += pow;
|
||||
if (pow >= divisor)
|
||||
{
|
||||
pow -= divisor;
|
||||
}
|
||||
|
||||
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,52 +0,0 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class Mod10UInt128Benchmarks
|
||||
{
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
|
||||
public UInt128 Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public UInt128 StandardModulo()
|
||||
{
|
||||
return Value % 10UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ModWithHighLowModulo()
|
||||
{
|
||||
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
||||
ulong value = (ulong)Value;
|
||||
ulong high = value >> 64;
|
||||
ulong low = value;
|
||||
ulong highRem = high % 10UL;
|
||||
ulong lowRem = low % 10UL;
|
||||
ulong combined = lowRem + highRem * 6UL;
|
||||
return combined % 10UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ModWithLoopModulo()
|
||||
{
|
||||
UInt128 value = Value;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (value == zero)
|
||||
return 0UL;
|
||||
|
||||
ulong result = (ulong)value;
|
||||
UInt128 high = value >> 64;
|
||||
|
||||
while (high != zero)
|
||||
{
|
||||
// 2^64 ≡ 6 (mod 10)
|
||||
result = (result + (ulong)high * 6UL) % 10UL;
|
||||
high >>= 64;
|
||||
}
|
||||
|
||||
return result % 10UL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod10UIntBenchmarks
|
||||
{
|
||||
[Params(10U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 10U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod10();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class Mod10ULongBenchmarks
|
||||
{
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong LegacyModulo()
|
||||
{
|
||||
return Value % 10UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ModMethodModulo()
|
||||
{
|
||||
return Value.Mod10();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Numerics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
@@ -7,56 +9,165 @@ 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; 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; 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, mod8, mod3, mod5;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (Value == zero)
|
||||
{
|
||||
mod3 = 0UL;
|
||||
mod5 = 0UL;
|
||||
mod8 = 0UL;
|
||||
mod10 = 0UL;
|
||||
return (mod10, mod8, mod3, mod5);
|
||||
}
|
||||
ulong mod10;
|
||||
ulong mod8;
|
||||
ulong mod3;
|
||||
ulong mod5;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (value == 0UL)
|
||||
{
|
||||
mod3 = 0UL;
|
||||
mod5 = 0UL;
|
||||
mod8 = 0UL;
|
||||
mod10 = 0UL;
|
||||
return (mod10, mod8, mod3, mod5);
|
||||
}
|
||||
|
||||
UInt128 high = Value >> 64;
|
||||
ulong result = Value;
|
||||
mod8 = result & 7UL;
|
||||
// 2^64 ≡ 1 (mod 3)
|
||||
ulong modRem = (result % 3UL) + ((ulong)high % 3UL);
|
||||
mod3 = modRem >= 3UL ? modRem - 3UL : modRem;
|
||||
// 2^64 ≡ 1 (mod 5)
|
||||
modRem = (result % 5UL) + ((ulong)high % 5UL);
|
||||
mod5 = modRem >= 5UL ? modRem - 5UL : modRem;
|
||||
UInt128 high = value >> 64;
|
||||
ulong result = value;
|
||||
mod8 = result & 7UL;
|
||||
|
||||
while (high != zero)
|
||||
{
|
||||
// 2^64 ≡ 6 (mod 10)
|
||||
result = (result + (ulong)high * 6UL) % 10UL;
|
||||
high >>= 64;
|
||||
}
|
||||
ulong modRem = (result % 3UL) + ((ulong)high % 3UL);
|
||||
mod3 = modRem >= 3UL ? modRem - 3UL : modRem;
|
||||
|
||||
mod10 = result % 10UL;
|
||||
modRem = (result % 5UL) + ((ulong)high % 5UL);
|
||||
mod5 = modRem >= 5UL ? modRem - 5UL : modRem;
|
||||
|
||||
while (high != zero)
|
||||
{
|
||||
result = (result + (ulong)high * 6UL) % 10UL;
|
||||
high >>= 64;
|
||||
}
|
||||
|
||||
mod10 = result % 10UL;
|
||||
return (mod10, mod8, mod3, mod5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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,65 +0,0 @@
|
||||
using System.Numerics;
|
||||
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)
|
||||
{
|
||||
// 2^64 ≡ 1 (mod 3)
|
||||
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)
|
||||
{
|
||||
// 2^64 ≡ 1 (mod 5)
|
||||
ulong high = (ulong)(value >> 64);
|
||||
ulong low = (ulong)value;
|
||||
ulong rem = (low % 5UL) + (high % 5UL);
|
||||
return rem >= 5UL ? rem - 5UL : rem;
|
||||
}
|
||||
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
|
||||
{
|
||||
Value.Mod10_8_5_3(out var mod10, out ulong mod8, out ulong mod5, out ulong mod3);
|
||||
Mod10R = mod10;
|
||||
Mod8R = mod8;
|
||||
Mod5R = mod5;
|
||||
Mod3R = mod3;
|
||||
|
||||
return (Mod10R, Mod8R, Mod3R, Mod5R);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo()
|
||||
{
|
||||
UInt128 value = Value;
|
||||
ulong mod10 = (ulong)(value % 10UL) % 10UL;
|
||||
ulong mod8 = (ulong)(value % 8UL);
|
||||
ulong mod3 = (ulong)(value % 3UL);
|
||||
ulong mod5 = (ulong)(value % 5UL);
|
||||
return (mod10, mod8, mod3, mod5);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
|
||||
{
|
||||
Value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
|
||||
// Step residues for q += 2*p
|
||||
mod10 %= 10UL;
|
||||
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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 11UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod11();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod11UIntBenchmarks
|
||||
{
|
||||
[Params(11U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 11U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod11();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod11ULongBenchmarks
|
||||
{
|
||||
[Params(11UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 11UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod11();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,81 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class Mod11_7_5_3Benchmarks
|
||||
{
|
||||
private static readonly byte[] Mod7ByteCoefficients = [1, 4, 2, 1, 4, 2, 1, 4];
|
||||
private static readonly byte[] Mod11ByteCoefficients = [1, 3, 9, 5, 4, 1, 3, 9];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong ReduceMod7(uint value)
|
||||
{
|
||||
while (value >= 7U)
|
||||
{
|
||||
value = (value >> 3) + (value & 7U);
|
||||
if (value >= 7U && value < 14U)
|
||||
{
|
||||
value -= 7U;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong ReduceMod11(uint value)
|
||||
{
|
||||
while (value >= 11U)
|
||||
{
|
||||
value = (value & 15U) + ((value >> 4) * 5U);
|
||||
if (value >= 11U && value < 22U)
|
||||
{
|
||||
value -= 11U;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void Mod11_7_5_3(ulong value, out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3)
|
||||
{
|
||||
uint mod3And5Accumulator = 0U;
|
||||
uint mod7Accumulator = 0U;
|
||||
uint mod11Accumulator = 0U;
|
||||
|
||||
int index = 0;
|
||||
byte[] mod7ByteCoefficients = Mod7ByteCoefficients;
|
||||
byte[] mod11ByteCoefficients = Mod11ByteCoefficients;
|
||||
ulong temp = value;
|
||||
|
||||
do
|
||||
{
|
||||
byte current = (byte)temp;
|
||||
mod3And5Accumulator += current;
|
||||
mod7Accumulator += (uint)(current * mod7ByteCoefficients[index]);
|
||||
mod11Accumulator += (uint)(current * mod11ByteCoefficients[index]);
|
||||
|
||||
temp >>= 8;
|
||||
index++;
|
||||
}
|
||||
while (temp != 0UL);
|
||||
|
||||
mod3 = mod3And5Accumulator % 3UL;
|
||||
mod5 = mod3And5Accumulator % 5UL;
|
||||
mod7 = ReduceMod7(mod7Accumulator);
|
||||
mod11 = ReduceMod11(mod11Accumulator);
|
||||
}
|
||||
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plain `%` baseline; timings hovered between 1.56 ns and 1.61 ns (1.611 ns at value 3, 1.563 ns at 8191, 1.561 ns at 131071,
|
||||
/// 1.572 ns at 2147483647).
|
||||
/// </summary>
|
||||
[Benchmark(Baseline = true)]
|
||||
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) ModuloOperator()
|
||||
{
|
||||
@@ -16,17 +83,14 @@ public class Mod11_7_5_3Benchmarks
|
||||
return (value % 11UL, value % 7UL, value % 5UL, value % 3UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) ExtensionMethods()
|
||||
{
|
||||
ulong value = Value;
|
||||
return (value.Mod11(), value.Mod7(), value.Mod5(), value.Mod3());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combined coefficient method; simplifies residue reuse but costs 2.10 ns at value 3, 4.93 ns at 8191, 7.10 ns at 131071, and
|
||||
/// 8.97 ns at 2147483647 (5.7x slower than the scalar `%` baseline on the largest input).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) CombinedMethod()
|
||||
{
|
||||
Value.Mod11_7_5_3(out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3);
|
||||
Mod11_7_5_3(Value, out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3);
|
||||
return (mod11, mod7, mod5, mod3);
|
||||
}
|
||||
}
|
||||
@@ -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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 128UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod128();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod128UIntBenchmarks
|
||||
{
|
||||
[Params(128U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 128U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod128();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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(128UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 128UL;
|
||||
}
|
||||
|
||||
[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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 3UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod3();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 3U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod3();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod3ULongBenchmarks
|
||||
{
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 17UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 3UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod3();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 5UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod5();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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(5U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 5U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod5();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod5ULongBenchmarks
|
||||
{
|
||||
[Params(5UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 31UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 5UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod5();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,81 +0,0 @@
|
||||
using System.Numerics;
|
||||
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 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; }
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = Iterations)]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
ulong checksum = 0UL;
|
||||
ulong value = StartValue;
|
||||
|
||||
for (int i = 0; i < Iterations; i++)
|
||||
{
|
||||
checksum += value.Mod6();
|
||||
value += (ulong)((i & 15) + 1);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 6UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod6();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod6UIntBenchmarks
|
||||
{
|
||||
[Params(6U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 6U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod6();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Numerics;
|
||||
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 ulong Divisor = 6UL;
|
||||
private static readonly ulong FastDivMul = (ulong)(((UInt128)1 << 64) / Divisor);
|
||||
|
||||
[Params(3UL, 131071UL, ulong.MaxValue)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % Divisor;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong DivisionBased()
|
||||
{
|
||||
ulong quotient = Value / Divisor;
|
||||
return Value - quotient * Divisor;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong FastDivHigh()
|
||||
{
|
||||
ulong quotient = Value.FastDiv64(Divisor, FastDivMul);
|
||||
return Value - quotient * Divisor;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod6();
|
||||
}
|
||||
}
|
||||
@@ -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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 7UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod7();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod7UIntBenchmarks
|
||||
{
|
||||
[Params(7U, 2047U, 65535U, 2147483647U)]
|
||||
public uint Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 7U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod7();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod7ULongBenchmarks
|
||||
{
|
||||
[Params(7UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 63UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 7UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod7();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,27 +0,0 @@
|
||||
using System.Numerics;
|
||||
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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return (ulong)(Value % 8UL);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod8();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public uint ModuloOperator()
|
||||
{
|
||||
return Value % 8U;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public uint ExtensionMethod()
|
||||
{
|
||||
return Value.Mod8();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +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 Mod8ULongBenchmarks
|
||||
{
|
||||
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
|
||||
public ulong Value { get; set; }
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ModuloOperator()
|
||||
{
|
||||
return Value % 8UL;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public ulong ExtensionMethod()
|
||||
{
|
||||
return Value.Mod8();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +21,79 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baseline Montgomery multiply used in earlier revisions; measured 247 ns (64 batch), 974 ns (256), and 3.94 μs (1024).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: BatchSize 64 → 247.0 ns (1.00×), 256 → 974.2 ns, 1024 → 3,940.6 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong OriginalImplementation()
|
||||
{
|
||||
@@ -44,6 +106,12 @@ public class MontgomeryMultiplyBenchmarks
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized Montgomery multiply; consistently 3–5% faster with 234.9 ns (64), 942.6 ns (256), and 3.75 μs (1024).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: BatchSize 64 → 234.9 ns (0.95×), 256 → 942.6 ns, 1024 → 3,751.6 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong OptimizedImplementation()
|
||||
{
|
||||
@@ -56,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);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class Mul64Benchmarks
|
||||
{
|
||||
private static readonly BigInteger ProductMask = (BigInteger.One << 128) - BigInteger.One;
|
||||
|
||||
private static readonly Mul64Input[] Inputs =
|
||||
[
|
||||
new(Create(0UL, 0x0000_0000_0000_0001UL), Create(0UL, 0x0000_0000_0000_0001UL), "TinyOperands"),
|
||||
new(Create(0UL, 0xFFFF_FFFF_FFFF_FFFBUL), Create(0UL, 0xFFFF_FFFF_FFFF_FFFFUL), "LowWordMax"),
|
||||
new(Create(0x0000_0000_0000_0001UL, 0x0000_0000_0000_0000UL), Create(0x0000_0000_0000_0001UL, 0x0000_0000_0000_0000UL), "HighWordOnly"),
|
||||
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), "AllBitsSet"),
|
||||
new(Create(0x1234_5678_9ABC_DEF0UL, 0x1357_9BDF_2468_ACEEUL), Create(0x0FED_CBA9_8765_4321UL, 0x0246_8ACE_1357_9BDFUL), "InterleavedHighLow"),
|
||||
new(Create(0x0000_0000_0000_0000UL, 0x0000_0001_0000_0000UL), Create(0xFFFF_FFFF_0000_0000UL, 0x0000_0000_FFFF_FFFFUL), "ShiftHeavyMixed"),
|
||||
];
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public Mul64Input Input { get; set; }
|
||||
|
||||
public static IEnumerable<Mul64Input> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Mul64 layout that keeps the high-word accumulation in locals; measured 1.49–1.57 ns across all operand mixes, making it
|
||||
/// the fastest CPU layout we profiled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 1.568 ns, HighWordOnly 1.495 ns, InterleavedHighLow 1.526 ns,
|
||||
/// LowWordMax 1.506 ns, ShiftHeavyMixed 1.481 ns, TinyOperands 1.503 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public UInt128 HighWordAccumulatedInLocals()
|
||||
{
|
||||
return Input.Left.Mul64(Input.Right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mul64 layout that folds the high-word expression into a single return value; costs roughly 3–5% extra with 1.54–1.58 ns
|
||||
/// runtimes depending on the operand mix.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 1.583 ns, HighWordOnly 1.542 ns, InterleavedHighLow 1.575 ns,
|
||||
/// LowWordMax 1.581 ns, ShiftHeavyMixed 1.562 ns, TinyOperands 1.546 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public UInt128 FoldedHighReturnValue()
|
||||
{
|
||||
return Mul64FoldedHighReturn(Input.Left, Input.Right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mul64 layout shaped for the GPU helper to keep the cross products in registers; roughly 2.35× slower than the baseline at
|
||||
/// 3.68–3.72 ns, but useful when mirroring GPU arithmetic on the CPU.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 3.682 ns, HighWordOnly 3.679 ns, InterleavedHighLow 3.719 ns,
|
||||
/// LowWordMax 3.684 ns, ShiftHeavyMixed 3.707 ns, TinyOperands 3.683 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public UInt128 GpuFriendlyLayout()
|
||||
{
|
||||
return Mul64GpuFriendly(Input.Left, Input.Right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference implementation that multiplies with <see cref="BigInteger"/> to expose the full 256-bit product; handy for
|
||||
/// validation but 22–57× slower at 34–87 ns depending on the operand distribution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 86.087 ns, HighWordOnly 78.860 ns, InterleavedHighLow 87.367 ns,
|
||||
/// LowWordMax 74.418 ns, ShiftHeavyMixed 81.020 ns, TinyOperands 34.286 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public UInt128 BigIntegerReference()
|
||||
{
|
||||
return Mul64WithBigInteger(Input.Left, Input.Right);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static UInt128 Mul64FoldedHighReturn(UInt128 left, UInt128 right)
|
||||
{
|
||||
ulong leftLow = (ulong)left;
|
||||
ulong rightLow = (ulong)right;
|
||||
return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHighCpu(rightLow)) << 64) | (UInt128)(leftLow * rightLow);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static UInt128 Mul64GpuFriendly(UInt128 left, UInt128 right)
|
||||
{
|
||||
GpuUInt128 gpuLeft = new((ulong)(left >> 64), (ulong)left);
|
||||
GpuUInt128 gpuRight = new((ulong)(right >> 64), (ulong)right);
|
||||
gpuLeft.Mul(gpuRight);
|
||||
return (UInt128)gpuLeft;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static UInt128 Mul64WithBigInteger(UInt128 left, UInt128 right)
|
||||
{
|
||||
BigInteger product = (BigInteger)left * (BigInteger)right;
|
||||
return (UInt128)(product & ProductMask);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static UInt128 Create(ulong high, ulong low)
|
||||
{
|
||||
return ((UInt128)high << 64) | low;
|
||||
}
|
||||
|
||||
public readonly record struct Mul64Input(UInt128 Left, UInt128 Right, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class MulHighBenchmarks
|
||||
{
|
||||
private static readonly MulHighInput[] Inputs =
|
||||
[
|
||||
new(0x0000000100000000UL, 0x0000000100000000UL, "LowHighWord"),
|
||||
new(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL, "Mixed"),
|
||||
new(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL, "AllBitsSet"),
|
||||
new(0x8000_0000_0000_0000UL, 0x8000_0000_0000_0000UL, "HighPowersOfTwo"),
|
||||
new(0x0000_0000_0001_0000UL, 0xFFFF_FFFF_0000_0000UL, "ShiftHeavy"),
|
||||
new(0x0000_0000_0000_0000UL, 0xFFFF_FFFF_FFFF_FFFFUL, "IncludesZero"),
|
||||
];
|
||||
|
||||
[ParamsSource(nameof(GetInputs))]
|
||||
public MulHighInput Input { get; set; }
|
||||
|
||||
public static IEnumerable<MulHighInput> GetInputs() => Inputs;
|
||||
|
||||
/// <summary>
|
||||
/// Baseline MulHigh implementation used across the scanner; measured 0.73–0.78 ns with minimal variance between operand
|
||||
/// mixes, yet still slower than the UInt128 intrinsic.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.7577 ns, HighPowersOfTwo 0.7352 ns, IncludesZero 0.7497 ns,
|
||||
/// LowHighWord 0.7296 ns, Mixed 0.7365 ns, ShiftHeavy 0.7747 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong CurrentImplementation()
|
||||
{
|
||||
// Routes through the UInt128-based helper adopted by CPU hot paths after benchmarking.
|
||||
return ULongExtensions.MulHighCpu(Input.X, Input.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses UInt128 to compute the high word directly; fastest option at 0.054–0.067 ns across all mixes, beating the baseline
|
||||
/// by roughly 11–14×.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.0641 ns, HighPowersOfTwo 0.0674 ns, IncludesZero 0.0541 ns,
|
||||
/// LowHighWord 0.0576 ns, Mixed 0.0634 ns, ShiftHeavy 0.0591 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong UInt128BuiltIn()
|
||||
{
|
||||
return MulHighWithUInt128(Input.X, Input.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emulates UInt128 multiplication using the GPU-friendly <see cref="GpuUInt128"/> type; lands between 0.854 ns and 0.869 ns,
|
||||
/// which is ~12–17% slower than the baseline but mirrors GPU arithmetic exactly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.8643 ns, HighPowersOfTwo 0.8584 ns, IncludesZero 0.8538 ns,
|
||||
/// LowHighWord 0.8576 ns, Mixed 0.8693 ns, ShiftHeavy 0.8625 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong GpuUInt128Style()
|
||||
{
|
||||
return MulHighWithGpuUInt128(Input.X, Input.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces trailing zeros before multiplying smaller operands; excels on high-power-of-two inputs (1.142 ns) but remains
|
||||
/// 20–55% slower than the baseline elsewhere.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.9894 ns, HighPowersOfTwo 1.1421 ns, IncludesZero 0.9270 ns,
|
||||
/// LowHighWord 0.9195 ns, Mixed 1.2042 ns, ShiftHeavy 1.1909 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong ShiftThenMultiply()
|
||||
{
|
||||
return MulHighWithTrailingZeroReduction(Input.X, Input.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schoolbook multiply splitting inputs into 32-bit halves; tracks the baseline closely at 0.739–0.748 ns with its best
|
||||
/// showing on mixed inputs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: AllBitsSet 0.7414 ns, HighPowersOfTwo 0.7459 ns, IncludesZero 0.7442 ns,
|
||||
/// LowHighWord 0.7441 ns, Mixed 0.7395 ns, ShiftHeavy 0.7485 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong SchoolbookVariant()
|
||||
{
|
||||
return MulHighSchoolbook(Input.X, Input.Y);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulHighWithUInt128(ulong x, ulong y)
|
||||
{
|
||||
return (ulong)(((UInt128)x * y) >> 64);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulHighWithGpuUInt128(ulong x, ulong y)
|
||||
{
|
||||
return ULongExtensions.MulHighGpu(x, y);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulHighWithTrailingZeroReduction(ulong x, ulong y)
|
||||
{
|
||||
if ((x | y) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
int shiftX = BitOperations.TrailingZeroCount(x);
|
||||
int shiftY = BitOperations.TrailingZeroCount(y);
|
||||
x >>= shiftX;
|
||||
y >>= shiftY;
|
||||
|
||||
UInt128 product = (UInt128)x * y;
|
||||
int totalShift = shiftX + shiftY;
|
||||
|
||||
if (totalShift >= 64)
|
||||
{
|
||||
return (ulong)(product << (totalShift - 64));
|
||||
}
|
||||
|
||||
return (ulong)(product >> (64 - totalShift));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulHighSchoolbook(ulong x, ulong y)
|
||||
{
|
||||
ulong xLow = (uint)x;
|
||||
ulong xHigh = x >> 32;
|
||||
ulong yLow = (uint)y;
|
||||
ulong yHigh = y >> 32;
|
||||
|
||||
ulong w1 = xLow * yHigh;
|
||||
ulong w2 = xHigh * yLow;
|
||||
ulong w3 = xLow * yLow;
|
||||
|
||||
ulong result = (xHigh * yHigh) + (w1 >> 32) + (w2 >> 32);
|
||||
result += ((w3 >> 32) + (uint)w1 + (uint)w2) >> 32;
|
||||
return result;
|
||||
}
|
||||
|
||||
public readonly record struct MulHighInput(ulong X, ulong Y, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using PerfectNumbers.Core;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[SimpleJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class MulMod64Benchmarks
|
||||
{
|
||||
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; }
|
||||
|
||||
public static IEnumerable<MulMod64Input> GetInputs()
|
||||
{
|
||||
return Inputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper; every input landed in the
|
||||
/// 3.56–3.61 ns window, keeping this baseline consistently ahead of the other CPU implementations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 3.606 ns (1.00×), MixedBitPattern 3.564 ns, NearFullRange 3.583 ns,
|
||||
/// PrimeSizedModulus 3.593 ns, SparseOperands 3.555 ns, ZeroOperands 3.599 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public ulong ExtensionBaseline()
|
||||
{
|
||||
return Input.Left.MulMod64(Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod(ulong, ulong, ulong)"/> helper; sparse operands (NearFullRange
|
||||
/// 5.71 ns, SparseOperands 5.67 ns) and zeros (1.98 ns) benefit, but dense patterns cost 22.9–64.8 ns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 22.870 ns (6.34×), MixedBitPattern 64.800 ns, NearFullRange 5.708 ns,
|
||||
/// PrimeSizedModulus 52.766 ns, SparseOperands 5.671 ns, ZeroOperands 1.976 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong GpuCompatibleMulModExtension()
|
||||
{
|
||||
GpuUInt128 gpuUInt128 = new(Input.Left);
|
||||
return gpuUInt128.MulMod(Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Observed means: CrossWordBlend 22.780 ns (6.32×), MixedBitPattern 59.165 ns, NearFullRange 5.952 ns,
|
||||
/// PrimeSizedModulus 50.490 ns, SparseOperands 5.239 ns, ZeroOperands 1.986 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong MulModGpu()
|
||||
{
|
||||
GpuUInt128 gpuUInt128 = new(Input.Left);
|
||||
return gpuUInt128.MulMod(Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses manual UInt128 multiplication inside the benchmark; dense operands land between 22.2 ns and 31.4 ns, while sparse
|
||||
/// or zero inputs stay near the 3.54–3.56 ns baseline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 22.218 ns (6.16×), MixedBitPattern 30.539 ns, NearFullRange 27.159 ns,
|
||||
/// PrimeSizedModulus 31.409 ns, SparseOperands 3.563 ns, ZeroOperands 3.542 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong InlineUInt128Operands()
|
||||
{
|
||||
return MulMod64Inline(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces both operands before the inline UInt128 multiply; reductions shine on NearFullRange (4.36 ns) and sparse inputs
|
||||
/// (4.62 ns) but dense mixes still take 22.6–32.2 ns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 22.626 ns (6.27×), MixedBitPattern 31.237 ns, NearFullRange 4.357 ns,
|
||||
/// PrimeSizedModulus 32.176 ns, SparseOperands 4.617 ns, ZeroOperands 4.286 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong InlineUInt128OperandsWithReductionFirst()
|
||||
{
|
||||
return MulMod64InlineWithReductionFirst(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduces each operand individually before multiplication; behaves like the reduction-first path with 4.28–4.32 ns on
|
||||
/// sparse and NearFullRange inputs, yet still spends 22.5–31.8 ns on dense patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 22.548 ns (6.25×), MixedBitPattern 30.210 ns, NearFullRange 4.319 ns,
|
||||
/// PrimeSizedModulus 31.786 ns, SparseOperands 4.282 ns, ZeroOperands 4.256 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong InlineUInt128OperandsWithOperandReduction()
|
||||
{
|
||||
return MulMod64InlineWithOperandReduction(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits the UInt128 product across locals; dense workloads cost 22.2–31.4 ns, while SparseOperands and ZeroOperands stay
|
||||
/// near 3.57–3.59 ns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 22.226 ns (6.16×), MixedBitPattern 30.882 ns, NearFullRange 27.137 ns,
|
||||
/// PrimeSizedModulus 31.351 ns, SparseOperands 3.566 ns, ZeroOperands 3.586 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong InlineUInt128WithLocals()
|
||||
{
|
||||
return MulMod64InlineWithLocals(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines locals with operand reduction; reductions help sparsity (11.86–15.10 ns) but dense cases inflate to 29.7–40.5 ns
|
||||
/// making it the slowest inline variant.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 29.746 ns (8.24×), MixedBitPattern 38.013 ns, NearFullRange 12.187 ns,
|
||||
/// PrimeSizedModulus 40.459 ns, SparseOperands 15.100 ns, ZeroOperands 11.863 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong InlineUInt128WithLocalsAndOperandReduction()
|
||||
{
|
||||
return MulMod64InlineWithLocalsAndOperandReduction(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs the 128-bit product from MulHigh/MulLow pieces; dense blends remain costly at 23.7–32.0 ns but sparse and
|
||||
/// NearFullRange inputs land around 5.20–5.55 ns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: CrossWordBlend 23.685 ns (6.57×), MixedBitPattern 30.114 ns, NearFullRange 5.221 ns,
|
||||
/// PrimeSizedModulus 31.950 ns, SparseOperands 5.204 ns, ZeroOperands 5.171 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public ulong MultiplyHighDecomposition()
|
||||
{
|
||||
return MulMod64MulHigh(Input.Left, Input.Right, Input.Modulus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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: 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 GpuCompatibleBaseline()
|
||||
{
|
||||
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)]
|
||||
private static ulong MulMod64Inline(ulong a, ulong b, ulong modulus) => (ulong)(((UInt128)a * b) % modulus);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulMod64InlineWithReductionFirst(ulong a, ulong b, ulong modulus) => (ulong)(((UInt128)(a % modulus) * (b % modulus)) % modulus);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulMod64InlineWithOperandReduction(ulong a, ulong b, ulong modulus)
|
||||
{
|
||||
ulong left = a % modulus;
|
||||
ulong right = b % modulus;
|
||||
return (ulong)(((UInt128)left * right) % modulus);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulMod64InlineWithLocals(ulong a, ulong b, ulong modulus)
|
||||
{
|
||||
UInt128 left128 = a;
|
||||
UInt128 right128 = b;
|
||||
UInt128 product = left128 * right128;
|
||||
UInt128 reduced = product % modulus;
|
||||
return (ulong)reduced;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulMod64InlineWithLocalsAndOperandReduction(ulong a, ulong b, ulong modulus)
|
||||
{
|
||||
UInt128 left128 = a;
|
||||
UInt128 right128 = b;
|
||||
left128 %= modulus;
|
||||
right128 %= modulus;
|
||||
UInt128 product = left128 * right128;
|
||||
UInt128 reduced = product % modulus;
|
||||
return (ulong)reduced;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong MulMod64MulHigh(ulong a, ulong b, ulong modulus)
|
||||
{
|
||||
ulong left = a % modulus;
|
||||
ulong right = b % modulus;
|
||||
ulong low = left * right;
|
||||
ulong high = left.MulHighCpu(right);
|
||||
UInt128 product = ((UInt128)high << 64) | low;
|
||||
return (ulong)(product % modulus);
|
||||
}
|
||||
|
||||
public readonly record struct MulMod64Input(ulong Left, ulong Right, ulong Modulus, string Name)
|
||||
{
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,518 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using System.Numerics;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
|
||||
public class Pow2MontgomeryModBenchmarks
|
||||
{
|
||||
private const int SampleCount = 256;
|
||||
|
||||
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
|
||||
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
|
||||
private readonly ulong[] _smallExponents = new ulong[SampleCount];
|
||||
private readonly ulong[] _largeExponents = new ulong[SampleCount];
|
||||
private readonly ulong[] _smallCycles = new ulong[SampleCount];
|
||||
private readonly ulong[] _largeCycles = new ulong[SampleCount];
|
||||
|
||||
private readonly Random _random = new(13);
|
||||
|
||||
public enum InputScale
|
||||
{
|
||||
Small,
|
||||
Large
|
||||
}
|
||||
|
||||
[ParamsAllValues]
|
||||
public InputScale Scale { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
ulong smallModulus = NextSmallOddModulus();
|
||||
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
|
||||
_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);
|
||||
_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;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
|
||||
}
|
||||
|
||||
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]
|
||||
public ulong BaselineGpu()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong BaselineWithKnownCycleCpu()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out var cycles);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong BaselineWithKnownCycleGpu()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out var cycles);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Left-to-right Montgomery scan that trails the baseline: 39.11 μs on small inputs (1.08×) and 126.38 μs on large ones (1.08×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong LeftToRight()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2MontgomeryModLeftToRight(exponents[i], divisors[i]);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes four exponent bits per loop, finishing at 38.74 μs on small cases (1.07× baseline) and 120.11 μs on large ones (1.02×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong Batched4()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2MontgomeryModBatched4(exponents[i], divisors[i]);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Windowed Montgomery ladder (width 5) that lags behind the baseline at 52.82 μs on small samples (1.45×) and 124.13 μs on large samples (1.06×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong SlidingWindow()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2MontgomeryModSlidingWindow(exponents[i], divisors[i]);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classic binary ladder without Montgomery reduction; runs 24.75 μs on small inputs (0.68× baseline) but collapses to 295.29 μs on large ones (2.52×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong BinaryMod()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2ModBinary(exponents[i], divisors[i].Modulus);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Precomputes all squarings once; 30.18 μs on small inputs (0.83× baseline) but 289.80 μs on large ones (2.47×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong PrecomputedTableMod()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2ModPrecomputedTable(exponents[i], divisors[i].Modulus);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relies on <see cref="BigInteger.ModPow"/>; fastest on small inputs at 17.21 μs (0.47× baseline) but slowest on large ones at 562.78 μs (4.80×).
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ulong BigIntegerMod()
|
||||
{
|
||||
GetData(out var exponents, out var divisors, out _);
|
||||
ulong checksum = 0UL;
|
||||
|
||||
for (int i = 0; i < SampleCount; i++)
|
||||
{
|
||||
checksum ^= Pow2ModBigInteger(exponents[i], divisors[i].Modulus);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
|
||||
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
|
||||
{
|
||||
if (Scale == InputScale.Small)
|
||||
{
|
||||
exponents = _smallExponents;
|
||||
divisors = _smallDivisors;
|
||||
cycles = _smallCycles;
|
||||
}
|
||||
else
|
||||
{
|
||||
exponents = _largeExponents;
|
||||
divisors = _largeDivisors;
|
||||
cycles = _largeCycles;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
|
||||
|
||||
private ulong NextLargeExponent() => (ulong)_random.NextInt64(1L << 60, long.MaxValue);
|
||||
|
||||
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 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 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);
|
||||
}
|
||||
|
||||
private static ulong Pow2MontgomeryModLeftToRight(ulong exponent, in MontgomeryDivisorData divisor)
|
||||
{
|
||||
ulong modulus = divisor.Modulus;
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
ulong result = divisor.MontgomeryOne;
|
||||
ulong baseVal = divisor.MontgomeryTwo;
|
||||
ulong nPrime = divisor.NPrime;
|
||||
|
||||
if (exponent == 0UL)
|
||||
{
|
||||
return result.MontgomeryMultiply(1UL, modulus, nPrime);
|
||||
}
|
||||
|
||||
int msbIndex = 63 - BitOperations.LeadingZeroCount(exponent);
|
||||
|
||||
for (int bitIndex = msbIndex; bitIndex >= 0; bitIndex--)
|
||||
{
|
||||
result = result.MontgomeryMultiply(result, modulus, nPrime);
|
||||
|
||||
if (((exponent >> bitIndex) & 1UL) != 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(baseVal, modulus, nPrime);
|
||||
}
|
||||
}
|
||||
|
||||
return result.MontgomeryMultiply(1UL, modulus, nPrime);
|
||||
}
|
||||
|
||||
private static ulong Pow2MontgomeryModBatched4(ulong exponent, in MontgomeryDivisorData divisor)
|
||||
{
|
||||
ulong modulus = divisor.Modulus;
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
ulong result = divisor.MontgomeryOne;
|
||||
ulong baseVal = divisor.MontgomeryTwo;
|
||||
ulong nPrime = divisor.NPrime;
|
||||
ulong remainingExponent = exponent;
|
||||
|
||||
while (remainingExponent != 0UL)
|
||||
{
|
||||
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);
|
||||
|
||||
if ((nibble & 1UL) != 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(base0, modulus, nPrime);
|
||||
}
|
||||
|
||||
if ((nibble & 2UL) != 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(base1, modulus, nPrime);
|
||||
}
|
||||
|
||||
if ((nibble & 4UL) != 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(base2, modulus, nPrime);
|
||||
}
|
||||
|
||||
if ((nibble & 8UL) != 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(base3, modulus, nPrime);
|
||||
}
|
||||
|
||||
baseVal = base3.MontgomeryMultiply(base3, modulus, nPrime);
|
||||
remainingExponent >>= 4;
|
||||
}
|
||||
|
||||
return result.MontgomeryMultiply(1UL, modulus, nPrime);
|
||||
}
|
||||
|
||||
// TODO: Update it with the latest implementations
|
||||
private static ulong Pow2MontgomeryModSlidingWindow(ulong exponent, in MontgomeryDivisorData divisor)
|
||||
{
|
||||
ulong modulus = divisor.Modulus;
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
ulong nPrime = divisor.NPrime;
|
||||
ulong result = divisor.MontgomeryOne;
|
||||
ulong baseVal = divisor.MontgomeryTwo;
|
||||
|
||||
if (exponent == 0UL)
|
||||
{
|
||||
return result.MontgomeryMultiply(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);
|
||||
for (int i = 1; i < oddPowers.Length; i++)
|
||||
{
|
||||
oddPowers[i] = oddPowers[i - 1].MontgomeryMultiply(baseSquared, modulus, nPrime);
|
||||
}
|
||||
|
||||
int bitIndex = 63 - BitOperations.LeadingZeroCount(exponent);
|
||||
|
||||
while (bitIndex >= 0)
|
||||
{
|
||||
if (((exponent >> bitIndex) & 1UL) == 0UL)
|
||||
{
|
||||
result = result.MontgomeryMultiply(result, modulus, nPrime);
|
||||
bitIndex--;
|
||||
continue;
|
||||
}
|
||||
|
||||
int windowStart = Math.Max(bitIndex - WindowSize + 1, 0);
|
||||
ulong windowValue = (exponent >> windowStart) & ((1UL << (bitIndex - windowStart + 1)) - 1UL);
|
||||
|
||||
while ((windowValue & 1UL) == 0UL)
|
||||
{
|
||||
windowValue >>= 1;
|
||||
windowStart++;
|
||||
}
|
||||
|
||||
int squares = bitIndex - windowStart + 1;
|
||||
for (int i = 0; i < squares; i++)
|
||||
{
|
||||
result = result.MontgomeryMultiply(result, modulus, nPrime);
|
||||
}
|
||||
|
||||
result = result.MontgomeryMultiply(oddPowers[(int)(windowValue >> 1)], modulus, nPrime);
|
||||
bitIndex = windowStart - 1;
|
||||
}
|
||||
|
||||
return result.MontgomeryMultiply(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)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
ulong result = 1UL % modulus;
|
||||
ulong baseVal = 2UL % modulus;
|
||||
ulong remainingExponent = exponent;
|
||||
|
||||
while (remainingExponent > 0UL)
|
||||
{
|
||||
if ((remainingExponent & 1UL) != 0UL)
|
||||
{
|
||||
result = MultiplyMod(result, baseVal, modulus);
|
||||
}
|
||||
|
||||
remainingExponent >>= 1;
|
||||
if (remainingExponent == 0UL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
baseVal = MultiplyMod(baseVal, baseVal, modulus);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ulong Pow2ModPrecomputedTable(ulong exponent, ulong modulus)
|
||||
{
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
if (exponent == 0UL)
|
||||
{
|
||||
return 1UL % modulus;
|
||||
}
|
||||
|
||||
Span<ulong> powers = stackalloc ulong[64];
|
||||
powers[0] = 2UL % modulus;
|
||||
int highestBit = 63 - BitOperations.LeadingZeroCount(exponent);
|
||||
|
||||
for (int index = 1; index <= highestBit; index++)
|
||||
{
|
||||
powers[index] = MultiplyMod(powers[index - 1], powers[index - 1], modulus);
|
||||
}
|
||||
|
||||
ulong result = 1UL % modulus;
|
||||
ulong remainingExponent = exponent;
|
||||
int bitIndex = 0;
|
||||
|
||||
while (remainingExponent > 0UL)
|
||||
{
|
||||
if ((remainingExponent & 1UL) != 0UL)
|
||||
{
|
||||
result = MultiplyMod(result, powers[bitIndex], modulus);
|
||||
}
|
||||
|
||||
remainingExponent >>= 1;
|
||||
bitIndex++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ulong Pow2ModBigInteger(ulong exponent, ulong modulus)
|
||||
{
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
BigInteger mod = modulus;
|
||||
BigInteger result = BigInteger.ModPow(2, exponent, mod);
|
||||
return (ulong)result;
|
||||
}
|
||||
|
||||
private static ulong MultiplyMod(ulong x, ulong y, ulong modulus)
|
||||
{
|
||||
UInt128 product = (UInt128)x * y;
|
||||
return (ulong)(product % modulus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
+94
@@ -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;
|
||||
}
|
||||
}
|
||||
+78
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using Open.Numeric.Primes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
|
||||
[ShortRunJob(RuntimeMoniker.Net80)]
|
||||
[MemoryDiagnoser]
|
||||
public class PrimeTesterBenchmarks
|
||||
{
|
||||
private HeuristicPrimeTester _cpuTester = null!;
|
||||
private HeuristicPrimeTester _gpuTester = null!;
|
||||
private ulong[] _candidates = [];
|
||||
|
||||
[ParamsSource(nameof(GetBenchmarkCases))]
|
||||
public PrimeBenchmarkCase BenchmarkCase { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
_cpuTester = new HeuristicPrimeTester();
|
||||
_gpuTester = new HeuristicPrimeTester();
|
||||
_candidates = BenchmarkCase.Candidates;
|
||||
|
||||
if (_candidates.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationToken cancellationToken = CancellationToken.None;
|
||||
_cpuTester.IsPrimeCpu(_candidates[0], cancellationToken);
|
||||
_gpuTester.IsPrimeGpu(_candidates[0]);
|
||||
_ = Prime.Numbers.IsPrime(_candidates[0]);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public int HeuristicCpu()
|
||||
{
|
||||
HeuristicPrimeTester tester = _cpuTester;
|
||||
ulong[] values = _candidates;
|
||||
CancellationToken cancellationToken = CancellationToken.None;
|
||||
int primeCount = 0;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (tester.IsPrimeCpu(values[i], cancellationToken))
|
||||
{
|
||||
primeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return primeCount;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int HeuristicGpu()
|
||||
{
|
||||
HeuristicPrimeTester tester = _gpuTester;
|
||||
ulong[] values = _candidates;
|
||||
CancellationToken cancellationToken = CancellationToken.None;
|
||||
int primeCount = 0;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (tester.IsPrimeGpu(values[i]))
|
||||
{
|
||||
primeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return primeCount;
|
||||
}
|
||||
|
||||
// This benchmark is very slow. No point in comparing it.
|
||||
// [Benchmark]
|
||||
public int NonHeuristicGpu()
|
||||
{
|
||||
// PrimeTester tester = _gpuTester;
|
||||
ulong[] values = _candidates;
|
||||
CancellationToken cancellationToken = CancellationToken.None;
|
||||
int primeCount = 0;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (PrimeTester.Exclusive.IsPrimeGpu(values[i], cancellationToken))
|
||||
{
|
||||
primeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return primeCount;
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int NonHeuristicCpu()
|
||||
{
|
||||
// PrimeTester tester = _gpuTester;
|
||||
ulong[] values = _candidates;
|
||||
CancellationToken cancellationToken = CancellationToken.None;
|
||||
int primeCount = 0;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (PrimeTester.IsPrimeCpu(values[i], cancellationToken))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
@@ -7,7 +7,24 @@ public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||
var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
|
||||
|
||||
// Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
|
||||
// {
|
||||
// Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
|
||||
// };
|
||||
|
||||
// GpuPrimeWorkLimiter.SetLimit(1);
|
||||
// benchmarks.Setup();
|
||||
|
||||
// Console.WriteLine("Press ENTER to run");
|
||||
// Console.ReadLine();
|
||||
|
||||
// foreach (int i in Enumerable.Range(0, 100_000))
|
||||
// {
|
||||
// benchmarks.MontgomeryWithPrecomputedCycle();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
@@ -10,6 +9,12 @@ public class ResidueComputationBenchmarks
|
||||
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
|
||||
public ulong Exponent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy modulo pipeline; stays in the 2.41–2.43 ns range even for the 31-bit exponent (2,147,483,647).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: Exponent 3 → 2.412 ns (1.00×), 8,191 → 2.405 ns, 131,071 → 2.416 ns, 2,147,483,647 → 2.427 ns.
|
||||
/// </remarks>
|
||||
[Benchmark(Baseline = true)]
|
||||
public (ulong Step10, ulong Step8, ulong Step3, ulong Step5) LegacyModulo()
|
||||
{
|
||||
@@ -21,6 +26,12 @@ public class ResidueComputationBenchmarks
|
||||
return (step10, step8, step3, step5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PerfectNumbers.Core.ExponentResidues.Mod10_8_5_3"/> helper; 2.80 ns at exponent 3 but stretching to 3.36 ns at the 31-bit case (1.16–1.38× slower than legacy).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Observed means: Exponent 3 → 2.799 ns (1.16×), 8,191 → 2.842 ns, 131,071 → 3.111 ns, 2,147,483,647 → 3.355 ns.
|
||||
/// </remarks>
|
||||
[Benchmark]
|
||||
public (ulong Step10, ulong Step8, ulong Step3, ulong Step5) ModMethodModulo()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Open.Numeric.Primes;
|
||||
|
||||
namespace EvenPerfectBitScanner.ResultsParser;
|
||||
@@ -150,22 +149,25 @@ internal static class Program
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong ParseRangeValue(in string option, in string value)
|
||||
{
|
||||
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
|
||||
}
|
||||
private static ulong ParseRangeValue(in string option, in string value)
|
||||
{
|
||||
// TODO: Replace TryParse with the Utf8Parser-based span helper once the results parser accepts ReadOnlySpan<char>
|
||||
// inputs so numeric option parsing matches the fastest CLI path identified in benchmarks.
|
||||
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(in string option, in string value)
|
||||
{
|
||||
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
|
||||
}
|
||||
private static int ParsePositiveInt(in string option, in string value)
|
||||
{
|
||||
// TODO: Switch to the Utf8Parser-based fast path so integer arguments avoid transient strings while parsing.
|
||||
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
@@ -176,22 +178,24 @@ internal static class Program
|
||||
out string option,
|
||||
out string value)
|
||||
{
|
||||
option = string.Empty;
|
||||
value = string.Empty;
|
||||
string current = arguments[index];
|
||||
int equalsIndex = current.IndexOf('=');
|
||||
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
|
||||
{
|
||||
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
option = string.Empty;
|
||||
value = string.Empty;
|
||||
string current = arguments[index];
|
||||
int equalsIndex = current.IndexOf('=');
|
||||
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
|
||||
{
|
||||
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
|
||||
option = current[..equalsIndex];
|
||||
value = current[(equalsIndex + 1)..];
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
// TODO: Swap this string slicing for ReadOnlySpan<char>-based parsing once the CLI helpers expose
|
||||
// Utf8Parser-compatible overloads so option handling avoids allocating substrings for every argument.
|
||||
option = current[..equalsIndex];
|
||||
value = current[(equalsIndex + 1)..];
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
@@ -226,7 +230,10 @@ internal static class Program
|
||||
|
||||
private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)
|
||||
{
|
||||
using StreamReader reader = new(inputPath);
|
||||
using StreamReader reader = new(inputPath);
|
||||
// TODO: Replace this vanilla StreamReader with a pooled FileStreamOptions + ArrayPool-backed reader so large
|
||||
// reloads reuse the benchmarked zero-allocation buffered pipeline instead of allocating new decoder buffers per
|
||||
// run.
|
||||
string? headerLine = reader.ReadLine();
|
||||
if (headerLine is null)
|
||||
{
|
||||
@@ -240,7 +247,10 @@ internal static class Program
|
||||
|
||||
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
|
||||
|
||||
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
|
||||
// TODO: Stream candidates through a pooled buffer pipeline so we can filter and dispatch without allocating
|
||||
// the entire list upfront; the benchmarks showed that retaining every row simultaneously increases GC pressure
|
||||
// during large result replays.
|
||||
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
|
||||
Console.WriteLine("Building sorted prime results...");
|
||||
List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
|
||||
Console.WriteLine("Building RAW prime results...");
|
||||
@@ -304,23 +314,29 @@ internal static class Program
|
||||
throw new FormatException("Input line does not contain expected comma separators.");
|
||||
}
|
||||
|
||||
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
// TODO: Replace ulong.Parse with the Utf8Parser-based span helper so CSV reload stays on the zero-allocation
|
||||
// path identified as fastest in the shared CLI benchmarks.
|
||||
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
if (p < pMin || p > pMax)
|
||||
{
|
||||
return CandidateResult.Empty;
|
||||
}
|
||||
|
||||
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
|
||||
// TODO: Swap bool.Parse for the Utf8Parser-powered boolean reader once exposed so reload avoids allocating
|
||||
// temporary strings per record.
|
||||
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
|
||||
return new CandidateResult(p, passedAllTests, line);
|
||||
}
|
||||
|
||||
private static string BuildOutputPath(in string inputPath, in string prefix)
|
||||
{
|
||||
string fileName = Path.GetFileName(inputPath);
|
||||
string prefixedName = prefix + fileName;
|
||||
string? directory = Path.GetDirectoryName(inputPath);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
private static string BuildOutputPath(in string inputPath, in string prefix)
|
||||
{
|
||||
// TODO: Switch this path composition to Span<char>-based stack buffers once we expose the pooled
|
||||
// formatter helpers so output names avoid intermediate strings for directory/file splits.
|
||||
string fileName = Path.GetFileName(inputPath);
|
||||
string prefixedName = prefix + fileName;
|
||||
string? directory = Path.GetDirectoryName(inputPath);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return prefixedName;
|
||||
}
|
||||
|
||||
@@ -329,39 +345,49 @@ internal static class Program
|
||||
|
||||
private static void WriteCsv(in string path, in string header, bool append, IEnumerable<CandidateResult> entries)
|
||||
{
|
||||
using StreamWriter writer = new(path, append, Encoding.UTF8);
|
||||
if (!append)
|
||||
{
|
||||
writer.WriteLine(header);
|
||||
}
|
||||
using StreamWriter writer = new(path, append, Encoding.UTF8);
|
||||
// TODO: Switch WriteCsv to a span-based Utf8Formatter pipeline backed by ArrayPool-rented buffers so
|
||||
// appends reuse the zero-allocation formatting path highlighted in the results writer benchmarks.
|
||||
if (!append)
|
||||
{
|
||||
writer.WriteLine(header);
|
||||
}
|
||||
|
||||
foreach (CandidateResult entry in entries)
|
||||
{
|
||||
writer.WriteLine(entry.Csv);
|
||||
}
|
||||
}
|
||||
foreach (CandidateResult entry in entries)
|
||||
{
|
||||
// TODO: Replace this per-entry WriteLine with a batched chunk writer that reuses pooled
|
||||
// StringBuilder instances so large files avoid the per-line flush overhead observed in the
|
||||
// chunked writer benchmarks.
|
||||
writer.WriteLine(entry.Csv);
|
||||
}
|
||||
|
||||
private static List<CandidateResult> LoadCandidates(
|
||||
}
|
||||
|
||||
private static List<CandidateResult> LoadCandidates(
|
||||
StreamReader reader,
|
||||
ulong pMin,
|
||||
ulong pMax,
|
||||
int threadCount,
|
||||
int bufferSize)
|
||||
{
|
||||
List<CandidateResult> candidates = [];
|
||||
if (bufferSize < 1)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
List<CandidateResult> candidates = [];
|
||||
if (bufferSize < 1)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
int workerLimit = Math.Max(1, threadCount);
|
||||
List<Task<ChunkResult>> activeChunks = new(workerLimit);
|
||||
// TODO: Replace the Task-based scheduling with the pooled work queue used in the scanner so candidate
|
||||
// parsing leverages the same low-overhead thread handoff profiled as fastest in the CLI benchmarks.
|
||||
List<Task<ChunkResult>> activeChunks = new(workerLimit);
|
||||
Dictionary<int, ChunkResult> completedChunks = new();
|
||||
int nextChunkToFlush = 0;
|
||||
int consoleProgress = 0;
|
||||
ulong lastReportedCandidate = 0UL;
|
||||
|
||||
string[] buffer = new string[bufferSize];
|
||||
// TODO: Rent this line buffer from ArrayPool<string> to eliminate repeated allocations when scanning
|
||||
// large result files.
|
||||
string[] buffer = new string[bufferSize];
|
||||
int buffered = 0;
|
||||
int chunkIndex = 0;
|
||||
|
||||
@@ -420,14 +446,18 @@ internal static class Program
|
||||
void DispatchBuffer(int length)
|
||||
{
|
||||
string[] chunkLines;
|
||||
if (length == buffer.Length)
|
||||
{
|
||||
chunkLines = buffer;
|
||||
buffer = new string[bufferSize];
|
||||
}
|
||||
if (length == buffer.Length)
|
||||
{
|
||||
chunkLines = buffer;
|
||||
// TODO: Rent this full-capacity buffer from ArrayPool<string> and reuse it between reads instead
|
||||
// of allocating a fresh array each time steady-state batches are dispatched.
|
||||
buffer = new string[bufferSize];
|
||||
}
|
||||
else
|
||||
{
|
||||
chunkLines = new string[length];
|
||||
// TODO: Rent the partial chunk buffer from ArrayPool<string> instead of allocating a fresh
|
||||
// array for every remainder dispatch.
|
||||
chunkLines = new string[length];
|
||||
Array.Copy(buffer, chunkLines, length);
|
||||
}
|
||||
|
||||
@@ -466,7 +496,9 @@ internal static class Program
|
||||
|
||||
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
|
||||
{
|
||||
CandidateResult[] items = new CandidateResult[lines.Length];
|
||||
// TODO: Rent the candidate array from ArrayPool<CandidateResult> so chunk processing remains allocation-free
|
||||
// for hot reload paths.
|
||||
CandidateResult[] items = new CandidateResult[lines.Length];
|
||||
int count = 0;
|
||||
ulong pEmpty = CandidateResult.Empty.P;
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
@@ -496,11 +528,15 @@ internal static class Program
|
||||
return [];
|
||||
}
|
||||
|
||||
List<CandidateResult> sortedCandidates = [.. candidates];
|
||||
// TODO: Replace this copy with an in-place filtering pipeline that reuses pooled buffers; duplicating the list
|
||||
// doubled working-set usage in the reload benchmarks when processing multi-million-entry snapshots.
|
||||
List<CandidateResult> sortedCandidates = [.. candidates];
|
||||
sortedCandidates.Sort(CandidateResultComparer.Instance);
|
||||
|
||||
List<CandidateResult> primeCandidates = [];
|
||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||
List<CandidateResult> primeCandidates = [];
|
||||
// TODO: Keep using the Open.Numeric prime enumerator for the merge walk but hoist a shared instance so reloads
|
||||
// reuse it instead of allocating a brand new enumerator for every pass.
|
||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||
if (!primeEnumerator.MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Prime generator did not provide any values.");
|
||||
@@ -539,7 +575,9 @@ internal static class Program
|
||||
return [];
|
||||
}
|
||||
|
||||
Dictionary<ulong, int> remainingCounts = new(primeCount);
|
||||
// TODO: Replace this Dictionary allocation with the pooled span-based frequency map once the ValueListBuilder
|
||||
// helper lands so raw prime reconstruction stops allocating temporary hash tables for every reload.
|
||||
Dictionary<ulong, int> remainingCounts = new(primeCount);
|
||||
for (int i = 0; i < primeCount; i++)
|
||||
{
|
||||
CandidateResult candidate = sortedPrimeResults[i];
|
||||
@@ -552,7 +590,9 @@ internal static class Program
|
||||
remainingCounts.Add(candidate.P, 1);
|
||||
}
|
||||
|
||||
List<CandidateResult> rawPrimes = new(primeCount);
|
||||
// TODO: Rent the raw prime accumulator from ArrayPool<List<CandidateResult>> or migrate to a pooled struct
|
||||
// builder so large reloads avoid repeated List allocations along this path.
|
||||
List<CandidateResult> rawPrimes = new(primeCount);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
CandidateResult candidate = candidates[i];
|
||||
@@ -598,10 +638,10 @@ internal static class Program
|
||||
return;
|
||||
}
|
||||
|
||||
if (threadCount <= 1 || count < threadCount * 4)
|
||||
{
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
if (threadCount <= 1 || count < threadCount * 4)
|
||||
{
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CandidateResult candidate = sortedResults[i];
|
||||
@@ -618,13 +658,20 @@ internal static class Program
|
||||
}
|
||||
|
||||
int workerCount = Math.Min(threadCount, count);
|
||||
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
|
||||
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
|
||||
Task[] tasks = new Task[workerCount];
|
||||
// TODO: Rent these segment arrays from ArrayPool<List<CandidateResult>> so parallel splitting avoids allocating
|
||||
// new jagged arrays on every reload; the pooled work queue already used by the scanner benchmarks faster.
|
||||
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
|
||||
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
|
||||
Task[] tasks = new Task[workerCount];
|
||||
// TODO: Rent the tasks array from ArrayPool<Task> so repeated reloads avoid allocating new
|
||||
// scheduling buffers; the pooled queue used by the scanner benchmarks noticeably faster.
|
||||
|
||||
int start = 0;
|
||||
int baseChunk = count / workerCount;
|
||||
int remainder = count % workerCount;
|
||||
int start = 0;
|
||||
// TODO: Replace the separate division/modulo here with Math.DivRem (or the branchless chunk
|
||||
// planner from the parallel partition benchmarks) so we avoid paying for two divisions when
|
||||
// distributing work across threads.
|
||||
int baseChunk = count / workerCount;
|
||||
int remainder = count % workerCount;
|
||||
for (int worker = 0; worker < workerCount; worker++)
|
||||
{
|
||||
int length = baseChunk + (worker < remainder ? 1 : 0);
|
||||
@@ -640,10 +687,15 @@ internal static class Program
|
||||
}
|
||||
|
||||
int workerIndex = worker;
|
||||
// TODO: Replace Task.Run with the pooled work queue from the scanner so scheduling
|
||||
// overhead matches the benchmarked fast path instead of spawning transient threads.
|
||||
tasks[workerIndex] = Task.Run(() =>
|
||||
{
|
||||
List<CandidateResult> localPassed = new(length);
|
||||
List<CandidateResult> localRejected = new(length);
|
||||
// TODO: Rent these per-worker lists from a dedicated pool so parallel splits reuse
|
||||
// pre-sized buffers; allocating new List instances showed up heavily in the multi-
|
||||
// threaded reload benchmarks once files crossed tens of millions of entries.
|
||||
int end = chunkStart + length;
|
||||
for (int i = chunkStart; i < end; i++)
|
||||
{
|
||||
@@ -664,10 +716,13 @@ internal static class Program
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
for (int worker = 0; worker < workerCount; worker++)
|
||||
{
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
// TODO: Promote these final result lists to pooled builders so we can reuse the buffers
|
||||
// across reloads; the pooled aggregators from the scanner benchmarks avoid repeated large
|
||||
// allocations during long parsing sessions.
|
||||
for (int worker = 0; worker < workerCount; worker++)
|
||||
{
|
||||
if (passedSegments[worker].Count > 0)
|
||||
{
|
||||
passedResults.AddRange(passedSegments[worker]);
|
||||
|
||||
@@ -1,22 +1,50 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
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]
|
||||
@@ -34,95 +62,84 @@ public class ProgramTests
|
||||
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);
|
||||
|
||||
@@ -134,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
|
||||
@@ -168,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
|
||||
@@ -205,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
|
||||
@@ -241,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
|
||||
@@ -279,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,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
|
||||
@@ -320,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
|
||||
@@ -358,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,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,
|
||||
@@ -386,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
|
||||
@@ -396,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,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,
|
||||
@@ -428,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
|
||||
@@ -438,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();
|
||||
}
|
||||
}
|
||||
@@ -503,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
|
||||
@@ -533,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
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))
|
||||
{
|
||||
startPrime = Utf8CliParser.ParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1));
|
||||
startPrimeProvided = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--max-prime=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
|
||||
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(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
|
||||
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(argument.IndexOf('=') + 1), out ulong parsedLimit))
|
||||
{
|
||||
divisorCyclesSearchLimit = parsedLimit;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedResidueMax))
|
||||
{
|
||||
residueKMax = parsedResidueMax;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
useMersenneOnGpu = !argument.AsSpan(argument.IndexOf('=') + 1).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(argument.IndexOf('=') + 1), 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(argument.IndexOf('=') + 1);
|
||||
nttBackend = value.Equals("staged", StringComparison.OrdinalIgnoreCase) ? NttBackend.Staged : NttBackend.Reference;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--mod-reduction=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
|
||||
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(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
useOrderOnGpu = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--rle-blacklist=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
rleBlacklistPath = argument[(argument.IndexOf('=') + 1)..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedMax))
|
||||
{
|
||||
rleHardMaxP = parsedMax;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--rle-only-last7=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
|
||||
rleOnlyLast7 = !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (Utf8CliParser.TryParseDouble(argument.AsSpan(argument.IndexOf('=') + 1), out double zeroFraction))
|
||||
{
|
||||
zeroFractionHard = zeroFraction;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--zero-conj=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
|
||||
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(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--primes=", StringComparison.OrdinalIgnoreCase) ||
|
||||
argument.StartsWith("--gpu-kernels=", StringComparison.OrdinalIgnoreCase) ||
|
||||
argument.StartsWith("--accelerator=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
forcePrimeKernelsOnCpu = argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--results-dir=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
resultsDirectory = argument[(argument.IndexOf('=') + 1)..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--results-prefix=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
resultsPrefix = argument[(argument.IndexOf('=') + 1)..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
gpuPrimeThreads = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
gpuPrimeBatch = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sliceSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
scanBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
blockSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filterFile = argument[(argument.IndexOf('=') + 1)..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
writeBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--divisor-cycles=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cyclesPath = argument[(argument.IndexOf('=') + 1)..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--divisor-cycles-device=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
useGpuCycles = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argument.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cyclesBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
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> span;
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
span = line.AsSpan().Trim();
|
||||
if (span.IsEmpty || span[0] == '#')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int index = 0;
|
||||
while (index < span.Length)
|
||||
{
|
||||
if (!char.IsDigit(span[index]))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
while (index < span.Length && char.IsDigit(span[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
if (Utf8CliParser.TryParseUInt64(span[start..index], out ulong parsed))
|
||||
{
|
||||
if (!maxPrimeConfigured || (UInt128)parsed <= maxPrimeLimit)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+535
-1858
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests;
|
||||
public class AlphaCacheTests
|
||||
{
|
||||
[Fact(Skip = "TODO: implement test")]
|
||||
[Trait("Category", "Fast")]
|
||||
public void Placeholder()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests;
|
||||
public class AlphaCalculationsTests
|
||||
{
|
||||
[Fact(Skip = "TODO: implement test")]
|
||||
[Trait("Category", "Fast")]
|
||||
public void Placeholder()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace PerfectNumbers.Core.Tests;
|
||||
|
||||
[Trait("Category", "Fast")]
|
||||
public class EulerPrimeTesterTests
|
||||
{
|
||||
private readonly EulerPrimeTester _tester = new(new PrimeTester());
|
||||
|
||||
[Theory]
|
||||
[InlineData(13UL, true)]
|
||||
[InlineData(29UL, true)]
|
||||
[InlineData(3UL, false)]
|
||||
[InlineData(21UL, false)]
|
||||
[InlineData(1UL, false)]
|
||||
public void IsEulerPrime_returns_expected(ulong n, bool expected)
|
||||
{
|
||||
_tester.IsEulerPrime(n).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,15 @@ public class GpuContextPoolTests
|
||||
{
|
||||
GpuContextPool.DisposeAll();
|
||||
|
||||
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
|
||||
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
|
||||
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
try
|
||||
{
|
||||
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
GpuContextPool.DisposeAll();
|
||||
}
|
||||
@@ -30,17 +36,27 @@ public class GpuContextPoolTests
|
||||
|
||||
Context firstContext;
|
||||
Accelerator firstAccelerator;
|
||||
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
|
||||
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
try
|
||||
{
|
||||
firstContext = lease.Context;
|
||||
firstAccelerator = lease.Accelerator;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
|
||||
lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
try
|
||||
{
|
||||
lease.Context.Should().BeSameAs(firstContext);
|
||||
lease.Accelerator.Should().BeSameAs(firstAccelerator);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
GpuContextPool.DisposeAll();
|
||||
}
|
||||
@@ -52,17 +68,27 @@ public class GpuContextPoolTests
|
||||
GpuContextPool.DisposeAll();
|
||||
|
||||
Context firstContext;
|
||||
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
|
||||
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
try
|
||||
{
|
||||
firstContext = lease.Context;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
GpuContextPool.DisposeAll();
|
||||
|
||||
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
|
||||
lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
try
|
||||
{
|
||||
lease.Context.Should().NotBeSameAs(firstContext);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
GpuContextPool.DisposeAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using ILGPU;
|
||||
using ILGPU.Runtime;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
using Xunit;
|
||||
|
||||
namespace PerfectNumbers.Core.Tests.Gpu;
|
||||
|
||||
[Collection("GpuNtt")]
|
||||
public class GpuKernelCompilationTests
|
||||
{
|
||||
private static readonly MethodInfo DivisorCycleCacheLoadKernelMethod =
|
||||
typeof(DivisorCycleCache).GetMethod("LoadKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("DivisorCycleCache.LoadKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetBitReverseKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetBitReverseKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetBitReverseKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetMulKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetMulKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetMulKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetStageKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetStageKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetStageKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetScaleKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetScaleKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetScaleKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetStageMontKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetStageMontKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetStageMontKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetStageBarrettKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetStageBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetStageBarrett128Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetScaleBarrettKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetScaleBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetScaleBarrett128Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetSquareBarrettKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetSquareBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetSquareBarrett128Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetToMont64KernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetToMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetToMont64Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetFromMont64KernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetFromMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetFromMont64Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetSquareMont64KernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetSquareMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetSquareMont64Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetScaleMont64KernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetScaleMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetScaleMont64Kernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetForwardKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetForwardKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetForwardKernel not found.");
|
||||
|
||||
private static readonly MethodInfo NttGetInverseKernelMethod =
|
||||
typeof(NttGpuMath).GetMethod("GetInverseKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("NttGpuMath.GetInverseKernel not found.");
|
||||
|
||||
private static readonly MethodInfo DivisorByDivisorGetKernelMethod =
|
||||
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetKernel not found.");
|
||||
|
||||
private static readonly MethodInfo DivisorTesterGetKernelMethod =
|
||||
typeof(MersenneNumberDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberDivisorGpuTester.GetKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetAddSmallKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetAddSmallKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetAddSmallKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetSubSmallKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetSubSmallKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetSubSmallKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetReduceKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetReduceKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetReduceKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetIsZeroKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetIsZeroKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetIsZeroKernel not found.");
|
||||
|
||||
private static readonly MethodInfo LucasLehmerGetBatchKernelMethod =
|
||||
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetBatchKernel", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetBatchKernel not found.");
|
||||
|
||||
private static readonly MethodInfo PrimeOrderGetPartialFactorKernelMethod =
|
||||
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPartialFactorKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPartialFactorKernel not found.");
|
||||
|
||||
private static readonly MethodInfo PrimeOrderGetOrderKernelMethod =
|
||||
typeof(PrimeOrderGpuHeuristics).GetMethod("GetOrderKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetOrderKernel not found.");
|
||||
|
||||
private static readonly MethodInfo PrimeOrderGetPow2ModKernelMethod =
|
||||
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPow2ModKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPow2ModKernel not found.");
|
||||
|
||||
private static readonly MethodInfo PrimeOrderGetPow2ModWideKernelMethod =
|
||||
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPow2ModWideKernel", BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPow2ModWideKernel not found.");
|
||||
|
||||
private static readonly Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> MersenneDivisorCyclesKernel =
|
||||
DivisorCycleKernels.GpuDivisorCycleKernel;
|
||||
|
||||
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>> PrimeTesterSmallPrimeKernel =
|
||||
PrimeTesterKernels.SmallPrimeSieveKernel;
|
||||
|
||||
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSharesFactorKernel =
|
||||
PrimeTesterKernels.SharesFactorKernel;
|
||||
|
||||
public static IEnumerable<object[]> KernelLoaders()
|
||||
{
|
||||
yield return Loader("DivisorCycleCache.GpuAdvanceDivisorCyclesKernel", CompileDivisorCycleCacheKernel);
|
||||
yield return Loader("NttGpuMath.BitReverseKernel", CompileNttBitReverseKernel);
|
||||
yield return Loader("NttGpuMath.MulKernel", CompileNttMulKernel);
|
||||
yield return Loader("NttGpuMath.StageKernel", CompileNttStageKernel);
|
||||
yield return Loader("NttGpuMath.ScaleKernel", CompileNttScaleKernel);
|
||||
yield return Loader("NttGpuMath.StageMontKernel", CompileNttStageMontKernel);
|
||||
yield return Loader("NttGpuMath.StageBarrett128Kernel", CompileNttStageBarrettKernel);
|
||||
yield return Loader("NttGpuMath.ScaleBarrett128Kernel", CompileNttScaleBarrettKernel);
|
||||
yield return Loader("NttGpuMath.SquareBarrett128Kernel", CompileNttSquareBarrettKernel);
|
||||
yield return Loader("NttGpuMath.ToMont64Kernel", CompileNttToMont64Kernel);
|
||||
yield return Loader("NttGpuMath.FromMont64Kernel", CompileNttFromMont64Kernel);
|
||||
yield return Loader("NttGpuMath.SquareMont64Kernel", CompileNttSquareMont64Kernel);
|
||||
yield return Loader("NttGpuMath.ScaleMont64Kernel", CompileNttScaleMont64Kernel);
|
||||
yield return Loader("NttGpuMath.ForwardKernel", CompileNttForwardKernel);
|
||||
yield return Loader("NttGpuMath.InverseKernel", CompileNttInverseKernel);
|
||||
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.CheckKernel", CompileDivisorByDivisorCheckKernel);
|
||||
yield return Loader("MersenneNumberDivisorGpuTester.Kernel", CompileDivisorTesterKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.Kernel", CompileLucasLehmerKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.AddSmallKernel", CompileLucasLehmerAddSmallKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.SubtractSmallKernel", CompileLucasLehmerSubSmallKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.ReduceModMersenneKernel", CompileLucasLehmerReduceKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.IsZeroKernel", CompileLucasLehmerIsZeroKernel);
|
||||
yield return Loader("MersenneNumberLucasLehmerGpuTester.KernelBatch", CompileLucasLehmerBatchKernel);
|
||||
yield return Loader("PrimeOrderGpuHeuristics.PartialFactorKernel", CompilePrimeOrderPartialFactorKernel);
|
||||
yield return Loader("PrimeOrderGpuHeuristics.CalculateOrderKernel", CompilePrimeOrderKernel);
|
||||
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernel", CompilePrimeOrderPow2ModKernel);
|
||||
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernelWide", CompilePrimeOrderPow2ModWideKernel);
|
||||
yield return Loader("DivisorCycleKernels.GpuDivisorCycleKernel", CompileMersenneDivisorCyclesKernel);
|
||||
yield return Loader("PrimeTesterKernels.SmallPrimeSieveKernel", CompilePrimeTesterSmallPrimeKernel);
|
||||
yield return Loader("PrimeTesterKernels.SharesFactorKernel", CompilePrimeTesterSharesFactorKernel);
|
||||
yield return Loader("GpuKernelPool.KernelLeaseKernels", CompileGpuKernelPoolKernels);
|
||||
}
|
||||
|
||||
private static object[] Loader(string name, Action<Accelerator> loader)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CompileDivisorCycleCacheKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = DivisorCycleCacheLoadKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttBitReverseKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetBitReverseKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttMulKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetMulKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttStageKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetStageKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttScaleKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetScaleKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttStageMontKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetStageMontKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttStageBarrettKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetStageBarrettKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttScaleBarrettKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetScaleBarrettKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttSquareBarrettKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetSquareBarrettKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttToMont64Kernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetToMont64KernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttFromMont64Kernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetFromMont64KernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttSquareMont64Kernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetSquareMont64KernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttScaleMont64Kernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetScaleMont64KernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttForwardKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetForwardKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileNttInverseKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = NttGetInverseKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileDivisorByDivisorCheckKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberDivisorByDivisorGpuTester();
|
||||
_ = DivisorByDivisorGetKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileDivisorTesterKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberDivisorGpuTester();
|
||||
_ = DivisorTesterGetKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerAddSmallKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetAddSmallKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerSubSmallKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetSubSmallKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerReduceKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetReduceKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerIsZeroKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetIsZeroKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileLucasLehmerBatchKernel(Accelerator accelerator)
|
||||
{
|
||||
var tester = new MersenneNumberLucasLehmerGpuTester();
|
||||
_ = LucasLehmerGetBatchKernelMethod.Invoke(tester, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompilePrimeOrderPartialFactorKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = PrimeOrderGetPartialFactorKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompilePrimeOrderKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = PrimeOrderGetOrderKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompilePrimeOrderPow2ModKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = PrimeOrderGetPow2ModKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompilePrimeOrderPow2ModWideKernel(Accelerator accelerator)
|
||||
{
|
||||
_ = PrimeOrderGetPow2ModWideKernelMethod.Invoke(null, new object[] { accelerator });
|
||||
}
|
||||
|
||||
private static void CompileMersenneDivisorCyclesKernel(Accelerator accelerator)
|
||||
{
|
||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(MersenneDivisorCyclesKernel);
|
||||
_ = kernel;
|
||||
}
|
||||
|
||||
private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator)
|
||||
{
|
||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel);
|
||||
_ = kernel;
|
||||
}
|
||||
|
||||
private static void CompilePrimeTesterSharesFactorKernel(Accelerator accelerator)
|
||||
{
|
||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(PrimeTesterSharesFactorKernel);
|
||||
_ = kernel;
|
||||
}
|
||||
|
||||
private static void CompileGpuKernelPoolKernels(Accelerator accelerator)
|
||||
{
|
||||
_ = accelerator;
|
||||
var lease = GpuKernelPool.GetKernel(useGpuOrder: false);
|
||||
try
|
||||
{
|
||||
_ = lease.OrderKernel;
|
||||
_ = lease.IncrementalKernel;
|
||||
_ = lease.Pow2ModKernel;
|
||||
_ = lease.IncrementalOrderKernel;
|
||||
_ = lease.Pow2ModOrderKernel;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
|
||||
var orderLease = GpuKernelPool.GetKernel(useGpuOrder: true);
|
||||
try
|
||||
{
|
||||
_ = orderLease.OrderKernel;
|
||||
_ = orderLease.IncrementalKernel;
|
||||
_ = orderLease.Pow2ModKernel;
|
||||
_ = orderLease.IncrementalOrderKernel;
|
||||
_ = orderLease.Pow2ModOrderKernel;
|
||||
}
|
||||
finally
|
||||
{
|
||||
orderLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using FluentAssertions;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
using Xunit;
|
||||
|
||||
namespace PerfectNumbers.Core.Tests.Gpu;
|
||||
|
||||
public class GpuRationalTests
|
||||
{
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void Constructor_ReducesFraction()
|
||||
{
|
||||
GpuRational value = new(6UL, 8UL);
|
||||
value.Numerator.Should().Be(new GpuUInt128(3UL));
|
||||
value.Denominator.Should().Be(new GpuUInt128(4UL));
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void Addition_ReturnsExpectedValue()
|
||||
{
|
||||
GpuRational left = new(1UL, 3UL);
|
||||
GpuRational right = new(1UL, 6UL);
|
||||
|
||||
GpuRational sum = left + right;
|
||||
|
||||
sum.Numerator.Should().Be(new GpuUInt128(1UL));
|
||||
sum.Denominator.Should().Be(new GpuUInt128(2UL));
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void Subtraction_ReturnsExpectedValue()
|
||||
{
|
||||
GpuRational left = new(5UL, 6UL);
|
||||
GpuRational right = new(1UL, 6UL);
|
||||
|
||||
GpuRational difference = left - right;
|
||||
|
||||
difference.Numerator.Should().Be(new GpuUInt128(2UL));
|
||||
difference.Denominator.Should().Be(new GpuUInt128(3UL));
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void Multiplication_ReturnsExpectedValue()
|
||||
{
|
||||
GpuRational left = new(3UL, 4UL);
|
||||
GpuRational right = new(2UL, 3UL);
|
||||
|
||||
GpuRational product = left * right;
|
||||
|
||||
product.Numerator.Should().Be(new GpuUInt128(1UL));
|
||||
product.Denominator.Should().Be(new GpuUInt128(2UL));
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void Division_ReturnsExpectedValue()
|
||||
{
|
||||
GpuRational left = new(3UL, 5UL);
|
||||
GpuRational right = new(9UL, 10UL);
|
||||
|
||||
GpuRational quotient = left / right;
|
||||
|
||||
quotient.Numerator.Should().Be(new GpuUInt128(2UL));
|
||||
quotient.Denominator.Should().Be(new GpuUInt128(3UL));
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void CompareTo_DetectsOrdering()
|
||||
{
|
||||
GpuRational smaller = new(1UL, 4UL);
|
||||
GpuRational larger = new(2UL, 3UL);
|
||||
|
||||
smaller.CompareTo(larger).Should().BeLessThan(0);
|
||||
larger.CompareTo(smaller).Should().BeGreaterThan(0);
|
||||
smaller.CompareTo(new GpuRational(1UL, 4UL)).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact, Trait("Category", "Fast")]
|
||||
public void ToDouble_ReturnsExpectedResult()
|
||||
{
|
||||
GpuRational value = new(1UL, 5UL);
|
||||
value.ToDouble().Should().BeApproximately(0.2, 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -79,29 +79,7 @@ public class GpuUInt128Tests
|
||||
var a = new GpuUInt128(aVal);
|
||||
var b = new GpuUInt128(bVal);
|
||||
UInt128 expected = ((UInt128)aVal * bVal) % modulus;
|
||||
a.MulMod(b, modulus);
|
||||
((UInt128)a).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
ulong result = a.MulMod(b, modulus);
|
||||
((UInt128)result).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,29 +150,36 @@ public class NttGpuMathTests
|
||||
var modulus = new GpuUInt128(0UL, 17UL);
|
||||
var primitiveRoot = new GpuUInt128(0UL, 3UL);
|
||||
|
||||
using var gpu = GpuContextPool.Rent();
|
||||
var accelerator = gpu.Accelerator;
|
||||
|
||||
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
|
||||
var cacheType = cache.GetType();
|
||||
|
||||
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
|
||||
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
|
||||
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
|
||||
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
|
||||
|
||||
int count = 7; // length 8 -> 7 twiddles
|
||||
var forward = new GpuUInt128[count];
|
||||
var inverse = new GpuUInt128[count];
|
||||
twiddlesMont.CopyToCPU(forward);
|
||||
twiddlesInvMont.CopyToCPU(inverse);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
GpuContextPool.GpuContextLease gpu = GpuContextPool.Rent();
|
||||
try
|
||||
{
|
||||
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
|
||||
prod.Should().Be(r2);
|
||||
var accelerator = gpu.Accelerator;
|
||||
|
||||
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
|
||||
var cacheType = cache.GetType();
|
||||
|
||||
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
|
||||
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
|
||||
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
|
||||
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
|
||||
|
||||
int count = 7; // length 8 -> 7 twiddles
|
||||
var forward = new GpuUInt128[count];
|
||||
var inverse = new GpuUInt128[count];
|
||||
twiddlesMont.CopyToCPU(forward);
|
||||
twiddlesInvMont.CopyToCPU(inverse);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
|
||||
prod.Should().Be(r2);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gpu.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,12 +345,23 @@ public class NttGpuMathTests
|
||||
values[1] = new GpuUInt128(0UL, 2UL);
|
||||
var modulus = new GpuUInt128(0UL, 17UL);
|
||||
var primitiveRoot = new GpuUInt128(0UL, 3UL);
|
||||
using (var gpu = GpuContextPool.Rent())
|
||||
GpuContextPool.GpuContextLease gpu = GpuContextPool.Rent();
|
||||
try
|
||||
{
|
||||
var buffer = gpu.Accelerator.Allocate1D<GpuUInt128>(values.Length);
|
||||
buffer.View.CopyFromCPU(ref values[0], values.Length);
|
||||
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
|
||||
buffer.Dispose();
|
||||
try
|
||||
{
|
||||
buffer.View.CopyFromCPU(ref values[0], values.Length);
|
||||
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
|
||||
}
|
||||
finally
|
||||
{
|
||||
buffer.Dispose();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gpu.Dispose();
|
||||
}
|
||||
|
||||
// Verify that the square cache now holds entries for the accelerator.
|
||||
|
||||
@@ -33,16 +33,23 @@ public class NttMontgomeryTests
|
||||
NttGpuMath.Square(cpu, modulus, primitiveRoot);
|
||||
|
||||
// Device execution
|
||||
using var context = GpuContextPool.Rent();
|
||||
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);
|
||||
|
||||
for (int i = 0; i < cpu.Length; i++)
|
||||
GpuContextPool.GpuContextLease context = GpuContextPool.Rent();
|
||||
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);
|
||||
|
||||
for (int i = 0; i < cpu.Length; i++)
|
||||
{
|
||||
gpu[i].Should().Be(cpu[i]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
using Xunit;
|
||||
|
||||
namespace PerfectNumbers.Core.Tests;
|
||||
|
||||
[Trait("Category", "Fast")]
|
||||
public class PrimeOrderGpuHeuristicsTests
|
||||
{
|
||||
public PrimeOrderGpuHeuristicsTests()
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.Clear();
|
||||
PrimeOrderGpuHeuristics.OverflowRegistryWide.Clear();
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2Mod_returns_overflow_for_marked_prime()
|
||||
{
|
||||
const ulong prime = 97UL;
|
||||
try
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2Mod_returns_success_for_supported_prime()
|
||||
{
|
||||
const ulong exponent = 138_000_001UL;
|
||||
const ulong prime = 138_000_001UL;
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Success);
|
||||
remainder.Should().Be(2UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2Mod_returns_overflow_when_modulus_exceeds_capability()
|
||||
{
|
||||
const ulong prime = 97UL;
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2Mod_returns_overflow_when_exponent_exceeds_capability()
|
||||
{
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
const ulong prime = 193UL;
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(16UL, prime, out ulong remainder, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(0UL);
|
||||
|
||||
GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, prime, out ulong followUpRemainder, divisorData);
|
||||
followUp.Should().Be(GpuPow2ModStatus.Success);
|
||||
followUpRemainder.Should().Be(16UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatch_returns_overflow_for_marked_prime()
|
||||
{
|
||||
const ulong prime = 193UL;
|
||||
try
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
|
||||
|
||||
ulong[] remainders = new ulong[2];
|
||||
ulong[] exponents = { 1UL, 2UL };
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainders[0].Should().Be(0UL);
|
||||
remainders[1].Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatch_returns_success_for_supported_prime()
|
||||
{
|
||||
ulong[] remainders = { 123UL, 456UL };
|
||||
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);
|
||||
expectedFirst.Should().Be(2UL);
|
||||
expectedSecond.Should().Be(2048UL);
|
||||
remainders[0].Should().Be(expectedFirst);
|
||||
remainders[1].Should().Be(expectedSecond);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatch_returns_overflow_when_modulus_exceeds_capability()
|
||||
{
|
||||
const ulong prime = 97UL;
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
ulong[] exponents = { 1UL, 2UL };
|
||||
ulong[] remainders = { 5UL, 6UL };
|
||||
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainders[0].Should().Be(0UL);
|
||||
remainders[1].Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatch_returns_overflow_when_any_exponent_exceeds_capability()
|
||||
{
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
ulong[] exponents = { 2UL, 16UL };
|
||||
ulong[] remainders = { 9UL, 11UL };
|
||||
|
||||
const ulong prime = 193UL;
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainders[0].Should().Be(0UL);
|
||||
remainders[1].Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModWide_returns_overflow_for_marked_prime()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 80) + 7;
|
||||
try
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistryWide[prime] = 0;
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(UInt128.One, prime, out UInt128 remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(UInt128.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModWide_returns_success_for_supported_prime()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 96) + 11;
|
||||
UInt128 exponent = ((UInt128)1 << 64) + 3;
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out UInt128 remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Success);
|
||||
UInt128 expected = (UInt128)BigInteger.ModPow(2, (BigInteger)exponent, (BigInteger)prime);
|
||||
remainder.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("193", "31")]
|
||||
[InlineData("193", "255")]
|
||||
[InlineData("79228162514264337593543950347", "36893488147419103251")]
|
||||
public void Pow2ModKernelCore_matches_cpu_windowed_path(string modulusText, string exponentText)
|
||||
{
|
||||
UInt128 modulus = UInt128.Parse(modulusText);
|
||||
UInt128 exponent = UInt128.Parse(exponentText);
|
||||
|
||||
var method = typeof(PrimeOrderGpuHeuristics).GetMethod("Pow2ModKernelCore", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
object? gpuResultObject = method!.Invoke(null, new object[] { new GpuUInt128(exponent), new GpuUInt128(modulus) });
|
||||
GpuUInt128 gpuResult = (GpuUInt128)gpuResultObject!;
|
||||
UInt128 expected = exponent.Pow2MontgomeryModWindowed(modulus);
|
||||
|
||||
((UInt128)gpuResult).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModWide_returns_overflow_when_modulus_exceeds_capability()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 80) + 9;
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 128);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(UInt128.One, prime, out UInt128 remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(UInt128.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModWide_returns_overflow_when_exponent_exceeds_capability()
|
||||
{
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(128, 4);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
UInt128 exponent = UInt128.One << 8;
|
||||
UInt128 prime = ((UInt128)1 << 70) + 3;
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out UInt128 remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(UInt128.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatchWide_returns_success_for_supported_prime()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 90) + 19;
|
||||
UInt128[] exponents = { UInt128.One, ((UInt128)1 << 64) + 5 };
|
||||
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Success);
|
||||
for (int i = 0; i < exponents.Length; i++)
|
||||
{
|
||||
UInt128 expected = (UInt128)BigInteger.ModPow(2, (BigInteger)exponents[i], (BigInteger)prime);
|
||||
remainders[i].Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatchWide_returns_overflow_when_modulus_exceeds_capability()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 88) + 13;
|
||||
UInt128[] exponents = { UInt128.One, (UInt128)5 };
|
||||
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 128);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainders[0].Should().Be(UInt128.Zero);
|
||||
remainders[1].Should().Be(UInt128.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatchWide_returns_overflow_when_any_exponent_exceeds_capability()
|
||||
{
|
||||
UInt128 prime = ((UInt128)1 << 72) + 23;
|
||||
UInt128[] exponents = { UInt128.One << 5, UInt128.One << 9 };
|
||||
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
|
||||
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(128, 6);
|
||||
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
|
||||
try
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainders[0].Should().Be(UInt128.Zero);
|
||||
remainders[1].Should().Be(UInt128.Zero);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
|
||||
}
|
||||
}
|
||||
|
||||
// This will never happen in production code because the caller ensures the remainder span is large enough.
|
||||
// [Fact]
|
||||
// public void TryPow2ModBatch_throws_when_remainder_span_is_too_small()
|
||||
// {
|
||||
// ulong[] remainders = new ulong[1];
|
||||
// ulong[] exponents = { 1UL, 2UL };
|
||||
|
||||
// Action act = () =>
|
||||
// {
|
||||
// const ulong prime = 5UL;
|
||||
// MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
// PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
|
||||
// };
|
||||
|
||||
// act.Should().Throw<ArgumentException>();
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked()
|
||||
{
|
||||
const ulong prime = 7UL;
|
||||
try
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
|
||||
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
|
||||
ulong result = PrimeOrderCalculator.Calculate(prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
|
||||
|
||||
result.Should().Be(3UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using PerfectNumbers.Core;
|
||||
using PerfectNumbers.Core.Gpu;
|
||||
using Xunit;
|
||||
|
||||
@@ -16,7 +14,7 @@ public class PrimeTesterGpuTests
|
||||
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();
|
||||
@@ -36,7 +34,7 @@ public class PrimeTesterGpuTests
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,14 +11,21 @@ public class GpuKernelPoolTests
|
||||
[Trait("Category", "Fast")]
|
||||
public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls()
|
||||
{
|
||||
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
|
||||
var accelerator = lease.Accelerator;
|
||||
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);
|
||||
second.Equals(first).Should().BeTrue();
|
||||
second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lease.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests;
|
||||
public class InputParserTests
|
||||
{
|
||||
[Fact(Skip = "TODO: implement test")]
|
||||
[Trait("Category", "Fast")]
|
||||
public void Placeholder()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests;
|
||||
public class KRangeFinderTests
|
||||
{
|
||||
[Fact(Skip = "TODO: implement test")]
|
||||
[Trait("Category", "Fast")]
|
||||
public void Placeholder()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using PerfectNumbers.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace PerfectNumbers.Core.Tests;
|
||||
@@ -41,7 +38,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
expected.Should().Contain(pair.divisor);
|
||||
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor));
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
|
||||
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -52,4 +50,58 @@ public class MersenneDivisorCyclesGenerateGpuTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Fast")]
|
||||
public void Generate_produces_unique_cycles_for_expected_divisors()
|
||||
{
|
||||
string path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4);
|
||||
|
||||
var pairs = new List<(ulong divisor, ulong cycle)>();
|
||||
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();
|
||||
ulong cycle = reader.ReadUInt64();
|
||||
pairs.Add((divisor, cycle));
|
||||
}
|
||||
|
||||
var expected = new List<ulong>();
|
||||
for (ulong divisor = 3; divisor <= 50; divisor++)
|
||||
{
|
||||
if ((divisor & 1UL) == 0UL || divisor % 3UL == 0UL || divisor % 5UL == 0UL || divisor % 7UL == 0UL || divisor % 11UL == 0UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
expected.Add(divisor);
|
||||
}
|
||||
|
||||
pairs.Should().HaveCount(expected.Count);
|
||||
|
||||
HashSet<ulong> uniqueDivisors = new();
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
uniqueDivisors.Add(pair.divisor).Should().BeTrue();
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
|
||||
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
|
||||
}
|
||||
|
||||
uniqueDivisors.Should().BeEquivalentTo(expected);
|
||||
|
||||
var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
|
||||
completeCount.Should().Be(expected.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ public class MersenneDivisorCyclesTests
|
||||
[MemberData(nameof(Divisors))]
|
||||
public void CalculateCycleLength_returns_expected_value(ulong divisor, ulong expected)
|
||||
{
|
||||
MersenneDivisorCycles.CalculateCycleLength(divisor).Should().Be(expected);
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(divisor);
|
||||
MersenneDivisorCycles.CalculateCycleLength(divisor, divisorData).Should().Be(expected);
|
||||
MersenneDivisorCycles.CalculateCycleLengthGpu(divisor).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user