Истинная небезопасная производительность кода

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

5 ответов

Некоторые измерения производительности

Преимущества производительности не так велики, как вы думаете.

Я провел некоторые измерения производительности нормального доступа к управляемым массивам по сравнению с небезопасными указателями в C#.


Результаты сборки, выполненной вне Visual Studio 2010, .NET 4, с использованием Any CPU | Выпуск основан на следующей спецификации ПК: ПК на базе x64, 1 четырехъядерный процессор. Intel64 Family 6 Model 23 Stepping 10 GenuineIntel ~ 2833 МГц.

Linear array access
 00:00:07.1053664 for Normal
 00:00:07.1197401 for Unsafe *(p + i)

Linear array access - with pointer increment
 00:00:07.1174493 for Normal
 00:00:10.0015947 for Unsafe (*p++)

Random array access
 00:00:42.5559436 for Normal
 00:00:40.5632554 for Unsafe

Random array access using Parallel.For(), with 4 processors
 00:00:10.6896303 for Normal
 00:00:10.1858376 for Unsafe

Обратите внимание, что небезопасные *(p++) идиома на самом деле бежала медленнее. Полагаю, это нарушило оптимизацию компилятора, которая сочетала в себе переменную цикла и доступ (сгенерированный компилятором) в безопасной версии.

Исходный код доступен на github.

Как отмечалось в других публикациях, вы можете использовать небезопасный код в очень специализированных контекстах, чтобы получить значительное улучшение производительности. Одним из таких сценариев является перебор массивов типов значений. Использование небезопасной арифметики указателей намного быстрее, чем использование обычного шаблона for-loop / indexer.

struct Foo
{
    int a = 1;
    int b = 2;
    int c = 0;
}

Foo[] fooArray = new Foo[100000];

fixed (Foo* foo = fooArray)  // foo now points to the first element in the array...
{
    var remaining = fooArray.length;
    while (remaining-- > 0)
    {
        foo->c = foo->a + foo->b;
        foo++;  // foo now points to the next element in the array...
    }
}

Основным преимуществом здесь является то, что мы полностью отключили проверку индекса массива.

Несмотря на высокую производительность, этот вид кода сложен в обращении, может быть довольно опасным (небезопасным) и нарушает некоторые фундаментальные правила (изменяемая структура). Но есть конечно сценарии, где это уместно...

Хороший пример - манипуляции с изображениями. Модификация пикселей с помощью указателя на их байты (что требует небезопасного кода) происходит немного быстрее.

Пример: http://www.gutgames.com/post/Using-Unsafe-Code-for-Faster-Image-Manipulation.aspx

При этом для большинства сценариев разница не будет столь заметной. Поэтому, прежде чем использовать небезопасный код, профилируйте свое приложение, чтобы увидеть узкие места в производительности и проверить, действительно ли небезопасный код является решением, позволяющим сделать его быстрее.

Я использую небезопасный код для видео манипулирования кодом. В таком коде вы хотите, чтобы он работал как можно быстрее без внутренних проверок значений и т. Д. Без небезопасных атрибутов я мог бы не справиться с потоком видео со скоростью 30 кадров в секунду или 60 кадров в секунду. (в зависимости от используемой камеры).

Но из-за скорости его широко используют люди, которые пишут графику.

Ну, я бы предложил прочитать этот пост в блоге: блоги MSDN: устранение проверки границ массива в CLR

Это разъясняет, как проверки границ выполняются в C#. Более того, тесты Томаса Братта кажутся мне бесполезными (глядя на код), так как JIT удаляет в своем цикле "сохранить" циклы проверки в любом случае.

Всем, кто смотрит на эти ответы, я хотел бы указать, что, хотя ответы отличные, многое изменилось, потому что ответы были опубликованы.

Обратите внимание, что.net немного изменился, и теперь у него есть возможность доступа к новым типам данных, таким как векторы, Span, ReadOnlySpan, а также к аппаратным библиотекам и классам, подобным тем, которые находятся в System.Runtime.Intrinsics в ядре 3.0.

Взгляните на это сообщение в блоге, чтобы узнать, как можно использовать оптимизированные для оборудования циклы, и в этом блоге, как вернуться к безопасным методам, если оптимальное оборудование недоступно.

В этом ответе на не имеющую отношения к делу статью о переполнении стека, просмотренную 881000 раз, показаны тесты для различных алгоритмов от байта [] до шестнадцатеричной строки.

Самый быстрый - это небезопасная реализация того же безопасного кода, который занимает второе место. Небезопасное в этом сравнении более чем в два раза быстрее.

Подробности этого алгоритма здесь.

Другие вопросы по тегам