皆様こんにちは。
AMDlabの齋藤です。
今回は並列処理をしてGrasshopperの関数の高速化をしたいと思います。
ただ、次のGrasshopper2では並列処理されるようにアップデートされるようなので、
そうなれば今回の内容はなくても並列処理ができると思います。
それでは始めましょう。
今回のデータはこちらに挙げております。
https://github.com/AMDlab/TechBlog-Parallel-Computing
6/5 追記
投稿後、様々なコメントを頂いたので、少し追記をさせていただきました。
今回は並列処理を簡単に実装して、高速化ができればいいなっという体で書かせていただきましたので、
より高速に処理ができる方法については、今回は取り扱いません。
並列処理とは?
まずは並列処理についてです。
並列処理とは、
並列処理とは、コンピュータに複数の処理装置を内蔵し、複数の命令の流れを同時に実行すること。
IT用語辞典 e-Words「並列処理 」
です。
具体的な内容は省きますが、普通のプログラムでは、上から順に処理を実行していくようになっています。
それを複数の処理を同時に実行することができるようにするのが並列処理です。
ここでポイントとなるのは、並列処理ができるものとできないものがあるということです。
並列処理ができるものとできないもの
並列処理ができるものとできないものがあります。
できるものは並列で処理できるもの、すなわち、結果が他の並列で処理されるものでないものです。
簡単な説明をChatGPTに尋ねると。
これを平易に説明すると、並列処理が可能なものは、例えば「各人が個別に問題を解く」という状況で、各人の解答は他の人の解答に影響を受けません。一方、並列処理ができないものは、「前の人が解いた問題の答えを使って次の問題を解く」状況で、前の人が解く問題の結果が次の人の問題解決に影響を及ぼします。後者の場合は、問題を順番に解かなければならず、一度に(並列に)解くことはできません。
と回答しました。
式にできるものの例を書くと
ここで
のような感じになります。
※詳しく書くと は処理、 は定数を想定します。
逆にできないものは結果が他の並列で処理されるものなので、
例として
ここで
のように、結果が他の並列処理(ここでは )に左右されるものになります。
並列で処理をしてみる
では、並列で処理をしてみます。
例として をしてみます。
Grasshopperで定数のを作ります。
これでaとbにランダムな配列を入れることができました。
ではC# Scriptで並列処理をしていきます。
実装の方法はParallel.ForとParallel.ForEachの2つでできます。
他にも非同期化させるAsync/Await等もありますが、C# Scriptでは普通に使えないため、この2つを使っていきます。
Parallel.Forで実装
Parallel.Forで実装していきます。Parallel.Forは簡単な処理の場合に適しているように思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System.Threading.Tasks; //--- usingディレクティブ ---- private void RunScript(List<double> a, List<double> b, ref object A) { // 答えを入れる配列を作る var y = new double[a.Count]; // 並列処理 Parallel.For(0, a.Count, index => { y[index] = a[index] * b[index]; }); A = y; } |
以下で内容を説明していきます。
1 |
using System.Threading.Tasks; |
Parallel.Forが使えるようusingディレクティブに using System.Threading.Tasks; を入れます。
5 6 |
// 答えを入れる配列を作る var y = new double[a.Count]; |
答えを入れる配列を作ります。ここで、答えがしっかり入れられるよう、配列の個数を決めるようにします。
8 9 10 11 |
// 並列処理 Parallel.For(0, a.Count, index => { y[index] = a[index] * b[index]; }); |
並列処理を行います。今回使ったParallel.Forは
Parallel.For (int fromInclusive, int toExclusive, Action<int> body); です。
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.parallel.for?view=netframework-4.0#system-threading-tasks-parallel-for(system-int32-system-int32-system-action((system-int32)))
実際に動かしてみます。
動きました。結果もちゃんと出ていそうです。
Parallel.ForEachで実装
次はParallel.ForEachで実装してみます。
Parallel.ForEachではクラスを作ってやり取りさせるとわかりやすく書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using System.Threading.Tasks; using System.Linq; //--- usingディレクティブ ---- private void RunScript(List<double> a, List<double> b, ref object A) { // kakezanListのリストを作成 var kakezanList = new List<Kakezan>(); for(int i = 0; i < a.Count; i++) kakezanList.Add(new Kakezan(a[i], b[i])); // 並列処理 Parallel.ForEach(kakezanList, kakezan => kakezan.Y = kakezan.Calc()); A = kakezanList.Select(kakezan => kakezan.Y); } // <Custom additional code> public class Kakezan { // a_i public double A; // b_i public double B; // y_i public double Y; public Kakezan(double a, double b) { A = a; B = b; } // 掛け算 public double Calc() { return A * B; } } // </Custom additional code> |
以下で内容を説明していきます。
1 2 |
using System.Threading.Tasks; using System.Linq; |
Parallel.ForEachが使えるようusingディレクティブに
using System.Threading.Tasks; を入れます。
今回は
using System.Linq; でSelect関数も使えるようにします。
6 7 8 9 |
// kakezanListのリストを作成 var kakezanList = new List<Kakezan>(); for(int i = 0; i < a.Count; i++) kakezanList.Add(new Kakezan(a[i], b[i])); |
kakezanListのリストを作成します。
Kakezanのクラスを作ってあるので、それが入れられるリストを作ります。
初期化でKakezanクラスのAとBの値を入れます。
11 12 13 |
// 並列処理 Parallel.ForEach(kakezanList, kakezan => kakezan.Y = kakezan.Calc()); |
並列処理を行います。今回使ったParallel.ForEachは
Parallel.ForEach<TSource> (System.Collections.Generic.IEnumerable<TSource> source, Action<TSource> body); です。
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.parallel.foreach?view=netframework-4.0#system-threading-tasks-parallel-foreach-1(system-collections-generic-ienumerable((-0))-system-action((-0)))
15 |
A = kakezanList.Select(kakezan => kakezan.Y); |
ここで結果を返します。
using System.Linq; のSelect関数を使えるようにしたので、kakezanListのYだけを出力させることができます。
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class Kakezan { // a_i public double A; // b_i public double B; // y_i public double Y; public Kakezan(double a, double b) { A = a; B = b; } // 掛け算 public double Calc() { return A * B; } } |
ここではKakezanクラスを構成しています。
このプログラムでは1つの計算に対して、このKakezanインスタンスを対応させるようにしています。
例えば の時、 kakezanListの1番目(最初が0番目)に入ってる
KakezanインスタンスのAが Bが となり、答えとなるYはとなります。
さらに、初期化では計算させずに、Calc()関数で計算させます。
ここで、返り値を出力させて、KakezanインスタンスのYにForEach上で入れることがポイントになります。
実際に動かしてみます。
こちらも動きました。ちゃんと結果が出ているか比較しましたが、どちらとも0になったのでおかしい結果となっていなさそうです。
例)線分を分割する
今回は構造設計でよく使いそうな、線分の分割をやってみようと思います。
はじめに、無作為な線分を用意します。
適当な範囲にPopulate2Dで点群を用意して、その2つの群を線分でつなぎました。
これから、以下のように並列処理をせず動かすことができます。
このようにして線分の分割ができます。今回はちゃんと分割されているかわかるようにEnd Pointsで表示させています。
しかし、見てわかるとおり、Curveの交点を算出するところで0.7秒時間がかかってしまいます。
そこで、この処理を並列処理で並列化したいと思います。
Parallel.ForEachで実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
using System.Threading.Tasks; using System.Linq; using Rhino.Geometry.Intersect; //--- usingディレクティブ ---- private void RunScript(List<Curve> LineCurves, double tol, ref object A) { // しきい値の入力 SplitIntersect.TOL = tol; // SplitIntersectのリストを作成 var splitIntersects = new List<SplitIntersect>(); foreach(var line in LineCurves){ splitIntersects.Add(new SplitIntersect( line, LineCurves.Where(item => item != line).ToList())); } // 並列処理 Parallel.ForEach(splitIntersects, splitIntersect => splitIntersect.Result = splitIntersect.Calc()); // 結果をDataTreeに入れて出力 var resultTree = new DataTree<Curve>(); for(int i = 0; i < splitIntersects.Count;i++) resultTree.AddRange(splitIntersects[i].Result, new GH_Path(i)); A = resultTree; } // <Custom additional code> public class SplitIntersect { public static double TOL; // Aは分割される線分 public Curve A; // Bsは分割する線分 public List<Curve> Bs; // 分割した結果を入れる public Curve[] Result; public SplitIntersect(Curve a, List<Curve> bs) { A = a; Bs = bs; } // 分割する計算 public Curve[] Calc() { var aSplit = new List<double>(){0, 1}; // Intersection.CurveCurveである線分に対しての交点の情報を出す foreach(var b in Bs){ var ci = Intersection.CurveCurve(A, b, TOL, TOL); foreach(var ei in ci){ // オーバーラップ(線分が重なるとき)は除外する if(ei.IsOverlap) continue; aSplit.Add(ei.ParameterA); } } return A.Split(aSplit); } } // </Custom additional code> |
以下で内容を説明していきます。
1 2 3 4 |
using System.Threading.Tasks; using System.Linq; using Rhino.Geometry.Intersect; //--- usingディレクティブ ---- |
Parallel.ForEachが使えるようusingディレクティブに
using System.Threading.Tasks; を入れます。
今回は
using System.Linq; でWhereやToList関数も使えるようにします。
さらに、交点を出したいので
using Rhino.Geometry.Intersect; を使えるようにします。
7 8 |
// しきい値の入力 SplitIntersect.TOL = tol; |
しきい値を入力します。
一々初期化で入力するのも面倒だったので、静的プロパティのTOLに入れられるようにしています。
10 11 12 13 14 15 |
// SplitIntersectのリストを作成 var splitIntersects = new List<SplitIntersect>(); foreach(var line in LineCurves){ splitIntersects.Add(new SplitIntersect( line, LineCurves.Where(item => item != line).ToList())); } |
SplitIntersectのリストを作成します。
初期化させるときにBsにAと同じCurveが入らないよう、Where関数で除外しています。
21 22 23 24 25 |
// 結果をDataTreeに入れて出力 var resultTree = new DataTree<Curve>(); for(int i = 0; i < splitIntersects.Count;i++) resultTree.AddRange(splitIntersects[i].Result, new GH_Path(i)); A = resultTree; |
結果をDataTreeに入れて出力します。
AddRange関数で入れることでリスト(IEnumerable)をGH_Pathを指定させてそのまま入れられます。
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// 分割する計算 public Curve[] Calc() { var aSplit = new List<double>(){0, 1}; // Intersection.CurveCurveである線分に対しての交点の情報を出す foreach(var b in Bs){ var ci = Intersection.CurveCurve(A, b, TOL, TOL); foreach(var ei in ci){ // オーバーラップ(線分が重なるとき)は除外する if(ei.IsOverlap) continue; aSplit.Add(ei.ParameterA); } } return A.Split(aSplit); } |
分割する計算をします。
ここではIntersectの関数、
CurveIntersections CurveCurve(CurvecurveA, CurvecurveB, double tolerance, double overlapTolerance) を使います。
そして
CurveIntersections (
ci として今回出力)は交点イベントのデータが配列として入っています。
個々の交点イベントの中でオーバーラップ(線分が重なるとき)は除外し、曲線Aに対しての交点位置のパラメーターを読み取ります。
最後に曲線Aをその読み取ったパラメーターでSplitをして出力します。
それでは実際に動かしてみましょう。
ちゃんと動きました!
End Pointsでしっかり分割されているか確認していますが、交点がしっかり出力されていることがわかります。
6/5 追記
内部の処理時間を記載し忘れておりました。
以下のようになります。さらに、C#並列処理なしの場合を試しておりませんでした。以下に示します。
今回のプログラムでは、処理が難しくないため、C#上ではそこまで早く処理できませんでした。
ただし、よりプログラムは複雑になりますが、1部を逐次処理化して処理を難しくすると、線分数1000の場合で、以下のように多少早くなる場合もあるようです。こちらも一応、今回の共有GitHubにあげております。
エラーになるとき
さて、ここまできましたが、内部関数でエラーになる場合があります。
その時は1つ以上のエラーがありますと出力されて、どう対応すればいいかわからなくなります。
そこで、try-catchを使って内部エラーを読みます。
1 2 3 4 5 |
try{ Parallel.ForEach(list, item => Calc(item)); }catch(Exception ex){ throw ex.InnerException; } |
それによってエラーがわかりやすくなります。
「見つかりにくいエラー」を投げていたので、それが隠れずに表示されました。
おわりに
いかがでしたでしょうか、
今回は並列処理を実装してみました。
これを使うことで、高速化を図ることができます。
ぜひ、やってみましょう!
COMMENTS