在.NET中将多维数组初始化为非默认值的最快方法是什么?

时间:2010-05-04 02:19:55

标签: .net initialization multidimensional-array

如何尽可能快地初始化基本类型的多维数组?

我坚持使用多维数组。我的问题是表现。以下例程初始化大约100x100阵列。 500蜱。删除int.MaxValue初始化导致大约。 180个滴答仅用于循环。大约100个刻度来创建数组而不循环并且没有初始化为int.MaxValue。

  • 与此类似的例程在“运行”期间被称为几十万到几百万次。
  • 在运行期间,数组大小不会更改,并且一次创建一个数组,使用,然后丢弃数组,并创建一个新数组。
  • 可以持续一分钟(使用10x10阵列)到45分钟(100x100)的“运行”。
  • 应用程序创建int,bool和struct数组。
  • 可以同时执行多个“运行”,但不是因为性能严重下降。
  • 我使用100x100作为基线。

我愿意接受有关如何优化数组的非默认初始化的建议。我的一个想法是在可用时使用较小的原始类型。例如,使用byte而不是int,可以节省100个滴答。我会对此感到满意,但我希望我不必更改原始数据类型。

    public int[,] CreateArray(Size size) {
        int[,] array = new int[size.Width, size.Height];
        for (int x = 0; x < size.Width; x++) {
            for (int y = 0; y < size.Height; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

使用以下内容减少到450个滴答:

    public int[,] CreateArray1(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int x = 0; x < iX; x++) {
            for (int y = 0; y < iY; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

CreateArray5;接受的实施:限制:无法调整大小,可以更改

在一次性初始化2800个滴答之后下降到大约165个滴答。 (请参阅下面的答案。)如果我可以让stackalloc使用多维数组,我应该能够获得相同的性能而无需初始化private static数组。

    private static bool _arrayInitialized5;
    private static int[,] _array5;

    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }

CreateArray8;接受的实施;限制:需要完全信任

在不使用上述“克隆技术”的情况下降至约165个滴答。 (请参阅下面的答案。)如果能算出CreateArray9的回报,我相信我可以降低价格。

    public unsafe static int[,] CreateArray8(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        fixed (int* pfixed = array) {
            int count = array.Length;
            for (int* p = pfixed; count-- > 0; p++)
                *p = int.MaxValue;
        }
        return array;
    }

备注

我提供有关此问题的所有代码和说明作为答案。希望它能在将来节省一些人的时间。

在大对象堆(LOH)上分配的数组不是本讨论的一部分。所听到的性能改进仅适用于堆上分配的数组。

Stackalloc

我使用stackalloc来消除初始化数组到默认值的想法没有用,因为必须从方法中复制分配的堆栈内存。意思是,我必须创建另一个数组来保存结果。该数组将被初始化,从而无法使用stackalloc

CreateArray8;不安全/固定方法

如果CLR位于完全受信任的程序集中,CLR将只执行unsafe代码。

CreateArray5;克隆方法

需要变量来确定数组是否已初始化并存储已初始化的数组。初始化后,性能与不安全/固定方法相同。请参阅Dan Tao对可能解决方案的回答。

性能提升300%?

我吮吸百分比,但300%是我想的(500到165刻度)。


申请的最终实施

对于这个应用程序,我决定使用“克隆”方法。以下是具有性能样本的应用程序中使用的“精简”通用实现。

初​​始化:

  • Grid<int>;通用克隆类初始化:4348,4336,4339,4654
  • Grid<bool>;通用克隆类初始化:2692,2684,3916,2680
  • Grid<Color>;通用克隆类初始化:3747,4630,2702,2708

使用:

  • Grid<int>;通用克隆类:185,159,152,290
  • Grid<bool>;通用克隆类:39,36,44,46
  • Grid<Color>;通用克隆类:2229,2431,2460,2466

    public class Grid<T> {
        private T[,] _array;
        private T _value;
        private bool _initialized;
        private int _x;
        private int _y;
        public Grid(Size size, T value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new T[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public T[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (T[,])_array.Clone();
        }
    }
    

7 个答案:

答案 0 :(得分:4)

关于Clone方法的说明:我怀疑你能否在性能方面击败它。但是,考虑到在第一次初始化之后,它可能是一个突破性的变化,它忽略了Size参数,并且在每次调用时只返回相同大小的数组。根据您的方案中实际上是否重要,您可以:

  1. 坚持下去,因为没关系。
  2. 创建Dictionary<Size, int[,]>(我相信 Size将作为密钥正常运行 - 尚未经过测试)每次唯一{{}时预初始化数组请求1}}。这个开销我不确定。
  3. 放弃Size想法。
  4. 如果您最终不得不使用上述3,那么这里有一些边缘荒谬的建议:

    <强> 1。在本地缓存您的CloneWidth属性,而不是在每次迭代时从Height结构中访问它们。

    Size

    要在我的机器上创建一个1000x1000阵列,这导致平均执行时间约为120000个刻度,而大约为140000个刻度。

    <强> 2。如果有的话,可以利用多个核心并并行初始化阵列。

    static int[,] CreateArray(Size size) {
        int w = size.Width;
        int h = size.Height;
    
        int[,] array = new int[w, h];
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                array[x, y] = int.MaxValue;
            }
        }
    
        return array;
    }
    

    这可能不是您场景中非常现实的建议,因为您似乎只创建了100x100阵列,在这种情况下,并行化的开销超过了性能提升。但是,为了创建一个1000x1000的数组,我发现这种方法平均将执行时间减少到大约70k(与我建议的第一次优化得到的~120k tick)相比。

    此外,如果您以这种方式创建多个阵列,我强烈建议并行化 (即,如果您需要创建一千个阵列,请从两个线程),假设您有多个处理器为您完成工作。没有多处理器,别忘了;添加线程只会损害您的性能。

    第3。使用static int[,] CreateArray(Size size) { int w = size.Width; int h = size.Height; int[,] array = new int[w, h]; Action<int[,], int, int> fillFirstHalf = FillArray; Action<int[,], int, int> fillSecondHalf = FillArray; var firstResult = fillFirstHalf.BeginInvoke(array, 0, h / 2, null, null); var secondResult = fillSecondHalf.BeginInvoke(array, h / 2, h, null, null); fillFirstHalf.EndInvoke(firstResult); fillSecondHalf.EndInvoke(secondResult); return array; } static void FillArray(int[,] array, int ystart, int yend) { int w = array.GetLength(0); for (int x = 0; x < w; ++x) { for (int y = ystart; y < yend; ++y) { array[x, y] = int.MaxValue; } } } 指针获得增强的性能。

    现在这里一个有趣的发现:似乎.NET中的二维数组以可预测的方式分配*:基本上作为一维内存块,其中每个“行”从起始点偏移相当于所有先前行的长度的量。换句话说,可以使用指针访问10x2数组,就像20x1数组一样;可以访问10x10阵列,如100x1阵列等。

    我不知道这是否是记录在案的行为。它可能是您不希望依赖的未指定的实现细节。无论哪种方式,都值得研究。

    * 大多数其他.NET开发人员可能已经知道了这一点,我只是说明显而已,在这种情况下,我取消了我对这个“有趣”的评论。

    在任何情况下,了解此信息都可以让您利用unsafe上下文中的fixed关键字获得显着的性能提升:

    unsafe

    为了初始化一个有意义的大小的数组,我甚至建议将上述方法(并行化)与这个方法结合起来 - 所以,在建议#2中保持相同的static int[,] CreateArray(Size size) { int w = size.Width; int h = size.Height; int[,] array = new int[w, h]; unsafe { fixed (int* ptr = array) { for (int i = 0; i < w * h; ++i) ptr[i] = int.MaxValue; } } return array; } ,然后重写CreateArray为:

    FillArray

    在我发布这篇文章之前,你似乎已经想出了这个最后一部分,但我认为我主要是为了将static void FillArray(int[,] array, int ystart, int yend) { int w = array.GetLength(0); unsafe { fixed (int* p = array) { for (int i = w * ystart; i < w * yend; ++i) p[i] = int.MaxValue; } } } 与并行化相结合这一点而包含它。


    关于unsafe的说明:我想你可能正在用彩虹追逐彩虹尽头的妖精。根据{{​​3}}:

      

    足够大小的内存块   包含stackalloc类型的expr个元素   在堆栈上分配,而不是   堆;块的地址是   存储在指针type中。这个记忆是   不受垃圾收集和影响   因此不必固定   (通过ptr)。 的生命   内存块仅限于   它的生命周期   定义。(强调我的)

    这让我相信你不能 返回一个对象,其数据存储在fixed从函数分配的内存中,因为该内存只是分配给函数的生命周期。

答案 1 :(得分:3)

粒度控制的非托管数组

在C#unmanaged(unsafe)模式下创建数组,如shown here[code project]并手动初始化。

在C#托管模式下,在循环开始填充之前,数组首先将所有元素初始化为默认值。您可以在非托管模式下减少加倍。这样可以节省很多时间。

答案 2 :(得分:2)

添加staticunsafe可以减少Ticks的数量。以下是一些样本。

  • CreateArray;非静态非不安全:521,464,453,474
  • CreateArray;静态:430,423,418,454
  • CreateArray;不安全:485,464,435,414
  • CreateArray; static unsafe:476,450,433,405

我尝试使用stackalloc。我的想法是分配数组,因为它是unsafe代码而不会被初始化。然后我会压缩数组,在我去的时候初始化int.MaxValue,然后Clone数组返回结果。但是,我对多维宣言感到困惑。

然后我记得在另一个项目中对数组使用Clone。每个Array.Clone都节省了几秒钟。基于这个想法,我创建了以下版本的CreateArray例程,获得了很好的结果。

现在,我需要做的就是让stackalloc使用多维数组。

  • CreateArray5;预初始化:2663,3036
  • CreateArray5;克隆: 157,172

    private static bool _arrayInitialized5;
    private static int[,] _array5;
    
    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }
    

    int[,] actual;

    int iHi = 10000 * 10 * 2; 
    //'absolute minimum times array will be created   (200,000)
    //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

    Stopwatch o;

    //'pre-initialize
    o = Stopwatch.StartNew();
    actual = CreateArray5(new Size(100, 100));
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks, "CreateArray5; pre-initialize");
    o = Stopwatch.StartNew();
    for (int i = 0; i < iHi; i++) { actual = CreateArray5(new Size(100, 100)); }
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray5; static unsafe clone");

答案 3 :(得分:1)

在这种情况下,这是未经测试的,但在类似的情况下也有效。 有时,交换数组遍历的顺序会因内存局部性而加快速度。

换句话说,而不是for(x) ... for(y)代替for(y) ... for(x)

答案 4 :(得分:0)

您可以使用并行库来并行使用每一行来初始化它,它会更快。

但是,我认为这是有限制的,只有64个操作可以排队,但在这种情况下你可以在Parallel.For中排队0到63,64到127等..

public int[,] CreateArray(Size size) { 
    int[,] array = new int[size.Width, size.Height]; 
    System.Threading.Paralle.For (0,size.Width, 
      x=>{ 
        for (int y = 0; y < size.Height; y++) { 
            array[x, y] = int.MaxValue; 
        } 
      }
    ); 
    return array; 
} 

这包含在.NEt 4中,但是对于.NET 3.51,您可以从codeplex获得相同的库。

答案 5 :(得分:0)

我能够将得分降至约165.请参阅下面的CreateArray8

我从jdk提供的http://www.codeproject.com/KB/dotnet/arrays.aspx的CodeProject文章的“范围检查消除”部分得到了一个想法。 (@jdk,非常感谢。)我们的想法是通过使用指针并在一个循环中初始化每个元素来消除范围检查。我能够将得分降低到大约165.与克隆一样好,没有预先初始化和支持静态变量的延迟。 (见我的另一个答案。)

我敢打赌,如果我能算出CreateArray9的回报,我可以减少一半。

  • CreateArray3; static unsafe:501,462,464,462
  • CreateArray7; (y,x)静态不安全:452,451,444,429
  • CreateArray8;静态不安全指针single_for: 187,173,156,145

[TestClass]
public class CreateArrayTest {

    public static unsafe int[,] CreateArray3(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int x = 0; x < iX; x++) {
            for (int y = 0; y < iY; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

    public unsafe static int[,] CreateArray7(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int y = 0; y < iY; y++) {
            for (int x = 0; x < iX; x++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

    public unsafe static int[,] CreateArray8(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        fixed (int* pfixed = array) {
            int count = array.Length;
            for (int* p = pfixed; count-- > 0; p++)
                *p = int.MaxValue;
        }
        return array;
    }

    public unsafe static int[,] CreateArray9(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        void* array = stackalloc int[iX * iY];
        int count = iX * iY;
        for (int* p = (int*)array; count-- > 0; p++)
            *p = int.MaxValue;

        //return (int[,])array; //how to return?
        return new int[1, 1];
    }

    [TestMethod()]
    public void CreateArray_Test() {

        int[,] actual;

        int iHi = 10000 * 10 * 2;
        //'absolute minimum times array will be created   (200,000)
        //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

        Stopwatch o;

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray3(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray3; static unsafe");

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray7(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray7; static unsafe for(y,x)");

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray8(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray8; static unsafe pointer single_for");

    }

}

答案 6 :(得分:0)

使用“克隆”方法的类和通用。

  • MDArray; clone class initalize:2444,2587,2421,2406
  • MDArray;克隆类:440,362,198,139
  • Grid<int>;通用克隆类初始化:5344,5334,5693,5272
  • Grid<int>;通用克隆类:187,204,199,288
  • Grid<bool>;通用克隆类初始化:3585,3537,3552,3569
  • Grid<bool>;通用克隆类:37,44,36,43
  • Grid<Color>;通用克隆类初始化:4139,3536,3503,3533
  • Grid<Color>;通用克隆类:2737,3177,2414,2171

[TestClass]
public class CreateArrayTest {

    public class MDArray {
        private bool _initialized;
        private int[,] _array;
        private int _x;
        private int _y;
        private int _value;
        public MDArray(Size size, int value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new int[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public int[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (int[,])_array.Clone();
        }
    }

    public class Grid<T> {
        private T[,] _array;
        private T _value;
        private bool _initialized;
        private int _x;
        private int _y;
        public Grid(Size size, T value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new T[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public T[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (T[,])_array.Clone();
        }
    }

    [TestMethod()]
    public void CreateArray_Test() {

        int[,] actual;

        int iHi = 10000 * 10 * 2; //'absolute minimum times array will be created   (200,000)
        //                          'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

        Stopwatch o;

        o = Stopwatch.StartNew();
        MDArray oMDArray = new MDArray(new Size(100, 100), int.MaxValue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "     MDArray; clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = oMDArray.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "     MDArray; clone class");

        o = Stopwatch.StartNew();
        Grid<int> oIntMap = new Grid<int>(new Size(100, 100), int.MaxValue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "   Grid<int>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = oIntMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "   Grid<int>; generic clone class");

        bool[,] fActual;
        o = Stopwatch.StartNew();
        Grid<bool> oBolMap = new Grid<bool>(new Size(100, 100), true, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "  Grid<bool>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { fActual = oBolMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "  Grid<bool>; generic clone class");

        Color[,] oActual;
        o = Stopwatch.StartNew();
        Grid<Color> oColMap = new Grid<Color>(new Size(100, 100), Color.AliceBlue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, " Grid<Color>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { oActual = oColMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, " Grid<Color>; generic clone class");
    }
}