====== std.sum — kompensiertes Summieren ====== Summen, die den Rundungsfehler mitführen statt ihn zu verlieren — und laufende Statistik nach Welford, die ohne zweiten Durchgang auskommt. → [[lyx_-_programmiersprache:units:mathematik|Mathematik-Units]] · [[lyx_-_programmiersprache:units:feq|std.feq]] · [[lyx_-_programmiersprache:units:prob|std.prob]] · [[lyx_-_programmiersprache:units:numeric|std.numeric]] import std.sum; Schicht 0, hängt an ''std.math'' und ''std.alloc''. 25 Funktionen, zwei Größenkonstanten (''SUM_ACC_BYTES'' = 16, ''SUM_STAT_BYTES'' = 24). Alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt. ---- ===== Das Problem in einer Zeile ===== Wer ''1e16'', ''1'' und ''−1e16'' in dieser Reihenfolge addiert, bekommt **null**. Die ''1'' geht beim ersten Schritt verloren, weil ''1e16 + 1'' in f64 wieder ''1e16'' ist — und nach dem Abziehen ist sie unwiederbringlich weg. unit main; import std.io; import std.string; import std.alloc; import std.sum; fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); } fn main(): int64 { var n: int64 := 3; var a: int64 := alloc(n * 8); pokef64(a, 10000000000000000.0); pokef64(a + 8, 1.0); pokef64(a + 16, -10000000000000000.0); ZF("Naive : ", SumNaive(a, n)); ZF("Kahan : ", SumKahan(a, n)); ZF("Neumaier : ", SumNeumaier(a, n)); ZF("Pairwise : ", SumPairwise(a, n)); var work: int64 := alloc(64 * 8); ZF("Exact : ", SumExact(a, n, work, 64)); return 0; } Naive : 0.000000 Kahan : 0.000000 Neumaier : 1.000000 Pairwise : 0.000000 Exact : 1.000000 Das richtige Ergebnis ist **1**. > **Bemerkenswert: Kahan versagt hier ebenfalls.** Das ist kein Fehler der Umsetzung, sondern die bekannte Grenze des Verfahrens. Kahan führt einen Korrekturterm mit, verrechnet ihn aber gegen den **neuen Summanden** — ist dieser selbst riesig (''−1e16''), geht die gesammelte Korrektur beim Verrechnen unter. **Neumaier** unterscheidet genau diesen Fall: er prüft, welcher der beiden Werte betragsmäßig größer ist, und legt den Fehler entsprechend an. Deshalb ist Neumaier und nicht Kahan die richtige Vorgabe. ^ Verfahren ^ Kosten ^ Wann ^ | ''SumNaive'' | 1 Addition | nur wenn alle Werte dieselbe Größenordnung haben | | ''SumPairwise'' | 1 Addition, rekursiv | gute Genauigkeit fast umsonst, hilft aber nicht gegen Auslöschung | | ''SumKahan'' | ~4 Operationen | klassisch, aber blind gegen große Summanden (siehe oben) | | ''SumNeumaier'' | ~5 Operationen | **die Vorgabe** — fängt auch den Fall oben | | ''SumExact'' | Arbeitspuffer | fehlerfrei, Ergebnis ist die korrekt gerundete Summe | ''SumExact'' braucht einen Arbeitsbereich: ''SumExact(arr, n, work, workCap)''. Er sammelt die nicht-überlappenden Teilsummen nach Shewchuk; ''workCap'' begrenzt deren Anzahl. ---- ===== Zwei Zahlen exakt addieren ===== Die Grundlage aller Verfahren: die Summe zweier f64-Zahlen lässt sich **exakt** als Paar aus Ergebnis und Fehler darstellen. ^ Funktion ^ Bedingung ^ | ''SumTwoSum(a, b, errAddr)'' | keine — immer gültig, 6 Operationen | | ''SumFastTwoSum(a, b, errAddr)'' | setzt voraus, dass der Betrag von ''a'' mindestens so groß ist wie der von ''b'' — dafür nur 3 Operationen | Beide liefern die Summe zurück und legen den Rest unter ''errAddr'' ab. **''SumFastTwoSum'' ohne die Vorbedingung liefert stillschweigend Unsinn** — wer die Reihenfolge nicht garantieren kann, nimmt ''SumTwoSum''. ---- ===== Laufend summieren ===== Wenn die Werte nicht als Feld vorliegen, sondern nach und nach eintreffen, gibt es die Akkumulator-Form. Der Aufrufer stellt ''SUM_ACC_BYTES'' Bytes: var acc: int64 := alloc(SUM_ACC_BYTES); SumNeumaierInit(acc); SumNeumaierAdd(acc, wert); // beliebig oft var s: f64 := SumNeumaierValue(acc); var e: f64 := SumNeumaierError(acc); // die mitgeführte Korrektur Dieselbe Form gibt es für Kahan (''SumKahanInit'' / ''Add'' / ''Value''). ''SumNeumaierError'' gibt Auskunft darüber, **wie viel** kompensiert wurde — ein brauchbares Maß dafür, ob das naive Summieren an dieser Stelle getragen hätte. Über Felder rechnen außerdem ''SumDot(a, b, n)'' (kompensiertes Skalarprodukt) und ''SumMean(arr, n)''. ---- ===== Statistik nach Welford ===== Mittelwert und Streuung in **einem** Durchgang, ohne die Werte zu behalten: unit main; import std.io; import std.string; import std.alloc; import std.sum; fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); } fn main(): int64 { var m: int64 := 5; var b: int64 := alloc(m * 8); var i: int64 := 0; while (i < m) { pokef64(b + i * 8, 2.0 + (i as f64)); i := i + 1; } var st: int64 := alloc(SUM_STAT_BYTES); SumStatInit(st); i := 0; while (i < m) { var v: f64 := peekf64(b + i * 8); SumStatAdd(st, v); i := i + 1; } Print("Count : "); PrintLn(IntToStr(SumStatCount(st))); ZF("Mean : ", SumStatMean(st)); ZF("VarP : ", SumStatVarianceP(st)); ZF("VarS : ", SumStatVarianceS(st)); return 0; } Count : 5 Mean : 4.000000 VarP : 2.000000 VarS : 2.500000 Für die Werte 2…6: Mittelwert 4, Varianz der **Grundgesamtheit** 2, Varianz der **Stichprobe** 2,5. Der Unterschied ist der Nenner — ''n'' gegen ''n−1''. Wer eine Stichprobe hat und ''VarianceP'' nimmt, unterschätzt die Streuung systematisch. > **Warum nicht die Schulformel?** ''E[x²] − E[x]²'' verlangt zwei große, fast gleiche Zahlen voneinander abzuziehen — bei Werten weit von null verliert das Ergebnis alle Stellen und kann sogar **negativ** werden. Welford führt Mittelwert und Abweichungsquadrat schrittweise mit und hat das Problem nicht. ''SumStatMerge(a, b, out)'' führt zwei getrennt gerechnete Statistiken zusammen — für parallel ausgewertete Teilmengen, ohne die Werte erneut zu lesen. > **Der Akkumulator ist roher Speicher.** ''SumStatInit'' muss vor dem ersten ''Add'' laufen, sonst wird auf uninitialisiertem Speicher gerechnet. Die Größen ''SUM_ACC_BYTES'' und ''SUM_STAT_BYTES'' sind Konstanten der Unit — nicht selbst ausrechnen, sondern verwenden. ---- ===== Wo es noch naiv summiert wird ===== Innerhalb der Mathematik-Units sind ''std.dist'', ''std.sparse'', ''std.linalg'' und ''std.prob'' auf kompensiertes Summieren umgestellt. **Noch nicht umgestellt** sind ''std.tensor'' (3 Stellen), ''std.convolve'' (8), ''std.wavelet'' (2) und ''std.fft'' (1) — bei ''convolve'' und ''fft'' betrifft es die inneren Faltungsschleifen. Wer dort über sehr viele Werte mit stark verschiedenen Größenordnungen rechnet, sollte das Ergebnis gegen eine ''SumExact''-Rechnung prüfen. ---- Letzte Aktualisierung: 2026-08-16 · alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt; Summen- und Statistikwerte gegen eine unabhängige Referenzrechnung geprüft.