Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
263 views
in Technique[技术] by (71.8m points)

clr - What is the overhead of C# fixed statement on a managed unsafe struct containing fixed arrays?

I've been trying to determine what the true cost of using the fixed statement within C# for managed unsafe structs that contain fixed arrays. Please note I am not referring to unmanaged structs.

Specifically, is there any reason to avoid the pattern shown by 'MultipleFixed' class below? Is the cost of simply fixing the data non zero, near zero (== cost similar to setting & clearing a single flag when entering/exiting the fixed scope), or is it significant enough to avoid when possible?

Obviously these classes are contrived to help explain the question. This is for a high usage data structure in an XNA game where read/write performance of this data is critical, so if I need to fix the array and pass it around everywhere I'll do that but if there is no difference at all I'd prefer to keep the fixed() local to the methods to help with keeping the function signatures slightly more portable to platforms that don't support unsafe code. (Yeah, its some extra grunt code but whatever it takes..)

 

    unsafe struct ByteArray
    {
       public fixed byte Data[1024];
    }

    class MultipleFixed
    {
       unsafe void SetValue(ref ByteArray bytes, int index, byte value)
       {
           fixed(byte* data = bytes.Data)
           {
               data[index] = value;
           }
       }

        unsafe bool Validate(ref ByteArray bytes, int index, byte expectedValue)
        {
           fixed(byte* data = bytes.Data)
           {
               return data[index] == expectedValue;
           }
        }

        void Test(ref ByteArray bytes)
        {
            SetValue(ref bytes, 0, 1);
            Validate(ref bytes, 0, 1);
        }
    }

    class SingleFixed
    {
       unsafe void SetValue(byte* data, int index, byte value)
       {
           data[index] = value;
       }

        unsafe bool Validate(byte* data, int index, byte expectedValue)
        {
           return data[index] == expectedValue;
        }

        unsafe void Test(ref ByteArray bytes)
        {
            fixed(byte* data = bytes.Data)
            {
                SetValue(data, 0, 1);
                Validate(data, 0, 1);
            }
        }
    }

Also, I looked for similar questions and the closest I found was this, but this question is different in that it is concerned only with pure managed code and the specific costs of using fixed in that context.

Thanks for any info!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

That was actually interesting question that I had myself.

The results I managed to obtain suggest slightly different reasons for performance loss than 'fixed' statement itself.

You can see the tests I run and the results below but there are following observations I draw from those:

  • the performance of using 'fixed' with pure pointers (x*), without IntPtr, is as good as in managed code; in Release Mode, it is even way better if fixed is not used too often - that's the most performent way of accessing multiple array values
  • in Debug Mode, using 'fixed' (inside a loop) has big negative performance impact but in Release Mode, it works almost as good as normal array access (method FixedAccess);
  • using 'ref' on a reference-type parameter value (float[]) was consistently more or equally performant (both modes)
  • Debug Mode has significant performance drop vs Release Mode when using IntPtr arithmetic (IntPtrAccess) but for both modes the performance was worse than normal array access
  • if using using offset not aligned to the array's values' offset, the performance is terrible, regardless of the mode (it actually takes the same amount of time to for both modes). That holds true for 'float' but has no impact for 'int'

Running the tests multiple times, gives slightly different but broadly consistent results. Probably I should have ran many series of tests and take the average times - but had no time for that :)

The test class first:

class Test {
    public static void NormalAccess (float[] array, int index) {
        array[index] = array[index] + 2;
    }

    public static void NormalRefAccess (ref float[] array, int index) {
        array[index] = array[index] + 2;
    }

    public static void IntPtrAccess (IntPtr arrayPtr, int index) {
        unsafe {
            var array = (float*) IntPtr.Add (arrayPtr, index << 2);
            (*array) = (*array) + 2;
        }
    }

    public static void IntPtrMisalignedAccess (IntPtr arrayPtr, int index) {
        unsafe {
            var array = (float*) IntPtr.Add (arrayPtr, index); // getting bits of a float
            (*array) = (*array) + 2;
        }
    }

    public static void FixedAccess (float[] array, int index) {
        unsafe {
            fixed (float* ptr = &array[index]) 
                (*ptr) = (*ptr) + 2;
        }
    }

    public unsafe static void PtrAccess (float* ptr) {
        (*ptr) = (*ptr) + 2;
    }

}

And the tests themselves:

    static int runs = 1000*1000*100;
    public static void Print (string name, Stopwatch sw) {
        Console.WriteLine ("{0}, items/sec = {1:N}  {2}", sw.Elapsed, (runs / sw.ElapsedMilliseconds) * 1000, name);
    }

    static void Main (string[] args) {
        var buffer = new float[1024*1024*100];
        var len = buffer.Length;

        var sw = new Stopwatch();
        for (int i = 0; i < 1000; i++) { 
            Test.FixedAccess (buffer, 55);
            Test.NormalAccess (buffer, 66);
        }

        Console.WriteLine ("Starting {0:N0} items", runs);


        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.NormalAccess (buffer, i % len);
        sw.Stop ();

        Print ("Normal access", sw);

        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.NormalRefAccess (ref buffer, i % len);
        sw.Stop ();

        Print ("Normal Ref access", sw);

        sw.Restart ();
        unsafe {
            fixed (float* ptr = &buffer[0])
                for (int i = 0; i < runs; i++) { 
                    Test.IntPtrAccess ((IntPtr) ptr, i % len);
                }
        }
        sw.Stop ();

        Print ("IntPtr access (fixed outside loop)", sw);

        sw.Restart ();
        unsafe {
            fixed (float* ptr = &buffer[0])
                for (int i = 0; i < runs; i++) { 
                    Test.IntPtrMisalignedAccess ((IntPtr) ptr, i % len);
                }
        }
        sw.Stop ();

        Print ("IntPtr Misaligned access (fixed outside loop)", sw);

        sw.Restart ();
        for (int i = 0; i < runs; i++) 
            Test.FixedAccess (buffer, i % len);
        sw.Stop ();

        Print ("Fixed access (fixed inside loop)", sw);

        sw.Restart ();
        unsafe {
            fixed (float* ptr = &buffer[0]) { 
                for (int i = 0; i < runs; i++) { 
                    Test.PtrAccess (ptr + (i % len));
                }
            }
        }
        sw.Stop ();

        Print ("float* access (fixed outside loop)", sw);

        sw.Restart ();
        unsafe {
            for (int i = 0; i < runs; i++) { 
                fixed (float* ptr = &buffer[i % len]) { 
                    Test.PtrAccess (ptr);
                }
            }
        }
        sw.Stop ();

        Print ("float* access (fixed in loop)", sw);

and finally the results:

Debug mode

Starting 100,000,000 items
00:00:01.0373583, items/sec = 96,432,000.00      Normal access
00:00:00.8582307, items/sec = 116,550,000.00     Normal Ref access
00:00:01.8822085, items/sec = 53,134,000.00      IntPtr access (fixed outside loop)
00:00:10.5356369, items/sec = 9,492,000.00       IntPtr Misaligned access (fixed outside loop)
00:00:01.6860701, items/sec = 59,311,000.00      Fixed access (fixed inside loop)
00:00:00.7577868, items/sec = 132,100,000.00     float* access (fixed outside loop)
00:00:01.0387792, items/sec = 96,339,000.00      float* access (fixed in loop)

Release mode

Starting 100,000,000 items
00:00:00.7454832, items/sec = 134,228,000.00     Normal access
00:00:00.6619090, items/sec = 151,285,000.00     Normal Ref access
00:00:00.9859089, items/sec = 101,522,000.00     IntPtr access (fixed outside loop)
00:00:10.1289018, items/sec = 9,873,000.00       IntPtr Misaligned access (fixed outside loop)
00:00:00.7899355, items/sec = 126,742,000.00     Fixed access (fixed inside loop)
00:00:00.5718507, items/sec = 175,131,000.00     float* access (fixed outside loop)
00:00:00.6842333, items/sec = 146,198,000.00     float* access (fixed in loop)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...