おれんじりりぃぶろぐ

きっと何者にもなれないエンジニアのブログ

Unityでの並列プログラミング(Parallel、PLinq)のパフォーマンス比較

概略

.NET4.0からは、簡易に並列処理を記述することができるParallelクラス、Parallel LINQ(PLinq)の提供がはじまりました。 今回は、前半でParallクラス、PLinqの使い方を見て、後半ではUnityでの実際のパフォーマンス調査を行った結果を示します。

Parallelクラス、PLinqクラスの使い方

For文

Parallel.For(0, N, i =>
{
    lock(syncObj){sum += i ;}
});

Foreach文

foreach (var x in data)
{
    lock(syncObj){sum += x;}
}

同じリソース(今の場合はsum)へのアクセスがある場合は排他制御が必要になります。

PLinq

data.AsParallel().Sum(x => x );

通常のLinq構文にAsParallel()をつけるだけになります。 PLinqの場合には、排他制御はシステム側が行ってくれます。

パフォーマンス検証

実行環境

  • Unity2018.1b13
    • .NET4.x
    • il2cpp
  • iPhone7Plus
  • android s8

実施項目

0 〜 29999の単純な加算を以下の4つの方法で処理時間測定した * 通常のFor文(SequentialSum) * Parallel.For文(ParallelSum) * 通常のLinq(PlaneLinqSum) * PLinq(PlinqSum)

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;

public class ParallelSample : MonoBehaviour
{

    private const int N = 30000;
    private System.Object syncObj = new System.Object();
    
    
    private void Start()
    {
        
    }

    public void SequentialSum()
    {
        int starttime = DateTime.Now.Hour * 60 *60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                    DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        for (int i = 0; i < N; i++)
        {
            sum += i;
        }
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
              DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"SequentialSum()実行時間: {now - starttime}ms");
    }

    public void ParallelSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        int sum = 0;
        Parallel.For(0, N, i =>
        {
            lock(syncObj){sum += i ;}
        });
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"ParallelSum()実行時間: {now - starttime}ms");
    }

    public void PlaneLinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlaneLinqSum()実行時間: {now - starttime}ms");
    }

    public void PlinqSum()
    {
        int starttime = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                        DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        var data = Enumerable.Range(0, N);
        
        int sum = data.AsParallel().Sum(x => x );
        Debug.Log($"sum: {sum}");
        
        int now = DateTime.Now.Hour * 60 * 60 * 1000 + DateTime.Now.Minute * 60 * 1000 + 
                  DateTime.Now.Second * 1000 + DateTime.Now.Millisecond;
        
        Debug.Log($"PlinqSum()実行時間: {now - starttime}ms");
    }
}

結果

SequentialSum ParallelSum PlaneLinqSum PlinqSum
Editor 4.4 22.2 7.8 31.2
ios 4.2 16.0 8.0 x
android 2.0 58.6 5.0 x

5回の平均値 単位はミリSec il2cppでのPlinqは以下のランタイムエラーが出て実行不可能

Unity   : ExecutionEngineException: Attempting to call method 'System.Linq.Parallel.SelectQueryOperator`2<System.Int32, System.Int32>::WrapPartitionedStream<System.Int32>' for which no ahead of time (AOT) code was generated. 

まとめ

  • PLinqはil2cpp環境では使用できない
  • 並列ループ内で同じリソースにアクセスする場合には、排他制御を行わなければならず結局は同期的な処理に近くなってしまう。さらにオーバーヘッドが大きく通常のFor文よりも遅くなる
  • Linqは気になるほどではないが通常のFor文に比べると遅い

参考