std.sum — kompensiertes Summieren
Summen, die den Rundungsfehler mitführen statt ihn zu verlieren — und laufende Statistik nach Welford, die ohne zweiten Durchgang auskommt.
→ Mathematik-Units · std.feq · std.prob · 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.SumStatInitmuss vor dem erstenAddlaufen, sonst wird auf uninitialisiertem Speicher gerechnet. Die GrößenSUM_ACC_BYTESundSUM_STAT_BYTESsind 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.
