7  Ein Beispiel zur Stabilität von Gleitkommaarithmetik

7.1 Berechnung von \(\pi\) nach Archimedes

Eine untere Schranke für \(2\pi\), den Umfang des Einheitskreises, erhält man durch die Summe der Seitenlängen eines dem Einheitskreis eingeschriebenen regelmäßigen \(n\)-Ecks. Die Abbildung links zeigt, wie man beginnend mit einem Viereck der Seitenlänge \(s_4=\sqrt{2}\) die Eckenzahl iterativ verdoppelt.

Abb. 1 Abb.2

Die zweite Abbildung zeigt die Geometrie der Eckenverdoppelung.

Mit \(|\overline{AC}|= s_{n},\quad |\overline{AB}|= |\overline{BC}|= s_{2n},\quad |\overline{MN}| =a, \quad |\overline{NB}| =1-a,\) liefert Pythagoras für die Dreiecke \(MNA\) und \(NBA\) jeweils \[ a^2 + \left(\frac{s_{n}}{2}\right)^2 = 1\qquad\text{bzw.} \qquad (1-a)^2 + \left(\frac{s_{n}}{2}\right)^2 = s_{2n}^2 \] Elimination von \(a\) liefert die Rekursion \[ s_{2n} = \sqrt{2-\sqrt{4-s_n^2}} \qquad\text{mit Startwert}\qquad s_4=\sqrt{2} \] für die Länge \(s_n\) einer Seite des eingeschriebenen regelmäßigen \(n\)-Ecks.

Die Folge \((n\cdot s_n)\) konvergiert monoton von unten gegen den Grenzwert \(2\pi\): \[ n\, s_n \rightarrow 2\pi % \qquad\text{und} \qquad {n c_n}\downarrow 2\pi \] Der relative Fehler der Approximation von 2π durch ein \(n\)-Eck ist: \[ \epsilon_n = \left| \frac{n\, s_n-2 \pi}{2\pi} \right| \]

7.2 Zwei Iterationsvorschriften1

Die Gleichung \[ s_{2n} = \sqrt{2-\sqrt{4-s_n^2}}\qquad \qquad \textrm{(Iteration A)} \] ist mathematisch äquivalent zu \[ s_{2n} = \frac{s_n}{\sqrt{2+\sqrt{4-s_n^2}}} \qquad \qquad \textrm{(Iteration B)} \]

(Bitte nachrechnen!)

Allerdings ist Iteration A schlecht konditioniert und numerisch instabil, wie der folgende Code zeigt. Ausgegeben wird die jeweils berechnete Näherung für π.

using Printf

iterationA(s) =  sqrt(2 - sqrt(4 - s^2))
iterationB(s) = s / sqrt(2 + sqrt(4 - s^2))

s_B = s_A = sqrt(2)    # Startwert 

ϵ(x) = abs(x - 2π)/2π  # rel. Fehler   

ϵ_A = Float64[]   # Vektoren für den Plot
ϵ_B = Float64[]
is  = Float64[]

@printf """                        approx. Wert von π
         n-Eck       Iteration A            Iteration B
"""

for i in 3:35
    push!(is, i)
    s_A = iterationA(s_A)         
    s_B = iterationB(s_B)         
    doublePi_A = 2^i * s_A
    doublePi_B = 2^i * s_B
    push!(ϵ_A, ϵ(doublePi_A))
    push!(ϵ_B, ϵ(doublePi_B))
    @printf "%14i %20.15f  %20.15f\n" 2^i doublePi_A/2 doublePi_B/2 
end
                        approx. Wert von π
         n-Eck       Iteration A            Iteration B
             8    3.061467458920719     3.061467458920718
            16    3.121445152258053     3.121445152258052
            32    3.136548490545941     3.136548490545939
            64    3.140331156954739     3.140331156954753
           128    3.141277250932757     3.141277250932773
           256    3.141513801144145     3.141513801144301
           512    3.141572940367883     3.141572940367092
          1024    3.141587725279961     3.141587725277160
          2048    3.141591421504635     3.141591421511200
          4096    3.141592345611077     3.141592345570118
          8192    3.141592576545004     3.141592576584873
         16384    3.141592633463248     3.141592634338564
         32768    3.141592654807589     3.141592648776986
         65536    3.141592645321215     3.141592652386592
        131072    3.141592607375720     3.141592653288993
        262144    3.141592910939673     3.141592653514594
        524288    3.141594125195191     3.141592653570994
       1048576    3.141596553704820     3.141592653585094
       2097152    3.141596553704820     3.141592653588619
       4194304    3.141674265021758     3.141592653589501
       8388608    3.141829681889202     3.141592653589721
      16777216    3.142451272494134     3.141592653589776
      33554432    3.142451272494134     3.141592653589790
      67108864    3.162277660168380     3.141592653589794
     134217728    3.162277660168380     3.141592653589794
     268435456    3.464101615137754     3.141592653589795
     536870912    4.000000000000000     3.141592653589795
    1073741824    0.000000000000000     3.141592653589795
    2147483648    0.000000000000000     3.141592653589795
    4294967296    0.000000000000000     3.141592653589795
    8589934592    0.000000000000000     3.141592653589795
   17179869184    0.000000000000000     3.141592653589795
   34359738368    0.000000000000000     3.141592653589795

Während Iteration B sich stabilisiert bei einem innerhalb der Maschinengenauigkeit korrekten Wert für π, wird Iteration A schnell instabil. Ein Plot der relativen Fehler \(\epsilon_i\) bestätigt das:

using PlotlyJS

layout = Layout(xaxis_title="Iterationsschritte", yaxis_title="rel. Fehler",
            yaxis_type="log", yaxis_exponentformat="power", 
            xaxis_tick0=2, xaxis_dtick=2)

plot([scatter(x=is, y=ϵ_A, mode="markers+lines", name="Iteration A", yscale=:log10),   
      scatter(x=is, y=ϵ_B, mode="markers+lines", name="Iteration B", yscale=:log10)],
      layout)

7.3 Stabilität und Auslöschung

Bei \(i=26\) erreicht Iteration B einen relativen Fehler in der Größe des Maschinenepsilons:

ϵ_B[22:28]
7-element Vector{Float64}:
 5.3716034620272725e-15
 9.895059008997609e-16
 1.4135798584282297e-16
 4.240739575284689e-16
 5.654319433712919e-16
 5.654319433712919e-16
 5.654319433712919e-16

Weitere Iterationen verbessern das Ergebnis nicht mehr. Sie stabilisieren sich bei einem relativen Fehler von etwa 2.5 Maschinenepsilon:

ϵ_B[end]/eps(Float64)
2.5464790894703255

Die Form Iteration A ist instabil. Bereits bei \(i=16\) beginnt der relative Fehler wieder zu wachsen.

Ursache ist eine typische Auslöschung. Die Seitenlängen \(s_n\) werden sehr schnell klein. Damit ist \(a_n=\sqrt{4-s_n^2}\) nur noch wenig kleiner als 2 und bei der Berechnung von \(s_{2n}=\sqrt{2-a_n}\) tritt ein typischer Auslöschungseffekt auf.

setprecision(80) # precision für BigFloat

s = sqrt(BigFloat(2))

@printf "     a = √(4-s^2) als BigFloat       und als Float64\n\n"

for i = 3:44
    s = iterationA(s)
    x = sqrt(4-s^2)
    if i > 20
        @printf "%i %30.26f %20.16f \n" i  x  Float64(x)
    end 
end
     a = √(4-s^2) als BigFloat       und als Float64

21   1.99999999999775591177215422   1.9999999999977560 
22   1.99999999999943897794303856   1.9999999999994389 
23   1.99999999999985974448576005   1.9999999999998597 
24   1.99999999999996493612143919   1.9999999999999649 
25   1.99999999999999123403035980   1.9999999999999913 
26   1.99999999999999780850758995   1.9999999999999978 
27   1.99999999999999945212689707   1.9999999999999996 
28   1.99999999999999986303172344   1.9999999999999998 
29   1.99999999999999996575793045   2.0000000000000000 
30   1.99999999999999999143948303   2.0000000000000000 
31   1.99999999999999999785987034   2.0000000000000000 
32   1.99999999999999999946496800   2.0000000000000000 
33   1.99999999999999999986624159   2.0000000000000000 
34   1.99999999999999999996656040   2.0000000000000000 
35   1.99999999999999999999163886   2.0000000000000000 
36   1.99999999999999999999790889   2.0000000000000000 
37   1.99999999999999999999947722   2.0000000000000000 
38   1.99999999999999999999986931   2.0000000000000000 
39   1.99999999999999999999996691   2.0000000000000000 
40   1.99999999999999999999999173   2.0000000000000000 
41   1.99999999999999999999999835   2.0000000000000000 
42   1.99999999999999999999999835   2.0000000000000000 
43   1.99999999999999999999999835   2.0000000000000000 
44   1.99999999999999999999999835   2.0000000000000000 

Man sieht die Abnahme der Zahl der signifikanten Ziffern. Man sieht auch, dass eine Verwendung von BigFloat mit einer Mantissenlänge von hier 80 Bit das Einsetzen des Auslöschungseffekts nur etwas hinaussschieben kann.

Gegen instabile Algorithmen helfen in der Regel nur bessere, stabile Algorithmen und nicht genauere Maschinenzahlen!


  1. nach: Christoph Überhuber, „Computer-Numerik“ Bd. 1, Springer 1995, Kap. 2.3↩︎