web-dev-qa-db-ja.com

2次元配列をリスト(1次元)に変換する高速な方法

2次元配列があり、それをリスト(同じオブジェクト)に変換する必要があります。各要素を取得してリストに追加するforまたはforeachループでそれを実行したくありません。それを行う他の方法はありますか?

25
Yanshof

まあ、それは余分なコピーを作ることを意味しますが、「ブリット」な種類のコピーを使用するようにできます:(

_double[] tmp = new double[array.GetLength(0) * array.GetLength(1)];    
Buffer.BlockCopy(array, 0, tmp, 0, tmp.Length * sizeof(double));
List<double> list = new List<double>(tmp);
_

もちろん、1次元配列に満足している場合は、最後の行を無視してください:)

_Buffer.BlockCopy_は、検証後に非常に効率的なコピーを使用するためにexpectを使用するネイティブメソッドとして実装されます。 _List<T> constructor_を受け入れる_IEnumerable<T>_は、_IList<T>_と同じように_double[]_を実装する場合に最適化されています。適切なサイズのバッキング配列を作成し、その配列に自分自身をコピーするように要求します。うまくいけば、それは_Buffer.BlockCopy_または同様のものも使用します。

3つのアプローチ(forループ、Cast<double>().ToList()、およびBuffer.BlockCopy)の簡単なベンチマークを次に示します。

_using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        double[,] source = new double[1000, 1000];
        int iterations = 1000;

        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingCast(source);
        }
        sw.Stop();
        Console.WriteLine("LINQ: {0}", sw.ElapsedMilliseconds);

        GC.Collect();
        GC.WaitForPendingFinalizers();

        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingForLoop(source);
        }
        sw.Stop();
        Console.WriteLine("For loop: {0}", sw.ElapsedMilliseconds);

        GC.Collect();
        GC.WaitForPendingFinalizers();

        sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            UsingBlockCopy(source);
        }
        sw.Stop();
        Console.WriteLine("Block copy: {0}", sw.ElapsedMilliseconds);
    }


    static List<double> UsingCast(double[,] array)
    {
        return array.Cast<double>().ToList();
    }

    static List<double> UsingForLoop(double[,] array)
    {
        int width = array.GetLength(0);
        int height = array.GetLength(1);
        List<double> ret = new List<double>(width * height);
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                ret.Add(array[i, j]);
            }
        }
        return ret;
    }

    static List<double> UsingBlockCopy(double[,] array)
    {
        double[] tmp = new double[array.GetLength(0) * array.GetLength(1)];    
        Buffer.BlockCopy(array, 0, tmp, 0, tmp.Length * sizeof(double));
        List<double> list = new List<double>(tmp);
        return list;
    }
}
_

結果(ミリ秒単位の時間);

_LINQ: 253463
For loop: 9563
Block copy: 8697
_

編集:forループを変更して各反復でarray.GetLength()を呼び出すようにしたため、forループとブロックコピーはほぼ同じ時間で処理されます。

57
Jon Skeet

forループが最速の方法です。

LINQでそれができるかもしれませんが、それは遅くなります。また、自分でループを作成することはありませんが、内部的にはループが残っています。

  • ギザギザの配列の場合、おそらくarr.SelectMany(x=>x).ToList()のようなことができます。
  • _T[,]_の_IEnumerable<T>_は2D配列のすべての要素を返すので、_T[,]_では単にarr.ToList()を実行できます。 2D配列はIEnumerableのみを実装し、_IEnumerable<T>_は実装していないようです。したがって、yetanothercoderが提案するように_Cast<double>_を挿入する必要があります。それはボクシングのためにそれをさらに遅くします。

単純なループよりもコードを速くすることができる唯一のことは、要素の数を計算し、正しい容量でリストを構築することです。そのため、成長する必要はありません。
配列が長方形の場合、サイズを_width*height_として取得できますが、ギザギザの配列の場合はさらに難しくなる可能性があります。

_int width=1000;
int height=3000;
double[,] arr=new double[width,height];
List<double> list=new List<double>(width*height);
int size1=arr.GetLength(1);
int size0=arr.GetLength(0);
for(int i=0;i<size0;i++)
{  
  for(int j=0;j<size1;j++)
    list.Add(arr[i,j]);
}
_

理論的には、プライベートリフレクションと安全でないコードを使用して、生のメモリコピーを少し高速に実行できる可能性があります。しかし、私はそれに対して強く助言します。

11
CodesInChaos