web-dev-qa-db-ja.com

なぜstd :: reduceがC ++ 17に追加されたのですか?

私はstd::reduceの「戻り値」の意味の完全な説明を探していました。これはcppreference.comによると:

enter image description here

「戻り値」セクションの背後にある考え方を適切に認識できた後、std::reduceよりもstd::accumulateを選択する場所をより適切に決定できる可能性があります。

36
Aria Pahlavan

あなたは徹底的な説明を求め、前の答えは基本のみを扱っているので、私はより徹底的な説明を追加する自由を取っています。

std::reduceは、 MapReduceプログラミングモデル の2番目の主要なステップを実行することを目的としています。基本的な考え方は、プラットフォーム(この場合はC++実装)がこれら2つのプリミティブ操作mapとreduceを提供し、プログラマーが「実際の作業」を実行する2つのそれぞれにコールバック操作を提供するというものです。

基本的に、マップ操作のコールバックは、入力値を中間値にマップします。 reduceのコールバックは、2つの中間値を1つの中間値に結合します。残っている最後の中間値は、MapReduce全体の出力になります。それ自体は非常に制限的なモデルであるように見えますが、それでも、幅広い用途があります。

プラットフォームは、もちろん、「シャッフル」(通常はグループ単位で異なる処理ユニットに入力を分散する)やスケジューリングなど、より多くの作業を行う必要がありますが、これはアプリケーションプログラマーにとってはほとんど関係ありません。

ところで、C++標準ライブラリでは、「マップ」操作はtransformと呼ばれます。 C++ 17で並列処理のサポートも受けていますが、後で並列処理について説明します。

作成例は次のとおりです。整数を文字列表現に変換する関数があるとします。ここで、整数のリストが与えられると、子音とボーカルの最大の比率を含むテキスト表現が必要になります。例えば。

  • 入力:1、17、22、4、8
  • 出力:22

(この結果が信じられない場合は、自分で試してみてください。)

ここでは、マップへのコールバックとしてint-to-text関数(rsp。std::transform)を使用してMapReduceを使用できます。それに応じて引数を渡します。ここにはいくつかの非効率性があります。特に、「比率」をキャッシュする必要がありますが、これらは詳細です。

今、あなたの質問は次のようになります。結局のところ、これまでのところ、たとえばstd::transformstd::reduceはこの方法で使用し、後者の代わりにstd::accumulateを使用することもできます。もちろん、答えは十分な数の入力値が与えられた場合です。実行ポリシーです。前の2つの標準関数テンプレートには、暗黙的な並列処理を可能にするオーバーロードがあります。しかし、スレッドプールやstd::asyncではなくMapReduceを使用し、手書きのループを大量に使用するのはなぜかという疑問が残っています。まず、特に大きなベクターまたはI/Oのない他のコンテナでの「純粋な」計算では、入力データの詳細をすべて処理する必要がないため、2つのMapReduceコールバックを記述する方が便利な場合がよくあります。別のスレッドに広がり、結合します。

第二に、MapReduceは、非常に効率的に並列化できる方法で計算を構造化することを奨励しています。もちろん、C++などの命令型パラダイムをサポートするプログラミング言語では、多数のミューテックスをロックしたり、他のスレッドを妨害したりすることで、物事を台無しにすることができます。しかし、MapReduceパラダイムは、独立したマッピングコールバックの作成を推奨しています。簡単な経験則として、これらのタスクがデータをまったく共有する場合、そのコピーを複数のCPUキャッシュに同時に保存できるように、読み取り専用にする必要があります。基盤となるアルゴリズムの効率的なプラットフォーム実装と組み合わされた適切に作成されたタスクは、既に一般的に使用されているMapReduceプラットフォーム(Apache Hadoopのように、しかし必要な場合のみこれを使用する)例であり、無償の広告ではありません)。

しかし、質問は主にstd::reduceに関するものでした。この非常にスケーラブルなマッピングを実行してから、中間結果に対してstd::accumulateを実行できますか?そして、ここがフランソワ・アンドリューが以前に書いたことに到達する場所です。 accumulateは、数学者が左折りと呼ぶものを実行します。演算とそのオペランドをバイナリツリーとして表示する場合、これは左寄りのツリーになります。 1、2、3、4を追加するには:

   +
  / \
  + 4
 / \
 + 3
/ \
1 2

ご覧のとおり、各操作の結果は次の操作の引数の1つです。これは、データ依存関係の線形チェーンが存在することを意味し、それがすべての並列処理の悩みの種です。数百万の数値を追加するには、1つのコアをブロックする100万の連続した操作が必要であり、追加のコアを使用することはできません。ほとんどの場合、何もすることはなく、通信のオーバーヘッドは計算のコストを大きく上回ります。 (最近のCPUはクロックごとに複数の単純な計算を実行できるため、実際にはそれよりも悪いですが、データの依存関係が非常に多い場合は機能しないため、ほとんどの AL またはFPUは使用されません)

左折を使用する必要があるという制限を解除することにより、std::reduceにより、プラットフォームは計算リソースをより効率的に使用できます。 シングルスレッドのオーバーロードを使用する場合、たとえば、プラットフォームはSIMDを使用して100万回未満の操作で100万個の整数を追加でき、データ依存関係の数が大幅に削減されます。よく書かれた整数加算の削減が10倍高速化されても驚かないでしょう。確かに、この特別なケースはas-ifルールで最適化される可能性があります。これは、C++実装が整数加算が(ほぼ以下を参照)連想であることを「知っている」からです。

ただし、前述のように、reduceは実行ポリシー、つまりほとんどの場合マルチコア並列処理をサポートすることで、それよりもさらに進んでいます。理論的には、バランスの取れたバイナリツリーの操作を使用できます。 (深さが2より小さい場合、または左のサブツリーの深さが右のサブツリーの深さと最大1異なる場合、ツリーのバランスが保たれることに注意してください。)このようなツリーは対数の深さしかない。 100万個の整数がある場合、最小ツリー深度は20です。したがって、理論的には、十分なコアと通信オーバーヘッドがない場合、最適化されたシングルスレッド計算でも50,000倍の高速化を達成できます。もちろん、実際には、それは希望的観測の負荷ですが、大幅な高速化が期待できます。

とはいえ、パフォーマンスは効率と同じではないという簡単な免責事項/リマインダーを追加します。 64コアをそれぞれ100ミリ秒使用すると、1コアを1,000ミリ秒使用するよりもはるかに高いパフォーマンスが得られますが、CPU効率は大幅に低下します。別の言い方をすれば、パフォーマンスは経過時間を最小化するという意味での効率ですが、使用される合計CPU時間、RAM使用、エネルギー消費など)並列MapReduceの主な動機は、パフォーマンスを向上させることです。CPU時間を短縮するか、エネルギー消費を減らすかは不明であり、おそらくincrease peak RAM usage。

締めくくりとして、ここにいくつかの警告があります。前述のように、操作が連想的でも可換でもない場合、reduceは非決定的です。幸いなことに、加算や乗算などの最も重要な算術演算は、連想と可換ですよね?たとえば、整数と浮動小数点の加算には、これらの両方のプロパティがあることがわかっています。そしてもちろん、私はファセットになっています。 C++では、符号付き整数の加算も浮動小数点の加算も、結合的ではありません。浮動小数点数の場合、これは中間結果の丸めの違いが考えられるためです。これは、例として、有効数字2桁の単純な10進浮動小数点形式を選択し、合計10 + 0.4 + 0.4を考慮すると簡単に視覚化できます。これが通常のC++構文規則によって左折りとして行われる場合、結果が10に切り捨てられるたびに(10 + 0.4)+ 0.4 = 10 + 0.4 = 10になります。しかし、10として行う場合+(0.4 + 0.4)、最初の中間結果は0.8であり、10 + 0.8は11に切り上げられます。また、操作ツリーの深さが深くなると、丸め誤差が大幅に拡大する可能性があるため、実際には左折りを行うのは1回です。精度に関して言えば、最悪の事態が発生する可能性があります。この動作に対処するには、入力の並べ替えやグループ化から中間精度の向上に至るまで、いくつかの方法がありますが、reduceになると、100%実行する方法はないかもしれません一貫性。

もう一つの、おそらくより驚くべき観察結果は、C++では符号付き整数の加算は結合的ではないということです。これの理由は、オーバーフローの可能性、つまり率直に言うと、(-1) + 1 + INT_MAXです。通常の構文規則、またはaccumulateに従って、結果はINT_MAXです。ただし、reduceを使用すると、整数オーバーフローと未定義の動作を含む(-1) + (1 + INT_MAX)として書き換えられる可能性があります。実際、reduceはオペランドの順序を任意に変更する可能性があるため、入力が{ INT_MAX, -1, 1 }の場合でもこれは当てはまります。

ここでの推奨事項は、reduceへのコールバックでオーバーフローが発生しないようにすることです。これは、入力の許容範囲を制限することで実行できます(例:1000 intsを追加する場合、それらがどれもINT_MAX / 1000より大きくないか、INT_MIN / 1000より小さく、切り上げられることを確認してください) 、たとえば、または同等に、より大きな整数型を使用するか、絶対的な最後の手段として(これは高価であり、正しく処理するのが難しいため)、reduceコールバックに追加のチェックを入れます。ほとんどの場合、reduceaccumulateよりも整数オーバーフローに関してわずかに安全性が低いだけです。

39
Arne Vogel

std::accumulatestd:reduce とは異なる順序でコンテナを繰り返し処理します。順序が保証されていないため、std::reduceは追加の要件を導入します。

Binary_opが結合的または可換的でない場合、動作は非決定的です。 binary_opが[first;の要素を変更したり、反復子を無効にした場合の動作は未定義です。最後]、終了イテレータを含む。

ただし、std::reduceは、std::accumulateでは使用できない並列化をサポートするオーバーロードを提供します。 std::reduceを使用すると、上記の要件を満たしていれば、操作を自動的に並列化できます。

30
  • 並列性を許可することがstd :: reduceを追加する主な理由です

  • また、std :: reduceで使用する操作が連想的かつ可換であることを確認する必要があります。

    • たとえば、加算は結合的であり、std :: reduceを使用して累積が並行して行われたときに同じ結果が得られます。
      100 + 50 + 40 + 10 = 200 (100 + 40) + (50 + 10) = 200

    • しかし、引き算は連想的ではないため、std :: reduceは間違った結果を与える可能性があります。
      100 - 50 - 40 - 10 = 0 NOT SAME AS (100 - 40) - (50 - 10) = 20

  • 効率
    std::vector<double> v(100000, 0.1); double result = std::accumulate(v.begin(), v.end(), 0.0); double result = std::reduce(std::execution::par,v.begin(),v.end()) //Faster

1
kbagal