Skip to content

Switch to anderson method to speed up zetazero#959

Merged
skirpichev merged 4 commits intompmath:masterfrom
d0sboots:anderson
Jun 20, 2025
Merged

Switch to anderson method to speed up zetazero#959
skirpichev merged 4 commits intompmath:masterfrom
d0sboots:anderson

Conversation

@d0sboots
Copy link
Copy Markdown
Contributor

For the smooth nature of the zeta function, it is an unambiguous win, while maintaining the bracketing guarantee.

This closes #958. I tried a variety of optimizations; some improved only specific areas of the parameter space, others were small wins in some cases but might be considered risky. This was unambiguously better without any risk of altering the existing behavior.

For the smooth nature of the zeta function, it is an unambiguous win,
while maintaining the bracketing guarantee.
@skirpichev
Copy link
Copy Markdown
Collaborator

Could you, please, provide some benchmark, e.g. with pyperf?

@d0sboots
Copy link
Copy Markdown
Contributor Author

import pyperf
import mpmath

runner = pyperf.Runner()
for prec in [53, 100, 200, 400]:
    with mpmath.mp.workprec(prec):
        for x in [9, 99, 999, 9999]:
            runner.bench_func(f"zetazero({x}) @{prec}", mpmath.zetazero, x)

New code:

.....................
zetazero(9) @53: Mean +- std dev: 11.3 ms +- 0.1 ms
.....................
zetazero(99) @53: Mean +- std dev: 95.7 ms +- 2.1 ms
.....................
zetazero(999) @53: Mean +- std dev: 175 ms +- 2 ms
.....................
zetazero(9999) @53: Mean +- std dev: 646 ms +- 5 ms
.....................
zetazero(9) @100: Mean +- std dev: 15.0 ms +- 0.2 ms
.....................
zetazero(99) @100: Mean +- std dev: 94.6 ms +- 1.3 ms
.....................
zetazero(999) @100: Mean +- std dev: 374 ms +- 5 ms
.....................
zetazero(9999) @100: Mean +- std dev: 1.18 sec +- 0.01 sec
.....................
zetazero(9) @200: Mean +- std dev: 18.6 ms +- 0.3 ms
.....................
zetazero(99) @200: Mean +- std dev: 142 ms +- 3 ms
.....................
zetazero(999) @200: Mean +- std dev: 319 ms +- 2 ms
.....................
zetazero(9999) @200: Mean +- std dev: 1.17 sec +- 0.02 sec
.....................
zetazero(9) @400: Mean +- std dev: 26.8 ms +- 0.5 ms
.....................
zetazero(99) @400: Mean +- std dev: 163 ms +- 4 ms
.....................
zetazero(999) @400: Mean +- std dev: 521 ms +- 6 ms
.....................
zetazero(9999) @400: Mean +- std dev: 1.27 sec +- 0.02 sec

Old code:

.....................
zetazero(9) @53: Mean +- std dev: 12.8 ms +- 0.2 ms
.....................
zetazero(99) @53: Mean +- std dev: 94.4 ms +- 0.8 ms
.....................
zetazero(999) @53: Mean +- std dev: 210 ms +- 2 ms
.....................
zetazero(9999) @53: Mean +- std dev: 741 ms +- 7 ms
.....................
zetazero(9) @100: Mean +- std dev: 17.9 ms +- 0.2 ms
.....................
zetazero(99) @100: Mean +- std dev: 118 ms +- 3 ms
.....................
zetazero(999) @100: Mean +- std dev: 389 ms +- 5 ms
.....................
zetazero(9999) @100: Mean +- std dev: 1.22 sec +- 0.03 sec
.....................
zetazero(9) @200: Mean +- std dev: 20.1 ms +- 0.3 ms
.....................
zetazero(99) @200: Mean +- std dev: 165 ms +- 3 ms
.....................
zetazero(999) @200: Mean +- std dev: 334 ms +- 15 ms
.....................
zetazero(9999) @200: Mean +- std dev: 1.19 sec +- 0.01 sec
.....................
zetazero(9) @400: Mean +- std dev: 28.3 ms +- 0.4 ms
.....................
zetazero(99) @400: Mean +- std dev: 186 ms +- 3 ms
.....................
zetazero(999) @400: Mean +- std dev: 534 ms +- 4 ms
.....................
zetazero(9999) @400: Mean +- std dev: 1.39 sec +- 0.03 sec

There were various instances of WARNING: the benchmark result may be unstable. I wasn't about to add more loops, it already took too long to run all those as-is. The new method is noticeably better at small values, and never worse. An interesting quirk (reproduced in both runs) is that zetazero(999) is faster at 200 bits of precision than 100.

@ariasdereyna
Copy link
Copy Markdown
Contributor

I was checking zetazeros() in 2010.
I make many checks, also about what method to use. When I see your checks, really do not compare. For example I compute the zero 10^15:

zero number 1000000000000000 is equal to
(mpc(real='0.5', imag='208514052006405.46942460229754774510611'),
[999999999999989L,1000000000000051L], 10L,
'(1)(1)(1)(02)(1)(1)(02)(1)(1)(1)(1)(1)(1)(1)(1)(02)(1)(1)(1)(1)(1)
(22)(1)(1)(1)(1)(1)(02)(1)(22)(1)(2110)(20)(1)(22)(1)(1)(02)(02)(014)
(1)(1)(1)(1)(1)(1)(1)')
computed in time 121893.041
}
\bigskip

The time spent on the computation is
$$ 1 \text{ day } 9 \text{ hours } 51 \text{ minutes and } 33.041 \text{ seconds.}$$

It is clear that this result is not reliable.
In the program it is said that our zero is the tenth of this block.
That is the last from the sequence $(1)(1)(1)(02)(1)(1)(02)(1)$.
So it appears to be contained in a good Gram interval.

The program search for two consecutive intervals where the Theorem of Brent can be
applied. To the left may be the two Rosser Blocks with zero pattern $(02)$ were not
separated at first, so the program has to search until the first three $(1)(1)(1)$.

To the right, it appears that there were 7 consecutive good Gram intervals.
Hence it must be that he has computed to a very low resolution $Z(g_k)$
for this values of $g_k$.

But what make this not reliable is that apparently the program has separated
more zeros than are allowed.

It follows that we must change the program so that it computes for this high values
to a great precision always.

To clear the situation I see how are the Gram points in the block studied

from mpmath import *
mp.dps = 35
for n in range(89,152):
t = grampoint(999999999999900L+n)
print 'the Gram point n = ',n, ' is = ', t
b = siegelz(t) * (-1)**(n)
if b>0:
ff = 'good'
else:
ff = 'bad'
print 'the Gram point n = ',n, ' is ', ff
print b
}

\medskip

we get

{\tt \obeylines\obeyspaces

the Gram point n = 89 is = 208514052006403.4671825650459474923
the Gram point n = 89 is good
3.3191264029299344503307700734809962
the Gram point n = 90 is = 208514052006403.66899912422674863729
the Gram point n = 90 is good
1.6977575469961316392065010438057504
the Gram point n = 91 is = 208514052006403.870815683407549776
the Gram point n = 91 is good
3.9514616769020477744480177044238929
the Gram point n = 92 is = 208514052006404.07263224258835090844
the Gram point n = 92 is good
6.04680417078079976990336007961848
the Gram point n = 93 is = 208514052006404.27444880176915203461
the Gram point n = 93 is bad
-1.6522602745948695373619278483488542
the Gram point n = 94 is = 208514052006404.4762653609499531545
the Gram point n = 94 is good
0.29480098879586965288249162422080949
the Gram point n = 95 is = 208514052006404.67808192013075426812
the Gram point n = 95 is good
3.9845771981991980262343439709581364
the Gram point n = 96 is = 208514052006404.87989847931155537546
the Gram point n = 96 is good
2.1878466499405715751383291295635855
the Gram point n = 97 is = 208514052006405.08171503849235647653
the Gram point n = 97 is bad
-0.57894955668729644175526571666134868
the Gram point n = 98 is = 208514052006405.28353159767315757133
the Gram point n = 98 is good
0.47057095626362139029125732300574702
the Gram point n = 99 is = 208514052006405.48534815685395865985
the Gram point n = 99 is good
0.11649071589605951483250536628088165
the Gram point n = 100 is = 208514052006405.6871647160347597421
the Gram point n = 100 is good
0.042896588509036158609892761932448283
the Gram point n = 101 is = 208514052006405.88898127521556081807
the Gram point n = 101 is good
1.0007461443489078782953683743467554
the Gram point n = 102 is = 208514052006406.09079783439636188777
the Gram point n = 102 is good
0.4433567270251574939791915242325895
the Gram point n = 103 is = 208514052006406.29261439357716295119
the Gram point n = 103 is good
5.0635993938690770301924092859565833
the Gram point n = 104 is = 208514052006406.49443095275796400834
the Gram point n = 104 is good
2.7612480026960667726456829944885538
the Gram point n = 105 is = 208514052006406.69624751193876505922
the Gram point n = 105 is good
1.2104803997603294141394919731035059
the Gram point n = 106 is = 208514052006406.89806407111956610382
the Gram point n = 106 is good
3.1573425821150026905624301841377755
the Gram point n = 107 is = 208514052006407.09988063030036714215
the Gram point n = 107 is bad
-0.067991726465650395359616112073389581
the Gram point n = 108 is = 208514052006407.3016971894811681742
the Gram point n = 108 is good
0.49149822446398820639828802217433198
the Gram point n = 109 is = 208514052006407.50351374866196919998
the Gram point n = 109 is good
1.8815598623467886945109137516899897
the Gram point n = 110 is = 208514052006407.70533030784277021949
the Gram point n = 110 is good
0.92947942827138615577332851901682654
the Gram point n = 111 is = 208514052006407.90714686702357123272
the Gram point n = 111 is good
1.7757992023740563687791410122907561
the Gram point n = 112 is = 208514052006408.10896342620437223968
the Gram point n = 112 is good
1.2720121309045878095944802331667765
the Gram point n = 113 is = 208514052006408.31077998538517324036
the Gram point n = 113 is good
1.3898229982701858845092427682119274
the Gram point n = 114 is = 208514052006408.51259654456597423477
the Gram point n = 114 is bad
-0.88598961203788032040784900545396293
the Gram point n = 115 is = 208514052006408.71441310374677522291
the Gram point n = 115 is good
8.6989014539143112167116056132362412
the Gram point n = 116 is = 208514052006408.91622966292757620477
the Gram point n = 116 is good
0.83178168667465748892757994015949123
the Gram point n = 117 is = 208514052006409.11804622210837718035
the Gram point n = 117 is good
1.96694938931949802940675768959908
the Gram point n = 118 is = 208514052006409.31986278128917814967
the Gram point n = 118 is good
0.35708048524441378057457221033598791
the Gram point n = 119 is = 208514052006409.52167934046997911271
the Gram point n = 119 is good
3.3431643315448185326271943233337073
the Gram point n = 120 is = 208514052006409.72349589965078006947
the Gram point n = 120 is good
0.25586502411219952761191233123499955
the Gram point n = 121 is = 208514052006409.92531245883158101996
the Gram point n = 121 is bad
-0.04619163739265467603007605332528392
the Gram point n = 122 is = 208514052006410.12712901801238196418
the Gram point n = 122 is good
0.061840019852838354979376633170738668
the Gram point n = 123 is = 208514052006410.32894557719318290212
the Gram point n = 123 is good
0.053544717436482037099929591373676381
the Gram point n = 124 is = 208514052006410.53076213637398383378
the Gram point n = 124 is bad
-0.18787073679096825274922624267681829
the Gram point n = 125 is = 208514052006410.73257869555478475918
the Gram point n = 125 is good
1.4798571904808417609504262958629444
the Gram point n = 126 is = 208514052006410.9343952547355856783
the Gram point n = 126 is good
0.32351733234293718969860018643172385
the Gram point n = 127 is = 208514052006411.13621181391638659114
the Gram point n = 127 is bad
-0.63225221532556563294728004626847697
the Gram point n = 128 is = 208514052006411.33802837309718749771
the Gram point n = 128 is bad
-1.0749337701175974980318051735623525
the Gram point n = 129 is = 208514052006411.53984493227798839801
the Gram point n = 129 is bad
-6.5785074630195267541988038379024807
the Gram point n = 130 is = 208514052006411.74166149145878929203
the Gram point n = 130 is good
3.2781641068779635691566344138201019
the Gram point n = 131 is = 208514052006411.94347805063959017978
the Gram point n = 131 is bad
-6.0647635409200529872939439640034219
the Gram point n = 132 is = 208514052006412.14529460982039106126
the Gram point n = 132 is good
19.489486820777800144434441872323732
the Gram point n = 133 is = 208514052006412.34711116900119193646
the Gram point n = 133 is good
27.007960431801056011390215499794025
the Gram point n = 134 is = 208514052006412.54892772818199280538
the Gram point n = 134 is bad
-2.2332094324073340351350575345968099
the Gram point n = 135 is = 208514052006412.75074428736279366803
the Gram point n = 135 is good
4.2276139715336875653197697057976117
the Gram point n = 136 is = 208514052006412.95256084654359452441
the Gram point n = 136 is good
3.6990372974998349162630829312807457
the Gram point n = 137 is = 208514052006413.15437740572439537452
the Gram point n = 137 is good
17.581852136413391604242813896467301
the Gram point n = 138 is = 208514052006413.35619396490519621834
the Gram point n = 138 is bad
-2.2200423532824676791081460961182023
the Gram point n = 139 is = 208514052006413.5580105240859970559
the Gram point n = 139 is good
3.4435742118521529914444087248581294
the Gram point n = 140 is = 208514052006413.75982708326679788718
the Gram point n = 140 is bad
-0.053992113096473481101291793271507284
the Gram point n = 141 is = 208514052006413.96164364244759871219
the Gram point n = 141 is good
0.32366414563499707183328071390504936
the Gram point n = 142 is = 208514052006414.16346020162839953092
the Gram point n = 142 is bad
-2.9016048386548009083435634859325563
the Gram point n = 143 is = 208514052006414.36527676080920034338
the Gram point n = 143 is bad
-0.66619297645782394470796459165183479
the Gram point n = 144 is = 208514052006414.56709331999000114956
the Gram point n = 144 is good
0.048925632324183558114971841011450196
the Gram point n = 145 is = 208514052006414.76890987917080194947
the Gram point n = 145 is good
0.84718051980782114661585837638450824
the Gram point n = 146 is = 208514052006414.97072643835160274311
the Gram point n = 146 is good
0.068099593766948538194551686118013865
the Gram point n = 147 is = 208514052006415.17254299753240353047
the Gram point n = 147 is good
0.34908502538992410064389190669928496
the Gram point n = 148 is = 208514052006415.37435955671320431156
the Gram point n = 148 is good
1.6079127974736404616980416510714288
the Gram point n = 149 is = 208514052006415.57617611589400508637
the Gram point n = 149 is good
1.2016572205649880159597840280974584
the Gram point n = 150 is = 208514052006415.77799267507480585491
the Gram point n = 150 is good
2.0190879880261615849392558264897629
the Gram point n = 151 is = 208514052006415.97980923425560661718
the Gram point n = 151 is good
0.17759081444914218408734322356450503

}

We intercalate the values in the pattern of zeros:

{\tt \obeylines\obeyspaces
G(1)G(1)G(1)G(0B2)G(1)G(1)G(0B2)G(1)G(1)G(1)G(1)G(1)G(1)
G(1)G(1)G(0B2)G(1)G(1)G(1)G(1)G(1)G(2B2)G(1)G(1)G(1)G(1)
G(1)G(0B2)G(1)G(2B2)G(1)G(2B1B1B0)G(2B0)G(1)G(2B2)G(1)G(1)G(0B2)
G(0B2)G(0B1B4)G(1)G(1)G(1)G(1)G(1)G(1)G(1)G }

In this way we see that the given values are consistent. It
appear that the parity of the zeros is well computed. It
appears that only at some point has changed the sign of a
computed value.

end of quote

This is only to show you that I worked hard to get a program giving the true n-th zero and to the correct precision.

@d0sboots
Copy link
Copy Markdown
Contributor Author

I am not sure what you are saying, exactly.

I make many checks, also about what method to use. When I see your checks, really do not compare. For example I compute the zero 10^15:

zero number 1000000000000000 is equal to
(mpc(real='0.5', imag='208514052006405.46942460229754774510611'),
[999999999999989L,1000000000000051L], 10L,
'(1)(1)(1)(02)(1)(1)(02)(1)(1)(1)(1)(1)(1)(1)(1)(02)(1)(1)(1)(1)(1)
(22)(1)(1)(1)(1)(1)(02)(1)(22)(1)(2110)(20)(1)(22)(1)(1)(02)(02)(014)
(1)(1)(1)(1)(1)(1)(1)')
computed in time 121893.041
}
\bigskip

The time spent on the computation is
$$ 1 \text{ day } 9 \text{ hours } 51 \text{ minutes and } 33.041 \text{ seconds.}$$

It is clear that this result is not reliable.

Are you saying that the change I made here is flawed, and you determined this by checking the zero 10^15? But this seems impossible, because the time listed for the computation is 1 day 9 hours, which is much longer than my PR has been open. So, I must assume you mean something else.

I do not dispute that you put careful engineering work into this function. Indeed, that is why the change I made here is making a minimal disturbance, and should have no observable numerical impact. If it does, it would imply (to me) that the root-finding method itself is broken.

@skirpichev
Copy link
Copy Markdown
Collaborator

@d0sboots, did you test also 'pegasus' option?

BTW, you could use compare_to pyperf's command to do comparison, e.g. as table.

@d0sboots
Copy link
Copy Markdown
Contributor Author

d0sboots commented May 30, 2025

I'd tried pegasus already, it's not as good.

I reran all the numbers to capture as json so I could use compare_to.

zetazero(9) @53
===============

Mean +- std dev: [bench_illinois] 12.7 ms +- 0.3 ms -> [bench_pegasus] 11.2 ms +- 0.1 ms: 1.13x faster
Mean +- std dev: [bench_illinois] 12.7 ms +- 0.3 ms -> [bench_anderson] 11.3 ms +- 0.3 ms: 1.12x faster

zetazero(999) @53
=================

Mean +- std dev: [bench_illinois] 209 ms +- 4 ms -> [bench_pegasus] 198 ms +- 3 ms: 1.06x faster
Mean +- std dev: [bench_illinois] 209 ms +- 4 ms -> [bench_anderson] 175 ms +- 3 ms: 1.19x faster

zetazero(9999) @53
==================

Mean +- std dev: [bench_illinois] 739 ms +- 4 ms -> [bench_pegasus] 646 ms +- 6 ms: 1.14x faster
Mean +- std dev: [bench_illinois] 739 ms +- 4 ms -> [bench_anderson] 645 ms +- 6 ms: 1.15x faster

zetazero(9) @100
================

Mean +- std dev: [bench_illinois] 17.9 ms +- 0.4 ms -> [bench_pegasus] 15.0 ms +- 0.4 ms: 1.19x faster
Mean +- std dev: [bench_illinois] 17.9 ms +- 0.4 ms -> [bench_anderson] 15.0 ms +- 0.2 ms: 1.19x faster

zetazero(99) @100
=================

Mean +- std dev: [bench_illinois] 118 ms +- 4 ms -> [bench_pegasus] 106 ms +- 1 ms: 1.11x faster
Mean +- std dev: [bench_illinois] 118 ms +- 4 ms -> [bench_anderson] 94.6 ms +- 1.1 ms: 1.25x faster

zetazero(999) @100
==================

Mean +- std dev: [bench_illinois] 389 ms +- 5 ms -> [bench_pegasus] 412 ms +- 10 ms: 1.06x slower
Mean +- std dev: [bench_illinois] 389 ms +- 5 ms -> [bench_anderson] 376 ms +- 5 ms: 1.03x faster

zetazero(9999) @100
===================

Mean +- std dev: [bench_illinois] 1.22 sec +- 0.02 sec -> [bench_pegasus] 1.18 sec +- 0.01 sec: 1.03x faster
Mean +- std dev: [bench_illinois] 1.22 sec +- 0.02 sec -> [bench_anderson] 1.18 sec +- 0.02 sec: 1.03x faster

zetazero(9) @200
================

Mean +- std dev: [bench_illinois] 20.2 ms +- 0.5 ms -> [bench_pegasus] 18.5 ms +- 0.4 ms: 1.09x faster
Mean +- std dev: [bench_illinois] 20.2 ms +- 0.5 ms -> [bench_anderson] 18.6 ms +- 0.2 ms: 1.09x faster

zetazero(99) @200
=================

Mean +- std dev: [bench_illinois] 166 ms +- 5 ms -> [bench_pegasus] 142 ms +- 3 ms: 1.17x faster
Mean +- std dev: [bench_illinois] 166 ms +- 5 ms -> [bench_anderson] 142 ms +- 2 ms: 1.17x faster

zetazero(999) @200
==================

Mean +- std dev: [bench_illinois] 332 ms +- 7 ms -> [bench_pegasus] 321 ms +- 6 ms: 1.04x faster
Mean +- std dev: [bench_illinois] 332 ms +- 7 ms -> [bench_anderson] 320 ms +- 4 ms: 1.04x faster

zetazero(9999) @200
===================

Mean +- std dev: [bench_illinois] 1.19 sec +- 0.01 sec -> [bench_pegasus] 1.17 sec +- 0.02 sec: 1.02x faster
Mean +- std dev: [bench_illinois] 1.19 sec +- 0.01 sec -> [bench_anderson] 1.16 sec +- 0.01 sec: 1.03x faster

zetazero(9) @400
================

Mean +- std dev: [bench_illinois] 28.4 ms +- 0.4 ms -> [bench_pegasus] 27.1 ms +- 0.6 ms: 1.05x faster
Mean +- std dev: [bench_illinois] 28.4 ms +- 0.4 ms -> [bench_anderson] 27.0 ms +- 0.7 ms: 1.05x faster

zetazero(99) @400
=================

Mean +- std dev: [bench_illinois] 187 ms +- 3 ms -> [bench_pegasus] 162 ms +- 1 ms: 1.15x faster
Mean +- std dev: [bench_illinois] 187 ms +- 3 ms -> [bench_anderson] 162 ms +- 2 ms: 1.16x faster

zetazero(999) @400
==================

Mean +- std dev: [bench_illinois] 533 ms +- 6 ms -> [bench_pegasus] 521 ms +- 7 ms: 1.02x faster
Mean +- std dev: [bench_illinois] 533 ms +- 6 ms -> [bench_anderson] 522 ms +- 9 ms: 1.02x faster

zetazero(9999) @400
===================

Mean +- std dev: [bench_illinois] 1.37 sec +- 0.01 sec -> [bench_pegasus] 1.28 sec +- 0.02 sec: 1.08x faster
Mean +- std dev: [bench_illinois] 1.37 sec +- 0.01 sec -> [bench_anderson] 1.27 sec +- 0.02 sec: 1.08x faster

Benchmark hidden because not significant (1): zetazero(99) @53

Geometric mean
==============

bench_pegasus: 1.07x faster
bench_anderson: 1.10x faster

Edit: The falloff in speedup as the precision and/or zero gets larger is expected. As these parameters get larger, the initial findroot becomes a smaller part of the running time in comparison to the hand-rolled newton iterations. This is also why benchmarking larger numbers is not particularly needed.

@skirpichev
Copy link
Copy Markdown
Collaborator

BTW, it seems that change in separate_zeros_in_block() doesn't affect your tests.

@ariasdereyna
Copy link
Copy Markdown
Contributor

My point the other day is that I did a lot of checking to implement zetazeros. I see your checks only cover zeros with n<10000. This is nothing. (Also I am not fluent in English. It is difficult for me to express in English. ) I recall to select very carefully the method to use to compute to high precision the zeros.

It is many year ago that I implemented zetazeros. The main difficulty is to get just the n-th zero and not any other of the many near it. To this end the method determines first an interval [a,b] in which we know there are just N zeros, from which our zero is the m-th. The distribution of zeros of zeta is such that some zeros are very close to each other and some are not.

When we determine the adequate number of changes of sign of Z(t) we will know an interval [a',b']\subset [a,b] in which Z(t) changes sign and contains a unique zero that is our zero.

So, we have to use a method based on bisection: Illinois, Pegasus or Anderson. I recall checking all these methods (and other ones).

You have been checking some small zeros, this is not sufficient, the problem was when zeros were very near. In some cases the program returned a zero not in the interval [a',b'], that is not the n-th zero. You have to check this type of zero. Improving the time in small zeros some fraction of a second, when the main time is spent to locate the zero is not a great advance.

Therefore before change the method see that it compute well the difficult cases. I do not find my checks. I give you some cases that I know were not easy. Apart from the 10^15 zero, here are some other more easily computed:

For example
zetazero(1048449114)
mpc(real='0.5', imag='388858886.0022851217767970582610330824019119')
zetazero(1048449115)
mpc(real='0.5', imag='388858886.0023936897027167200756700895162012')

Van de Lune gives me two near zeros
zetazero(3570918901)
mpc(real='0.5', imag='1239587702.547450313754819029745631023067728')
zetazero(3570918902)
mpc(real='0.5', imag='1239587702.547523871747088188555055346198066')

Or check the zeros Gourdon find closer for n<10^13

zero number 8637740722917 is
(mpc(real='0.5', imag='2124447368584.3929646615152911269375')
zero number 8637740722918 is
(mpc(real='0.5', imag='2124447368584.3929817060386050127775')

@skirpichev
Copy link
Copy Markdown
Collaborator

I recall to select very carefully the method to use to compute to high precision the zeros.

That's something worth to be documented in code.

@d0sboots
Copy link
Copy Markdown
Contributor Author

So, we have to use a method based on bisection: Illinois, Pegasus or Anderson. I recall checking all these methods (and other ones).

You have been checking some small zeros, this is not sufficient, the problem was when zeros were very near. In some cases the program returned a zero not in the interval [a',b'], that is not the n-th zero. You have to check this type of zero. Improving the time in small zeros some fraction of a second, when the main time is spent to locate the zero is not a great advance.

The reason I did not check large zeros is because there should not be any correctness implications switching between these methods; the only implication should be performance. All of these methods are based on bisection and maintain the invariant that they keep the root bracketed, as long as there is only a single root in the interval from the start. That is the same invariant that all of them require.

Let me be more plain: If you found a correctness difference between these methods when implementing zetazeros, it either speaks to a bug in findroot (or one of its solvers), or the original invariant of having an interval bracketing a single root wasn't holding. It sounds like in some cases the latter was true (the interval was previously too wide), but if zetazeros is correct now, that is no longer the case.

If you are worried about particular difficult cases, the right answer is for those to be unittests. If they are too large to be run regularly, they can be added as skipped tests, or there are other solutions - I'm not sure how it's done in this project, but I am happy to add more tests in this PR. But relying on manual testing for correctness is not sustainable.

@d0sboots
Copy link
Copy Markdown
Contributor Author

BTW, it seems that change in separate_zeros_in_block() doesn't affect your tests.

Yes, I changed that for consistency but it only triggers on specific values, which my benchmarks don't hit. I didn't expect it to be a significant source of the runtime for the ranges where this optimization is noticable.

@d0sboots
Copy link
Copy Markdown
Contributor Author

d0sboots commented Jun 1, 2025

For example zetazero(1048449114) mpc(real='0.5', imag='388858886.0022851217767970582610330824019119') zetazero(1048449115) mpc(real='0.5', imag='388858886.0023936897027167200756700895162012')

Van de Lune gives me two near zeros zetazero(3570918901) mpc(real='0.5', imag='1239587702.547450313754819029745631023067728') zetazero(3570918902) mpc(real='0.5', imag='1239587702.547523871747088188555055346198066')

Or check the zeros Gourdon find closer for n<10^13

zero number 8637740722917 is (mpc(real='0.5', imag='2124447368584.3929646615152911269375') zero number 8637740722918 is (mpc(real='0.5', imag='2124447368584.3929817060386050127775')

In turns out that two of these were already in the unittests (the first one and the third one, although the latter is commented out for "potentially taking hours"). I added the middle one, and I'm running the commented-out ones to verify.

Edit: Pleasant surprise, only took 8 minutes each (at the default 53-bit precision of the tests). Both passed.

@skirpichev
Copy link
Copy Markdown
Collaborator

Edit: Pleasant surprise, only took 8 minutes each (at the default 53-bit precision of the tests). Both passed.

On my box these tests timeout. You might try to uncomment them to see if they are passed on CI. Though, I think it's better to have a separate test with "slow" mark.

@d0sboots
Copy link
Copy Markdown
Contributor Author

d0sboots commented Jun 1, 2025

I think I'm missing the module that causes tests to timeout. In any case, 8 minutes is far too slow to run as part of the CI; I was just happy that they ran faster than the rumored "hours".

@skirpichev skirpichev self-requested a review June 3, 2025 06:49
skirpichev added a commit to skirpichev/mpmath that referenced this pull request Jun 8, 2025
@skirpichev
Copy link
Copy Markdown
Collaborator

there is only a single root in the interval from the start.

@ariasdereyna, is this a valid assumption about your code, that calls findroot() or we miss something?

@ariasdereyna
Copy link
Copy Markdown
Contributor

@skirpichev

The short answer is yes.

The program determine first an interval (g_r, g_s] containing m zeros, we know also that our zero is the k zero of this interval.

After we separate the m zeros in the interval by locating the adequate number of changes of sign, we determine an interval (a,b)\subset(g_r,g_s] that contains only the zero we are computing. We have also a change of sign Z(a) Z(b) < 0.

At this moment is when we compute the zero to high precision.

Note that in the process of separation of the m zeros, maybe we have to increase the precision to which we are computing. Because the zeros can be very near. Frequently we are computing to a higher precision, to the one we asked for. This explain that to compute a zero, the program is faster if we use from the start a higher precision.

I do not recall why I preferred the method Illinois to Anderson. Theoretically both are good. Certainly I used Anderson on many trials.

@skirpichev
Copy link
Copy Markdown
Collaborator

The short answer is yes.

So, correctness shouldn't be an issue, right?

I tried some of your latest "uneasy cases":

Benchmark ref anderson
zetazero(1048449114) @113 11.2 sec 9.49 sec: 1.18x faster
zetazero(3570918901) @113 17.5 sec 17.2 sec: 1.02x faster
zetazero(3570918902) @113 13.0 sec 13.4 sec: 1.03x slower
Geometric mean (ref) 1.04x faster

Benchmark hidden because not significant (1): zetazero(1048449115) @113

So, we have some speedup here as well.

I do not recall why I preferred the method Illinois to Anderson.

I understand that.

Though, I see no performance regressions so far. @d0sboots, could you please test other posted cases?

Details
# bench.py
import pyperf
import mpmath
from mpmath import zetazero

runner = pyperf.Runner()
#for prec in [53, 100, 200, 400]:
#    with mpmath.mp.workprec(prec):
#        for x in [9, 99, 999, 9999]:
#            runner.bench_func(f"zetazero({x}) @{prec}", zetazero, x)
prec=113
with mpmath.mp.workprec(prec):
    for x in [1048449114, 1048449115, 3570918901,
              3570918902,
              # 8637740722917, 8637740722918,
              ]:
        runner.bench_func(f"zetazero({x}) @{prec}", zetazero, x)

@skirpichev
Copy link
Copy Markdown
Collaborator

Here all tests for your latest "uneasy cases":

$ python a.py  # on the branch
(0.5 + 388858886.002285j)  in  8.106628894805908
(0.5 + 388858886.002394j)  in  11.133415460586548
(0.5 + 1239587702.54745j)  in  15.664631605148315
(0.5 + 1239587702.54752j)  in  12.10592269897461
(0.5 + 2124447368584.39j)  in  3744.5918004512787
(0.5 + 2124447368584.39j)  in  3611.094599723816
$ python a.py  # on master
(0.5 + 388858886.002285j)  in  9.89375615119934
(0.5 + 388858886.002394j)  in  12.0871901512146
(0.5 + 1239587702.54745j)  in  15.6492760181427
(0.5 + 1239587702.54752j)  in  12.52377438545227
(0.5 + 2124447368584.39j)  in  3731.0318746566772
(0.5 + 2124447368584.39j)  in  3596.809224843979

Original benchmark on my system:

Benchmark ref patch
zetazero(9) @53 74.5 ms 65.2 ms: 1.14x faster
zetazero(999) @53 1.37 sec 1.14 sec: 1.20x faster
zetazero(9999) @53 4.70 sec 4.10 sec: 1.15x faster
zetazero(9) @100 103 ms 86.0 ms: 1.20x faster
zetazero(99) @100 758 ms 609 ms: 1.24x faster
zetazero(999) @100 2.51 sec 2.41 sec: 1.04x faster
zetazero(9999) @100 7.62 sec 7.34 sec: 1.04x faster
zetazero(9) @200 122 ms 113 ms: 1.08x faster
zetazero(99) @200 1.06 sec 914 ms: 1.16x faster
zetazero(999) @200 2.18 sec 2.10 sec: 1.04x faster
zetazero(9999) @200 7.63 sec 7.43 sec: 1.03x faster
zetazero(9) @400 174 ms 165 ms: 1.05x faster
zetazero(99) @400 1.20 sec 1.04 sec: 1.16x faster
zetazero(999) @400 3.43 sec 3.36 sec: 1.02x faster
zetazero(9999) @400 8.74 sec 8.08 sec: 1.08x faster
Geometric mean (ref) 1.10x faster

Benchmark hidden because not significant (1): zetazero(99) @53

I think we can revert this small patch later, but for now I'll merge this. It definitely improves things for small indexes.

Details
from time import time
from mpmath import zetazero
for n in [1048449114,
          1048449115,
          3570918901,
          3570918902,
          8637740722917,
          8637740722918,
          #1000000000000000
          ]:
    start = time()
    r = zetazero(n)
    end = time()
    print(r, " in ", end - start)

@skirpichev skirpichev merged commit 9b98a71 into mpmath:master Jun 20, 2025
12 checks passed
@skirpichev skirpichev added this to the 1.4 milestone Oct 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

zetazero is suboptimal?

3 participants