Effiziente stabile Summe der bestellten Nummern

12

Ich habe eine ziemlich lange Liste von Gleitkomma-Positivzahlen ( std::vector<float>, Größe ~ 1000). Die Nummern sind in absteigender Reihenfolge sortiert. Wenn ich sie in der Reihenfolge summiere:

for (auto v : vec) { sum += v; }

Ich denke, ich kann ein numerisches Stabilitätsproblem haben, da nahe am Ende des Vektors sumviel größer sein wird als v. Die einfachste Lösung wäre, den Vektor in umgekehrter Reihenfolge zu durchlaufen. Meine Frage ist: Ist das genauso effizient wie der Vorwärtsfall? Wird mir mehr Cache fehlen?

Gibt es eine andere intelligente Lösung?

Ruggero Turra
quelle
1
Geschwindigkeitsfrage ist leicht zu beantworten. Benchmarking.
Davide Spataro
Ist Geschwindigkeit wichtiger als Genauigkeit?
stark
Nicht ganz ein Duplikat, aber sehr ähnliche Frage: Summe der Serien mit float
acraig5075
4
Möglicherweise müssen Sie auf negative Zahlen achten.
AProgrammer
3
Wenn Ihnen die Präzision in hohem Maße am Herzen liegt, lesen Sie die Kahan-Summierung .
Max Langhof

Antworten:

3

Ich denke, ich kann ein numerisches Stabilitätsproblem haben

Also teste es. Derzeit haben Sie ein hypothetisches Problem, das heißt, überhaupt kein Problem.

Wenn Sie testen und die Hypothese zu einem tatsächlichen Problem wird, sollten Sie sich darum kümmern, es tatsächlich zu beheben.

Das heißt, Gleitkommapräzision kann Probleme verursachen, aber Sie können überprüfen, ob dies wirklich für Ihre Daten gilt, bevor Sie dies vor allem anderen priorisieren.

... wird mir mehr Cache fehlen?

Eintausend Floats sind 4 KB groß - es passt in den Cache eines modernen Massenmarktsystems (wenn Sie eine andere Plattform im Sinn haben, sagen Sie uns, was es ist).

Das einzige Risiko besteht darin, dass der Prefetcher Ihnen beim Rückwärtslaufen nicht hilft, aber Ihr Vektor befindet sich möglicherweise bereits im Cache. Sie können dies erst dann wirklich feststellen, wenn Sie sich im Kontext Ihres vollständigen Programms profilieren. Es macht also keinen Sinn, sich darüber Gedanken zu machen, bis Sie ein vollständiges Programm haben.

Gibt es eine andere intelligente Lösung?

Machen Sie sich keine Sorgen über Dinge, die zu Problemen werden könnten, bis sie tatsächlich zu Problemen werden. Es lohnt sich höchstens, mögliche Probleme zu erwähnen und Ihren Code so zu strukturieren, dass Sie später die einfachste Lösung durch eine sorgfältig optimierte ersetzen können, ohne alles andere neu zu schreiben.

Nutzlos
quelle
5

Ich habe ein Benchmarking durchgeführt Ihren Anwendungsfall mit einem und die Ergebnisse (siehe beigefügtes Bild) weisen auf die Richtung hin, in der es keinen Leistungsunterschied macht, vorwärts oder rückwärts zu schleifen.

Möglicherweise möchten Sie auch auf Ihrem Hardware + Compiler messen.


Die Verwendung von STL zur Ausführung der Summe ist so schnell wie das manuelle Durchlaufen von Daten, aber viel aussagekräftiger.

Verwenden Sie Folgendes für die umgekehrte Akkumulation:

std::accumulate(rbegin(data), rend(data), 0.0f);

während für die Vorwärtsakkumulation:

std::accumulate(begin(data), end(data), 0.0f);

Geben Sie hier die Bildbeschreibung ein

Davide Spataro
quelle
Diese Website ist super cool. Nur um sicher zu gehen: Sie planen die zufällige Generierung nicht, oder?
Ruggero Turra
Nein, nur der Teil in der stateSchleife ist zeitgesteuert.
Davide Spataro
2

Die einfachste Lösung wäre, den Vektor in umgekehrter Reihenfolge zu durchlaufen. Meine Frage ist: Ist das genauso effizient wie der Vorwärtsfall? Fehlt mir mehr Cache?

Ja, es ist effizient. Die Verzweigungsvorhersage und die Smart-Cache-Strategie Ihrer Hardware sind auf sequentiellen Zugriff abgestimmt. Sie können Ihren Vektor sicher akkumulieren:

#include <numeric>

auto const sum = std::accumulate(crbegin(v), crend(v), 0.f);
YSC
quelle
2
Können Sie klarstellen: In diesem Zusammenhang bedeutet "sequentieller Zugriff" vorwärts, rückwärts oder beides?
Ruggero Turra
1
@RuggeroTurra Ich kann nicht, wenn ich keine Quelle finde, und ich bin gerade nicht in der Stimmung, CPU-Datenblätter zu lesen.
YSC
@RuggeroTurra Normalerweise würde sequentieller Zugriff vorwärts bedeuten. Alle semi-anständigen Speicher-Prefetchers erhalten sequentiellen Vorwärtszugriff.
Zahnbürste
@Toothbrush, danke. Wenn ich also im Prinzip rückwärts schleife, kann dies ein Leistungsproblem sein
Ruggero Turra,
Im Prinzip zumindest auf etwas Hardware, wenn sich der gesamte Vektor nicht bereits im L1-Cache befindet.
Nutzlos
2

Zu diesem Zweck können Sie den Reverse-Iterator ohne Transpositionen in Ihrem std::vector<float> vec:

float sum{0.f};
for (auto rIt = vec.rbegin(); rIt!= vec.rend(); ++rIt)
{
    sum += *rit;
}

Oder machen Sie den gleichen Job mit dem Standardalgortithmus:

float sum = std::accumulate(vec.crbegin(), vec.crend(), 0.f);

Die Leistung muss gleich sein und nur die Bypass-Richtung Ihres Vektors ändern

Malov Vladimir
quelle
Korrigieren Sie mich, wenn ich falsch liege, aber ich denke, dies ist noch effizienter als die foreach-Anweisung, die OP verwendet, da sie einen Overhead verursacht. YSC hat Recht mit dem numerischen Stabilitätsteil.
Sephiroth
4
@sephiroth Nein, jedem halbwegs anständigen Compiler ist es egal, ob Sie einen Range-For oder einen Iterator für geschrieben haben.
Max Langhof
1
Die tatsächliche Leistung kann aufgrund von Caches / Prefetching definitiv nicht garantiert werden. Es ist vernünftig, dass das OP diesbezüglich vorsichtig ist.
Max Langhof
1

Wenn Sie unter numerischer Stabilität Genauigkeit verstehen, kann dies zu Genauigkeitsproblemen führen. Abhängig vom Verhältnis der größten zu den kleinsten Werten und Ihren Anforderungen an die Genauigkeit des Ergebnisses kann dies ein Problem sein oder auch nicht.

Wenn Sie eine hohe Genauigkeit wünschen, ziehen Sie die Kahan-Summierung in Betracht - dies verwendet einen zusätzlichen Float zur Fehlerkompensation. Es gibt auch eine paarweise Summierung .

Eine detaillierte Analyse des Kompromisses zwischen Genauigkeit und Zeit finden Sie in diesem Artikel .

UPDATE für C ++ 17:

Einige der anderen Antworten erwähnen std::accumulate. Seit C ++ 17 gibt es Ausführungsrichtlinien, mit denen Algorithmen parallelisiert werden können.

Zum Beispiel

#include <vector>
#include <execution>
#include <iostream>
#include <numeric>

int main()
{  
   std::vector<double> input{0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6, 0.5};

   double reduceResult = std::reduce(std::execution::par, std::begin(input), std::end(input));

   std:: cout << "reduceResult " << reduceResult << '\n';
}

Dies sollte das Summieren großer Datenmengen auf Kosten nicht deterministischer Rundungsfehler beschleunigen (ich gehe davon aus, dass der Benutzer die Thread-Partitionierung nicht bestimmen kann).

Paul Floyd
quelle