7

有没有更快的方法使用 C# 来做到这一点?

double[,] myArray = new double[length1, length2];

for(int i=0;i<length1;i++)
    for(int j=0;j<length2;j++)
        myArray[i,j] = double.PositiveInfinity;

我记得使用 C++,有一些事情memset()需要做这些事情......

4

3 回答 3

5

多维数组只是一大块内存,所以我们可以把它当做一个,类似于它的memset()工作原理。这需要不安全的代码。除非它真的对性能至关重要,否则我不会说它值得做。不过,这是一个有趣的练习,所以这里有一些使用 BenchmarkDotNet 的基准:

    public class ArrayFillBenchmark
    {
        const int length1 = 1000;
        const int length2 = 1000;
        readonly double[,] _myArray = new double[length1, length2];

        [Benchmark]
        public void MultidimensionalArrayLoop()
        {
            for (int i = 0; i < length1; i++)
            for (int j = 0; j < length2; j++)
                _myArray[i, j] = double.PositiveInfinity;
        }

        [Benchmark]
        public unsafe void MultidimensionalArrayNaiveUnsafeLoop()
        {
            fixed (double* a = &_myArray[0, 0])
            {
                double* b = a;

                for (int i = 0; i < length1; i++)
                for (int j = 0; j < length2; j++)
                    *b++ = double.PositiveInfinity;
            }
        }

        [Benchmark]
        public unsafe void MultidimensionalSpanFill()
        {
            fixed (double* a = &_myArray[0, 0])
            {
                double* b = a;
                var span = new Span<double>(b, length1 * length2);
                span.Fill(double.PositiveInfinity);
            }
        }

        [Benchmark]
        public unsafe void MultidimensionalSseFill()
        {
            var vectorPositiveInfinity = Vector128.Create(double.PositiveInfinity);

            fixed (double* a = &_myArray[0, 0])
            {
                double* b = a;

                ulong i = 0;
                int size = Vector128<double>.Count;

                ulong length = length1 * length2;
                for (; i < (length & ~(ulong)15); i += 16)
                {
                    Sse2.Store(b+size*0, vectorPositiveInfinity);
                    Sse2.Store(b+size*1, vectorPositiveInfinity);
                    Sse2.Store(b+size*2, vectorPositiveInfinity);
                    Sse2.Store(b+size*3, vectorPositiveInfinity);
                    Sse2.Store(b+size*4, vectorPositiveInfinity);
                    Sse2.Store(b+size*5, vectorPositiveInfinity);
                    Sse2.Store(b+size*6, vectorPositiveInfinity);
                    Sse2.Store(b+size*7, vectorPositiveInfinity);
                    b += size*8;
                }
                for (; i < (length & ~(ulong)7); i += 8)
                {
                    Sse2.Store(b+size*0, vectorPositiveInfinity);
                    Sse2.Store(b+size*1, vectorPositiveInfinity);
                    Sse2.Store(b+size*2, vectorPositiveInfinity);
                    Sse2.Store(b+size*3, vectorPositiveInfinity);
                    b += size*4;
                }
                for (; i < (length & ~(ulong)3); i += 4)
                {
                    Sse2.Store(b+size*0, vectorPositiveInfinity);
                    Sse2.Store(b+size*1, vectorPositiveInfinity);
                    b += size*2;
                }
                for (; i < length; i++)
                {
                    *b++ = double.PositiveInfinity;
                }
            }
        }
    }

结果:

|                               Method |       Mean |     Error |    StdDev | Ratio |
|------------------------------------- |-----------:|----------:|----------:|------:|
|            MultidimensionalArrayLoop | 1,083.1 us | 11.797 us | 11.035 us |  1.00 |
| MultidimensionalArrayNaiveUnsafeLoop |   436.2 us |  8.567 us |  8.414 us |  0.40 |
|             MultidimensionalSpanFill |   321.2 us |  6.404 us | 10.875 us |  0.30 |
|              MultidimensionalSseFill |   231.9 us |  4.616 us | 11.323 us |  0.22 |

MultidimensionalArrayLoop由于边界检查,速度很慢。JIT 在每个循环中发出代码,以确保它[i, j]在数组的范围内。JIT 有时可以省略边界检查,我知道它适用于一维数组。我不确定它是否适用于多维。

MultidimensionalArrayNaiveUnsafeLoop本质上与代码相同,MultidimensionalArrayLoop但没有边界检查。它要快得多,需要 40% 的时间。但是,它被认为是“天真的”,因为循环仍然可以通过展开循环来改进。

MultidimensionalSpanFill也没有边界检查,并且或多或少与 相同MultidimensionalArrayNaiveUnsafeLoop,但是,Span.Fill在内部进行循环展开,这就是为什么它比我们天真的不安全循环快一点的原因。它只需要 30% 的时间作为我们原来的。

MultidimensionalSseFill通过做两件事来改进我们的第一个不安全循环:循环展开和矢量化。这需要一个支持 Sse2 的 CPU,但它允许我们在一条指令中写入 128 位(16 字节)。这为我们提供了额外的速度提升,将速度降低到原来的 22%。有趣的是,使用 Avx(256 位)的相同循环始终比 Sse2 版本慢,因此此处不包括基准测试。

但这些数字仅适用于 1000x1000 的数组。当您更改数组的大小时,结果会有所不同。例如,当我们将数组大小更改为 10000x10000 时,所有不安全基准的结果都非常接近。可能是因为对于较大的数组有更多的内存获取,它倾向于平衡最近三个基准测试中看到的较小的迭代改进。

那里有一个教训,但我主要只是想分享这些结果,因为这是一个非常有趣的实验。

于 2019-11-29T17:31:25.220 回答
1

我写的方法不是更快,但它适用于实际的多维数组,而不仅仅是 2D。

public static class ArrayExtensions
{
    public static void Fill(this Array array, object value)
    {
        var indicies = new int[array.Rank];

        Fill(array, 0, indicies, value);
    }

    public static void Fill(Array array, int dimension, int[] indicies, object value)
    {
        if (dimension < array.Rank)
        {
            for (int i = array.GetLowerBound(dimension); i <= array.GetUpperBound(dimension); i++)
            {
                indicies[dimension] = i;
                Fill(array, dimension + 1, indicies, value);
            }
        }
        else
            array.SetValue(value, indicies);
    }
}
于 2019-11-29T13:41:54.063 回答
1
double[,] myArray = new double[x, y];

if( parallel == true )
{
    stopWatch.Start();
    System.Threading.Tasks.Parallel.For( 0, x, i =>
    {
        for( int j = 0; j < y; ++j )
            myArray[i, j] = double.PositiveInfinity;  
    });
    stopWatch.Stop();
    Print( "Elapsed milliseconds: {0}", stopWatch.ElapsedMilliseconds );
}
else
{
    stopWatch.Start();
    for( int i = 0; i < x; ++i )
        for( int j = 0; j < y; ++j )
          myArray[i, j] = double.PositiveInfinity;  
    stopWatch.Stop();
    Print("Elapsed milliseconds: {0}", stopWatch.ElapsedMilliseconds);
}

设置时,x我得到了单线程方法和多线程方法。y10000553 milliseconds170

于 2019-11-29T14:31:18.317 回答