Истинная небезопасная производительность кода
Я понимаю, что небезопасный код больше подходит для доступа к таким вещам, как 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 раз, показаны тесты для различных алгоритмов от байта [] до шестнадцатеричной строки.
Самый быстрый - это небезопасная реализация того же безопасного кода, который занимает второе место. Небезопасное в этом сравнении более чем в два раза быстрее.
Подробности этого алгоритма здесь.