順序付けられた数値の効率的な安定合計


12

浮動小数点正数のかなり長いリスト(std::vector<float>、サイズ〜1000)があります。番号は降順でソートされます。順序に従ってそれらを合計すると:

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

ベクトルの終わり近くはsumより大きいので、数値の安定性の問題が発生する可能性がありますv。最も簡単な解決策は、ベクトルを逆の順序でトラバースすることです。私の質問は、前向きの場合と同様に効率的ですか?キャッシュが不足しますか?

他のスマートなソリューションはありますか?


1
スピードの質問は簡単に答えられます。それをベンチマークします。
Davide Spataro

精度よりも速度の方が重要ですか?
スターク

まったく同じではありませんが、非常によく似た質問:フロートを使用したシリーズの合計
acraig5075

4
負の数に注意を払う必要があるかもしれません。
AProgrammer

3
実際に精度を重視する場合は、Kahanの総和を確認してください。
Max

回答:


3

数値の安定性に問題があると思います

それをテストしてください。現在、架空の問題があります。つまり、まったく問題ありません。

テストし、仮説が実際の問題に具体化する場合、実際にそれを修正することについて心配する必要があります。

つまり、浮動小数点の精度問題引き起こす可能ありますが、それを他のすべてのものよりも優先する前に、それが本当にデータに対して有効かどうかを確認できます。

...キャッシュが不足しますか?

1000フロートは4Kbです-それは現代の大衆市場システムのキャッシュに収まります(別のプラットフォームを考えている場合は、それを教えてください)。

唯一のリスクは、逆方向に反復するときにプリフェッチャーが役に立たないことですが、もちろんベクトルはすでにキャッシュにある可能性があります。完全なプログラムのコンテキストでプロファイルを作成するまで、これを実際に決定することはできないため、完全なプログラムを作成するまで、このことを心配する必要はありません。

他のスマートなソリューションはありますか?

実際に問題になるまでは、問題になる可能性があることについて心配しないでください。ほとんどの場合、考えられる問題に注意し、コードを構造化することで、他のすべてを書き直さなくても、可能な限り簡単なソリューションを注意深く最適化したソリューションに置き換えることができます。


5

あなたのユースケースをベンチマークし、結果(添付の画像を参照)は、順方向または逆方向にループしてもパフォーマンスに違いがない方向を示しています。

ハードウェア+コンパイラーで測定することもできます。


STLを使用して合計を実行すると、データを手動でループするのと同じくらい高速ですが、はるかに表現力に富んでいます。

逆累積には次を使用します。

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

フォワードアキュムレーションの場合:

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

ここに画像の説明を入力してください


そのウェブサイトはとてもクールです。念のために言うと、ランダム生成のタイミングをとっていませんよね?
Ruggero

いいえ、stateループの一部のみがタイミングをとります。
Davide Spataro

2

最も簡単な解決策は、ベクトルを逆の順序でトラバースすることです。私の質問は、前向きの場合と同様に効率的ですか?キャッシュが不足しますか?

はい、それは効率的です。ハードウェアからの分岐予測とスマートキャッシュ戦略は、シーケンシャルアクセス用に調整されています。ベクトルを安全に蓄積できます。

#include <numeric>

auto const sum = std::accumulate(crbegin(v), crend(v), 0.f);

2
明確にできますか?このコンテキストでの「順次アクセス」とは、前方、後方、または両方を意味しますか?
Ruggero

1
@RuggeroTurraソースを見つけられない限りできません。今、CPUデータシートを読む気にはなれていません。
YSC 2019年

@RuggeroTurra通常、順次アクセスは転送を意味します。すべての準まともなメモリプリフェッチャーは、順方向順次アクセスをキャッチします。
歯ブラシ

@歯ブラシ、ありがとう。そのため、逆方向にループすると、原則としてパフォーマンスの問題になる可能性があります
Ruggero

原則的には、少なくともいくつかのハードウェア上で、全体のベクトルがない場合には、既に L1キャッシュに。
役に立たない

2

この目的のために、あなたの中で転置なしで逆反復子を使うことができますstd::vector<float> vec

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

または、標準のアルゴリズムを使用して同じ作業を行います。

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

パフォーマンスは同じで、ベクターのバイパス方向のみを変更する必要があります


私が間違っている場合は修正してください。ただし、オーバーヘッドが発生するため、これはOPが使用するforeachステートメントよりも効率的だと思います。YSCは数値安定性の部分については正しいです。
セフィロス

4
@sephirothいいえ、中途半端なコンパイラーは、range-forとiterator forのどちらを作成したかを気にしません。
Max Langhof

1
キャッシュ/プリフェッチにより、実際のパフォーマンスは同じであるとは限りません。OPがそれを警戒するのは理にかなっています。
Max Langhof

1

数値的安定性が正確さを意味する場合、はい、正確さの問題が発生する可能性があります。最大値と最小値の比率、および結果を正確にするための要件によっては、これが問題になる場合と問題にならない場合があります。

高精度が必要な場合は、Kahan加算を検討してください。これは、誤差補正に追加の浮動小数点を使用します。ペアワイズ総和もあります。

精度と時間のトレードオフの詳細な分析については、この記事を参照してください

C ++ 17の更新:

他の回答のいくつかは言及していstd::accumulateます。C ++ 17以降、アルゴリズムの並列を可能にする実行ポリシーがあります。

例えば

#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';
}

これにより、非決定論的な丸めエラーを犠牲にして、大規模なデータセットの集計が速くなります(ユーザーがスレッドのパーティション分割を判断できないと想定しています)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.