Ada 202x (11日目) - parallel

ランデブーはAdaの代名詞ではあるのですが、いつの間にかスレッドプールと自動並列化の時代になりまして。

経緯

gccにもlibgompだの付いてきまして、C言語やgfortranでもちょっとpragmaを書き足すだけで並列化できたりするわけですよ。 Adaの自動並列化やOpenMPの実装は研究レベルでは存在していますが、少なくともGNATにはないです。 gcc 9以降ではGNATでもOpenACC用のpragmaが使えます。 (OpenMP、GNU的には「Offloading and Multi Processing」らしいですがどういう経緯でしょう?libgompはOpenACC等も含むから?)

標準化された機能に限定しましても、C11以降は <threads.h> 、Fortran 2008以降はCoArrayも入りましたから明示的な並列化も標準の範囲でできるようになりました。

陽に task が書けるだけでは立場がなくなりそうなのです。

Ada 202xでの改善

予約語 parallel が追加されました。

以下 Total := Process (1) + Process (2) + Process (3) + Process (4) を求める例です。

まずは一番単純な使い方の parallel do 文。

declare
   P : array (1 .. 4) of Float;
   Total : Float;
begin
   parallel do
      P (1) := Process (1);
   and
      P (2) := Process (2);
   and
      P (3) := Process (3);
   and
      P (4) := Process (4);
   end do;
   Total := P'Reduce ("+", 0.0);

続いて parallel for 文。

declare
   P : array (1 .. 4) of Float;
   Total : Float;
begin
   parallel for I in 1 .. 4 loop
      P (I) := Process (I);
   end loop;
   Total := P'Reduce ("+", 0.0);

parallel for 文では、並列化するスレッド(チャンク)の数をループ数とは別に指定することもできます。

declare
   P : array (1 .. 2) of Float := (others => 0.0);
   Total : Float;
begin
   parallel (Chunk in P'Range) for I in 1 .. 4 loop
      P (Chunk) := @ + Process (I);
   end loop;
   Total := P'Reduce ("+", 0.0);

ループの何回目の試行がどう割り振られるかはわかりませんが、同じスレッドならチャンクの番号も同じになりますので P (Chunk) の同期は不要です。

ユーザー定義コンテナを parallel for 文に対応させるため Parallel_Iterator が追加されています。 (またコンテナの実装が大変になりました……。)

ユーザー定義コンテナの Cursor は整数型に限りませんので計算で割り振りができませんし、いっそコンテナの構造に沿って分割してもらったほうが効率的だからでしょうか。

そして昨日も少し触れました 'Parallel_Reduce です。

begin
   -- Pに値を入れるところは省略
   Total := P'Parallel_Reduce ("+", 0.0);

配列の要素と結果の型が異なる場合は並列化のため第3引数が必要です。 この例の場合は両方 Float ですのでいらないです。

さてreduction式の対象は一度変数に入れる必要がないことを思い出しますと P 自体が不要な気がしてきます。 次のように書けそうですが、匿名の数列と 'Parallel_Reduce の組み合わせは許されていません。

declare
   Total : Float;
begin
   Total := [for I in 1 .. 4 => Process (I)]'Parallel_Reduce ("+", 0.0);
     -- error

この組み合わせが許されていないのは逐次処理できず一旦コンテナを作らないといけないからではないでしょうか、と想像します。 aggregate式を Float_Array'( ) で囲って型を付ければ恐らく文法上は通る形になると思いますが、それでも Process (I) の呼び出しを含むaggregate式自体は並列化されませんので意味はないです。

ですのでreduction式に限定してaggregate式も並列化できます。

declare
   Total : Float;
begin
   Total := [parallel for I in 1 .. 4 => Process (I)]'Reduce ("+", 0.0);

勿論スレッドの数も指定できます。

begin
   Total := [parallel (Chunk in 1 .. 2)
             for I in 1 .. 4 => Process (I)]'Reduce ("+", 0.0);

Ada.Iterator_Interfaces.Parallel_Iterator

Ada.Iterator_Interfaces.Parallel_Reversible_Iterator

関連AI

所感

task は宣言するだけでも行数必要ですから、すごくお手軽になりました。

reduction式に妙に力入ってるのほんと何なのでしょうね。