スポンサーリンク

C++CLIでParallel::For (改)

過去のParallel::Forの記事がかなりわかりにくかったので再度書く。過去記事:

C++CLIでParallel::For

以下サンプル

C++(普通コード)

int i;
int c1[100];

for (i = 0; i < 100; i ++) {
  c1[i] = i;
}

C++CLI (Parallel::For)

#include "pch.h"

using namespace System;
 
// Parallel::Forでデータを操作する関数を含んだクラス
ref struct LoopObject {
    int *_array;
public:
    LoopObject(int* data) {
        _array = data;
    }

    // データを操作する関数
    // Parallel::Forの場合は、intの引数一つである必要がある
    // 引数 i ループカウンタ for(int i = 0;i<SIZE;i++){  function(i); } のような感じ
void function(int i) { // データを操作 _array[i] = i; } };
int main(array<System::String^>^ args)
{
    // データ
    const int SIZE = 100;
    int c1[SIZE];

    // Parallel::Forでデータを操作する関数を含んだクラスのインスタンスを生成
    LoopObject^ forInstance = gcnew LoopObject(c1);// コンストラクタにデータを渡す

    // forの範囲を指定
    int BEGIN = 0;
    int END = SIZE;

    // 並列処理実行 For( 開始Index , 終了Index , 一回分の処理を行う関数オブジェクト )
    System::Threading::Tasks::Parallel::For(
        BEGIN,
        END,
        gcnew Action<int>(forInstance, &LoopObject::function)
    );

    // 結果を表示
    for (int i = 0; i < SIZE; i++) {
        System::Console::WriteLine(
            System::String::Format("mydata[{0}] = {1}", i, c1[i]));
    }

    Console::ReadLine();

    return 0;
}

Action<int>のintはLoopObject::functionの引数がintなので、ループカウンタを与える関数に合わせているのだが、問題はParallel::Forがintしか受け付けていないので、実質Action<int>以外に選択肢がない(Action<size_t>などやってもParallel::Forでビルドエラーになる)。

C++で構造だけまねた例

要は「関数オブジェクトを並列で呼び出す関数」がParallel::Forで、Actionのインスタンスが関数オブジェクトになる。C++でそれっぽいコードを書くと以下になる。

std::sort(begin,end,[](...){...});とやるときと考え方は同じ。

#include <functional>

struct LoopObject {
    int* _array;
public:
    LoopObject(int* data) {
        _array = data;
    }

    // データを操作する関数
    // Parallel::Forの場合は、intの引数一つである必要がある
    // 引数 i ループカウンタ for(int i = 0;i<SIZE;i++){  function(i); } のような感じ
    void function(int i) {

        // データを操作
        _array[i] = i;

    }
};

namespace Parallel
{
    void For(int start, int end, LoopObject* action)
    {
        // 本来はここは並列処理で呼び出すが
        // 今回はシングルスレッドで実行する
        for (int i = start; i < end; i++)
        {
            action->function(i);
        }
    }
}

int main()
{
    int c1[100];

    auto action = new LoopObject(c1);

    Parallel::For(0, 100, action);


    // 結果の表示
    for (int i = 0; i < 100; i++)
    {
        printf("%d\n", c1[i]);
    }


}

3 件のコメント

  • * VB.net

    Class MainWindow

    Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
    Dim i As Integer
    Dim n1 As New MyClass1(100) ‘c1,c2 のバッファーを作成
    For i = 0 To n1.c2.Count – 1 ‘c2() は内容を10000で初期化
    n1.c2(i) = 10000
    Next

    n1.sub1() ‘並列処理実行

    For i = 0 To n1.c2.Count – 1
    Debug.Print($”{i}: {n1.c1(i):F3} : {n1.c2(i)}”)
    Next
    End Sub

    End Class

    *C++/CLI

    #pragma once

    using namespace System;
    using namespace System::Diagnostics;//debug::を使うため

    #pragma managed

    // Parallel::Forでデーターを操作する関数を含んだ構造体
    ref struct LoopObject {
    private:
    int count1 = 0;//atomic操作でインクリメントする
    array^ _array1; //MyClass1::c1へのポインター
    array^ _array2; //MyClass1::c2へのポインター

    public:
    //構造体のコンストラクター
    LoopObject(array^ data1, array^ data2) {
    _array1 = data1; _array2 = data2;
    }

    // この関数が並列で動作すると考える
    //並列処理数のインデックス(int i:Global ID)一つが渡されることがOpenCLのカーネルに似ている
    void ParallelFunction(int i) {
    _array1[i] = count1+0.01;// データーを操作
    _array2[i] += i;//VB.net側で内容が10000に初期化されている これに加算する

    System::Threading::Interlocked::Increment(count1);//atomic inc.
    }
    };

    //テスト用クラス
    public ref class MyClass1 {

    public:
    array^c1;//ここにデーターを格納する VB.net側から初期化することも可能
    array^ c2;

    int size1;//size of Data

    MyClass1(int _size) {//コンストラクター c1,c2配列の要素数を確保
    size1 = _size;
    c1 = gcnew array(_size);
    c2 = gcnew array(_size);
    }

    //VB.netから呼ばれるSub
    void sub1() {
    // forの範囲を指定
    int BEGIN = 0;
    int END = size1;

    // 並列処理実行 For( 開始Index , 終了Index , 一回分の処理を行う関数オブジェクト )
    System::Threading::Tasks::Parallel::For(
    BEGIN,
    END,
    gcnew Action(gcnew LoopObject(c1, c2), &LoopObject::ParallelFunction)
    );
    }

    };

    //
    //

    まとめ

    C++/CLIでの並列処理の定義は、OpenCLによるGPUを使った並列処理の方法によく似ているという印象を持ちました。
    OpenCLでは並列処理カーネルという単位で並列処理プログラムを定義します。GPUの演算プロセッサーは、多いものでは数千個あり、PCのCPUが普通2~8コアという数に対して桁違いに多数あります。
    OpenCLの並列処理に使われるカーネルプログラムは、私が書いたC++/CLIのソースコードのvoid ParallelFunction()に相当します。
    CPUのコアの数だけこの関数が並列に動作すると考えられます。

    並列処理で何を行っているかを解説すると、これもOpenCLで使われるものですが、int count1というカウンターをグローバルに用意しておき、カーネルが動作するごとにインクリメントしています。この時、x=x+1という方法では、x+1を求めた後で別の並列処理が走った場合xが書き換わり、正常にx=x+1が計算されません。これをアセンブリ言語のインクリメントに相当するアトミックIncという方法で、確実にインクリメントが行われるようにしています。
    このカウンターをバッファーに入れると、バッファーの要素が書き換わった順番が結果に入るという、面白いことが見られます。
    要素数100程度ではあまり大きな変化はありませんが、要素数を10000にしたところ、コア数4のCPUでは最初の要素から約2,500が0でなく7,500付近から始まるなど、約2,500(10,000/4=2,500)要素毎にまとまった数値が書き込まれていました。つまり4コアでの格納要素の取り合い競争が行われていることが確認できました。

    最後に、C++/CLIでの並列処理の応用分野は何が適しているのかと考えると、残念ながら相当多く(数千万以上)の要素を持つバッファーへの読み書きが行われないと、CPU単独動作に比べて顕著な動作速度の向上は見えてきませんでした。この処理速度の単独CPUと並列処理の比較は、以前にもVB.netで調査したことがありますが、.Net FrameworkでのParallel.forが動作していることに違いは無く、ほとんど改善効果がありませんでした。
    GPUを持たないPCの場合は、並列処理をOpenCLで行うと、複数コアへの処理分担をOpenCLがうまくやってくれるので、処理内容にも依りますが、単独CPUに比較して4コアの並列動作は動作速度で3割以上はアップします。

    今回の御解答は、非常に参考になりました。誠にありがとうございました。

    オカダ・システムエンジニアリング研究所

    • 詳しい返信をありがとうございます。
      どちらもほとんど使わないので軽く調べた限り、OpenCLはGPUによるデータ並列、Parallel::ForはCPUによるタスク並列という感じでしょうか。今回のサンプルはデータが独立しているので、データ並列向きなのかもしれません。件数が多ければですが。
      タスク並列でカーネルが動作するたびにインクリメントするような処理だとmutexが必要なので、処理が軽いほど不利になるので、自分で書くなら、
      thread[0] = job(data,0,20); // 0~19件を処理
      thread[1] = job(data,21,40);
      thread[2] = job(data,41,60);
      thread[3] = job(data,61,80);
      thread[4] = job(data,81,100);
      join();//全部終わるまで待機
      のようにある程度まとめて走らせられないか検討します。Parallel::Forも内部的にはそうしてるかもしれません(詳しくない)。
      あとスレッドプールも使いますが、Parallel::Forは自動で用意しているようですね。羨ましい。
      私も仕事柄スレッドは使いますが、総当たりのような、データ数に対して処理時間が指数的に増えていく場合には有効ですが、そうでない場合は苦労の割に得られるものが少ない印象です。

      • 御返答ありがとうございます。
        お考えの通り、Parallel::Forも内部的は正にそういう処理を自動で行っています。空いているコアに、上手にスレッドを分配してくれます。
        OpenCLで並列処理プロセッサーにCPUを選択した場合は、これはもう愚直にコア数だけスレッドを分配しますから、CPUの全コアを限界まで回す状態を作ることもでき、効率の良い処理ができます。
        私は、VB.netでの処理をC++/CLIとOpenCLに移植し、CPUの単独コア処理で70分掛かったものを0.5秒で完了させることにも成功しています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


この記事のトラックバックURL: