Каковы издержки оператора C # fixed на управляемой небезопасной структуре, содержащей фиксированные массивы? - PullRequest
13 голосов
/ 14 декабря 2011

Я пытался определить, какова истинная стоимость использования фиксированного оператора в C # для управляемых небезопасных структур, содержащих фиксированные массивы.Обратите внимание, что я не имею в виду неуправляемые структуры.

В частности, есть ли причина избегать шаблона, показанного классом 'MultipleFixed' ниже?Является ли стоимость простого исправления данных ненулевым, близким к нулю (== стоимость аналогична установке и очистке одного флага при входе / выходе из фиксированной области действия), или она достаточно значительна, чтобы ее по возможности избежать?

Очевидно, что эти классы придуманы, чтобы помочь объяснить вопрос.Это для структуры данных с высокой нагрузкой в ​​игре XNA, где производительность чтения / записи этих данных имеет решающее значение, поэтому, если мне нужно будет исправить массив и передать его везде, я сделаю это, но если разницы нет вообще, яЯ бы предпочел, чтобы метод fixed () был локальным по отношению к методам, чтобы подписи функций были немного более переносимыми для платформ, которые не поддерживают небезопасный код.(Да, это какой-то лишний хрюканский код, но все, что нужно)в том смысле, что речь идет только о чистом управляемом коде и конкретных затратах на его использование в этом контексте.

Спасибо за любую информацию!

Ответы [ 2 ]

11 голосов
/ 05 января 2015

Это был действительно интересный вопрос, который у меня был сам.

Результаты, которые мне удалось получить, указывают на несколько иные причины потери производительности, чем на сам фиксированный оператор.

Вы можете увидеть тесты, которые я запускаю, и результаты ниже, но есть следующие наблюдения, которые я извлек из них:

  • производительность использования 'fixed' с чистыми указателями (x *) без IntPtr такая же, как и в управляемом коде; в режиме Release это даже способ лучше , если исправлено не используется слишком часто - это самый эффективный способ доступа к множественным значениям массива
  • в режиме отладки, использование 'fixed' (внутри цикла) имеет большое отрицательное влияние на производительность, но в режиме Release оно работает почти так же хорошо, как обычный доступ к массиву (метод FixedAccess);
  • использование 'ref' для значения параметра эталонного типа (float []) было последовательно более или одинаково эффективным (оба режима)
  • Режим отладки имеет значительное падение производительности по сравнению с режимом выпуска при использовании арифметики IntPtr (IntPtrAccess), но для обоих режимов производительность была хуже, чем при обычном доступе к массиву
  • если использовать смещение, не выровненное по отношению к смещению значений массива, производительность будет ужасной, независимо от режима (на самом деле для обоих режимов требуется одинаковое количество времени). Это верно для 'float', но не влияет на 'int'

Запуск тестов несколько раз, дает немного разные, но в целом согласованные результаты. Вероятно, я должен был провести много серий тестов и взять среднее время - но у меня не было времени на это:)

Сначала тестовый класс:

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;
    }

}

И сами тесты:

    static int runs = 1000*1000*100;
    public static void Print (string name, Stopwatch sw) {
        Console.WriteLine ("{0}, items/sec = {1:N} \t {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);

и, наконец, результаты:

Режим отладки

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)

Режим разблокировки

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)
8 голосов
/ 14 декабря 2011

Эмпирически, издержки в лучшем случае составляют ~ 270% для 32-битного JIT и ~ 200% для 64-битных (и эти издержки ухудшаются по мере того, как вы "звоните" fixed).Поэтому я постараюсь минимизировать ваши fixed блоки, если производительность действительно критична.

Извините, я недостаточно знаком с фиксированным / небезопасным кодом, чтобы понять, почему это так


Подробности

Я также добавилнекоторые TestMore методы, которые вызывают два ваших тестовых метода 10 раз вместо 2, чтобы дать более реальный сценарий вызова нескольких методов для вашей fixed структуры.

Код, который я использовал:

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);
        }
    }
}

И результаты в .NET 4.0, 32-битный 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

И в .NET 4.0, 64-битный 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
...