8

Is there a faster way of doing this using 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;

I remember using C++, there was something called memset() for doing these kind of things...

Sait
  • 19,045
  • 18
  • 72
  • 99
  • 4
    The way you have it looks fine. With C# you don't mess with the memory itself it is handled for you. There may be other syntax available for this, but it won't be any faster once compiled. – KingCronus Jul 26 '12 at 14:59
  • 3
    Is this purely academic or is this _the_ bottleneck in your code? It takes about 1.2sec to fill up ~2GB worth of doubles on my box. That being said, it seems like this should only be done once in an app so that 1.2sec wouldn't be all that bad. – Austin Salonen Jul 26 '12 at 15:11
  • I agree with KingCronus. There are a few different ways to do this, but none of them will essentially be any faster or more efficient than what you have posted. You were probably asking if there was a way to declare those values when you initialize the array...there isnt one that I have found. The array should initialize to the default double value. – Nevyn Jul 26 '12 at 15:21
  • To tell the truth, I haven't tested the speed of my code. I thought there should be another way which will be faster (e.g., a parallel way etc.). However, now it turns out that it is reasonably fast and I don't need any other way to do the same thing. Thanks. – Sait Jul 26 '12 at 15:25
  • @zagy If you parallelized this you would more than likely *hurt* performance, not help it. The bottleneck is the memory bus, not CPU. If you parallelize this you'll just end up accessing the memory out of order, which will actually *slow it down* as it is specifically designed to optimize accessing sequential blocks of memory. On top of that, you're adding the overhead of managing threads. – Servy Jul 26 '12 at 15:41
  • @Servy: Great than! Faster and simpler code. My favourite one! – Sait Jul 26 '12 at 15:51

4 Answers4

13

A multi-dimensional array is just a large block of memory, so we can treat it like one, similar to how memset() works. This requires unsafe code. I wouldn't say it's worth doing unless it's really performance critical. This is a fun exercise, though, so here are some benchmarks using 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;
                }
            }
        }
    }

Results:

|                               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 is slow because of bounds checking. The JIT emits code each loop that makes sure that [i, j] is inside the bounds of the array. The JIT can elide bounds checking sometimes, I know it does for single-dimensional arrays. I'm not sure if it does it for multi-dimensional.

MultidimensionalArrayNaiveUnsafeLoop is essentially the same code as MultidimensionalArrayLoop but without bounds checking. It's considerably faster, taking 40% of the time. It's considered 'Naive', though, because the loop could still be improved by unrolling the loop.

MultidimensionalSpanFill also has no bounds check, and is more-or-less the same as MultidimensionalArrayNaiveUnsafeLoop, however, Span.Fill internally does loop unrolling, which is why it's a bit faster than our naive unsafe loop. It only take 30% of the time as our original.

MultidimensionalSseFill improves on our first unsafe loop by doing two things: loop unrolling and vectorizing. This requires a CPU with Sse2 support, but it allows us to write 128-bits (16 bytes) in a single instruction. This gives us an additional speed boost, taking it down to 22% of the original. Interestingly, this same loop with Avx (256-bits) was consistently slower than the Sse2 version, so that benchmark is not included here.

But these numbers only apply to an array that is 1000x1000. As you change the size of the array, the results differ. For example, when we change the array size to 10000x10000, the results for all of the unsafe benchmarks are very close. Probably because there are more memory fetches for the larger array that it tends to equalize the smaller iterative improvements seen in the last three benchmarks.

There's a lesson in there somewhere, but I mostly just wanted to share these results, since it was a pretty fun experiment to do.

Christopher Currens
  • 29,917
  • 5
  • 57
  • 77
1

I wrote the method that is not faster, but it works with actual multidimensional arrays, not only 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);
    }
}
Mark Shevchenko
  • 7,937
  • 1
  • 25
  • 29
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);
}

When setting x and y to 10000 I get 553 milliseconds for the single-threaded approach and 170 for the multi-threaded one.

Soleil
  • 6,404
  • 5
  • 41
  • 61
Felipe Gutierrez
  • 675
  • 6
  • 17
0

There is a possibility to quickly fill an md-array that does not use the keyword unsafe (see answers for this question)

user3324131
  • 923
  • 8
  • 10