Search code examples
c#clrfixedunsafe

What is the overhead of the 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 a 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 the 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, it’s 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.


Solution

  • Empirically, the overhead appears to be, in the best case, ~270% on 32 bit JIT and ~200% on 64 bit (and the overhead gets worse the more times you "call" fixed). So I'd try to minimise your fixed blocks if performance is really critical.

    Sorry, I'm not familiar enough with fixed / unsafe code to know why that's the case


    Details

    I also added some TestMore methods which call your two test methods 10 times instead of 2 to give a more real world scenario of multiple methods being called on your fixed struct.

    The code I used:

    class Program
    {
        static void Main(string[] args)
        {
            var someData = new ByteArray();
            int iterations = 1000000000;
            var multiple = new MultipleFixed();
            var single = new SingleFixed();
    
            // Warmup.
            for (int i = 0; i < 100; i++)
            {
                multiple.Test(ref someData);
                single.Test(ref someData);
                multiple.TestMore(ref someData);
                single.TestMore(ref someData);
            }
    
            // Environment.
            if (Debugger.IsAttached)
                Console.WriteLine("Debugger is attached!!!!!!!!!! This run is invalid!");
            Console.WriteLine("CLR Version: " + Environment.Version);
            Console.WriteLine("Pointer size: {0} bytes", IntPtr.Size);
            Console.WriteLine("Iterations: " + iterations);
    
            Console.Write("Starting run for Single... ");
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                single.Test(ref someData);
            }
            sw.Stop();
            Console.WriteLine("Completed in {0:N3}ms - {1:N2}/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);
    
            Console.Write("Starting run for More Single... ");
            sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                single.Test(ref someData);
            }
            sw.Stop();
            Console.WriteLine("Completed in {0:N3}ms - {1:N2}/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);
    
    
            Console.Write("Starting run for Multiple... ");
            sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                multiple.Test(ref someData);
            }
            sw.Stop();
            Console.WriteLine("Completed in {0:N3}ms - {1:N2}/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);
    
            Console.Write("Starting run for More Multiple... ");
            sw = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++)
            {
                multiple.TestMore(ref someData);
            }
            sw.Stop();
            Console.WriteLine("Completed in {0:N3}ms - {1:N2}/sec", sw.Elapsed.TotalMilliseconds, iterations / sw.Elapsed.TotalSeconds);
    
    
            Console.ReadLine();
        }
    }
    
    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;
            }
        }
    
        public void Test(ref ByteArray bytes)
        {
            SetValue(ref bytes, 0, 1);
            Validate(ref bytes, 0, 1);
        }
        public void TestMore(ref ByteArray bytes)
        {
            SetValue(ref bytes, 0, 1);
            Validate(ref bytes, 0, 1);
            SetValue(ref bytes, 0, 2);
            Validate(ref bytes, 0, 2);
            SetValue(ref bytes, 0, 3);
            Validate(ref bytes, 0, 3);
            SetValue(ref bytes, 0, 4);
            Validate(ref bytes, 0, 4);
            SetValue(ref bytes, 0, 5);
            Validate(ref bytes, 0, 5);
        }
    }
    
    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;
        }
    
        public unsafe void Test(ref ByteArray bytes)
        {
            fixed (byte* data = bytes.Data)
            {
                SetValue(data, 0, 1);
                Validate(data, 0, 1);
            }
        }
        public unsafe void TestMore(ref ByteArray bytes)
        {
            fixed (byte* data = bytes.Data)
            {
                SetValue(data, 0, 1);
                Validate(data, 0, 1);
                SetValue(data, 0, 2);
                Validate(data, 0, 2);
                SetValue(data, 0, 3);
                Validate(data, 0, 3);
                SetValue(data, 0, 4);
                Validate(data, 0, 4);
                SetValue(data, 0, 5);
                Validate(data, 0, 5);
            }
        }
    }
    

    And the results in .NET 4.0, 32 bit JIT:

    CLR Version: 4.0.30319.239
    Pointer size: 4 bytes
    Iterations: 1000000000
    Starting run for Single... Completed in 2,092.350ms - 477,931,580.94/sec
    Starting run for More Single... Completed in 2,236.767ms - 447,073,934.63/sec
    Starting run for Multiple... Completed in 5,775.922ms - 173,132,528.92/sec
    Starting run for More Multiple... Completed in 26,637.862ms - 37,540,550.36/sec
    

    And in .NET 4.0, 64 bit JIT:

    CLR Version: 4.0.30319.239
    Pointer size: 8 bytes
    Iterations: 1000000000
    Starting run for Single... Completed in 2,907.946ms - 343,885,316.72/sec
    Starting run for More Single... Completed in 2,904.903ms - 344,245,585.63/sec
    Starting run for Multiple... Completed in 5,754.893ms - 173,765,185.93/sec
    Starting run for More Multiple... Completed in 18,679.593ms - 53,534,358.13/sec