C#: Небезопасный(Unsafe) код
C # также имеет возможность получить доступ к памяти напрямую. Рассмотрим следующий код C, где
byte
typedef для char
(1 байт):int main() {
int a = 35181;
byte* b = (byte*)&a;
byte b1 = *b;
byte b2 = *(b + 1);
byte b3 = *(b + 2);
byte b4 = *(b + 3);
return 0;
}
Этот код позволит нам разделить 4 байтовый целочисленный тип на его компоненты. Приятно, однако, этот код также отлично подходит и для C#!
unsafe static void Main()
{
int a = 35181;
byte* b = (byte*)&a;
byte b1 = *b;
byte b2 = *(b + 1);
byte b3 = *(b + 2);
byte b4 = *(b + 3);
}
К сожалению (или, к счастью, как мы увидим позже), это не работает из коробки, поскольку опция компиляции по умолчанию исключает небезопасные контексты. Мы должны включить параметр «Разрешить небезопасный код» в свойствах соответствующего проекта. Соответствующий диалог отображается на следующем изображении:

Есть некоторые причины для этого:
- Некоторые платформы запускают C# только в управляемом режиме, запрещая любую небезопасную компиляцию. Следовательно, код, написанный с помощью
unsafe
блоков, не будет работать там. - Управляется общий тип объектов, что означает, что их можно переместить в память. Если у нас есть (фиксированные) указатели на их адреса, мы можем оказаться в недопустимом участке памяти (ошибка сегментации), так как эти объекты могли быть перемещены без нашего знания об этом.
- Небезопасный код не может быть оптимизирован, как управляемый код. Это не так плохо, как для языка в C (с точки зрения оптимизации), но может иметь негативные последствия для производительности.
Тем не менее, небезопасный код может также привести к устранению некоторых проблем с производительностью. В небезопасных блоках мы могли перебирать массив без использования оператора индекса C#. Это хорошо, поскольку оператор индекса C # вызывает некоторые накладные расходы, проверяя, находится ли он в границах. В некоторых случаях это может быть огромной проблемой, например, при анализе растрового изображения.
Прежде чем мы обсудим этот пример для эффективного использования ключевого слова
unsafe
, давайте обсудим некоторые другие свойства (или функции) небезопасных областей. Мы можем создавать полные области, методы или определения (например, class
), используя ключевое слово unsafe
:unsafe class MyClass
{
/* pointers can be used here */
}
class MyOtherClass
{
unsafe void PerformanceCriticalMethod()
{
/* pointers can be used here */
}
}
class MyThirdClass
{
void ImportantMethod()
{
/* some stuff */
unsafe
{
/* pointers can be used here */
}
/* more stuff */
}
}
Пример использования ключевого слова fixed.
static int x;
unsafe static void F(int* p)
{
*p = 1;
}
static void Main()
{
int[] a = new int[10];
unsafe
{
fixed (int* p = &x) F(p);
fixed (int* p = &a[0]) F(p);
fixed (int* p = a) F(p);
}
}
fixed
оператор используется для фиксации массива, поэтому его адрес может быть передан в метод , который принимает указатель.static void Main()
{
int[,,] a = new int[2,3,4];
unsafe
{
fixed (int* p = a)
{
for (int i = 0; i < a.Length; ++i)
p[i] = i;
}
}
}
Последнее (прежде чем мы обсудим пример эффективного доступа к данным растрового изображения), что интересно в отношении такого небезопасного кода, - это возможность замены
new
на
stackalloc
. Проблема заключается в следующем: Что делать , если мы хотим выделить память из стека вызовов , который будет использоваться массивом элементарного (или неуправляемого) типа (например char
, int
, double
, ...)? Прямо сейчас каждый массив естественно помещается в кучу, где он управляется сборщиком мусора.char* buffer = stackalloc char[16];
Ключевое слово stackalloc
может быть использовано только в unsafe
контексте. Выделенная память автоматически отбрасывается, когда область остается. В основном ключевое слово эквивалентно new
, с той разницей, что очистка выполняется мгновенно после выхода из области видимости, что может привести к уменьшению количества удаляемых объектов сборщиком мусора и повышению производительности.Это эквивалентно
alloc
функции, обычно используемой в реализациях C и C ++.Возвращаемся к обещанному примеру повышения производительности при манипулировании изображениями с помощью небезопасного блока. Проблема в том, что единственный доступ в C# к растровым данным (в
Bitmap
классе) задается путем итерации по двумерному массиву. Каждое значение представляет собой структуру с несколькими значениями для цветов. Вся ситуация зависит, естественно, от растрового изображения (например, от количества байтов на цвет).var path = @"1680x1050_1mb.jpg";
var bmp = (Bitmap)Bitmap.FromFile(path);
var sw = Stopwatch.StartNew();
for (var i = 0; i < bmp.Width; i++)
{
for (var j = 0; j < bmp.Height; j++)
{
bmp.GetPixel(i, j);
}
}
sw.Stop();
Console.WriteLine(sw);
Этот (бессмысленный) код выполняется около 2550 мс на моей машине. Давайте использовать небезопасный код, чтобы ускорить его!
var path = @"1680x1050_1mb.jpg";
var bmp = (Bitmap)Bitmap.FromFile(path);
//This is basically a (managed) call to fix the bitmap (no memory movement)
var data = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, bmp.PixelFormat);
//This gets the number of bytes per pixel, usually the same as the number of colors ...
var bpp = data.Stride / data.Width;
var sw = Stopwatch.StartNew();
unsafe
{
//This gets the base pointer address - where we can start to scan
byte* scan0 = (byte*)data.Scan0.ToPointer();
//This is just a duplicate, however, the one which will be moved
byte* scan = scan0;
for (var i = 0; i < bmp.Width; i++)
{
for (var j = 0; j < bmp.Height; j++)
scan += bpp;
}
}
sw.Stop();
//Here we free the bitmap, so that it can be moved again
bmp.UnlockBits(data);
Console.WriteLine(sw);
На моей машине этот код отрабатывает за 335 мс, или примерно в 8 раз быстрее. Один из самых больших недостатков состоит в том, что у нас все еще есть 2 цикла. Конечно, он может быть сжат в один цикл, однако теперь мы хотим быть еще быстрее, не переходя к неуправляемому коду.
Как мы можем достичь этого? Волшебное слово: Interoperability (или короткий переход). COM-взаимодействие - это технология, включенная в .NET CLR, которая позволяет COM-объектам взаимодействовать с объектами .NET и наоборот. Мы можем использовать это, чтобы позволить собственному коду выполнять некоторое копирование массива. После этого мы получаем доступ к данным байта изображения в линейном массиве.
var path = @"1680x1050_1mb.jpg";
var bmp = (Bitmap)Bitmap.FromFile(path);
//Same as before - this time required for the interop
var data = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), ImageLockMode.ReadOnly, bmp.PixelFormat);
//We still need to know how many bytes to skip per pixel
var bpp = data.Stride / data.Width;
//Now we need the number of bytes - data.Stride is a whole line
var bytes = data.Stride * bmp.Height;
//Just to do something in the loop
byte value;
var sw = Stopwatch.StartNew();
//Get the start address - this time it is enough to stay with the IntPtr type
var ptr = data.Scan0;
//Create an empty byte array - this will be the destination
var values = new byte[bytes];
//Marshal is a static class with interesting interop calls
Marshal.Copy(ptr, values, 0, bytes);
for (var i = 0; i < bytes; i += bpp)
value = values[i];
sw.Stop();
//Again unlocking is important
bmp.UnlockBits(data);
Console.WriteLine(sw);
Здесь мы начинаем копирование по заданному адресу, указанному
IntPtr
экземпляром. В качестве адресата мы передаем массив байтов values
, который имеет смещение 0. В конце мы хотим получить bytes
.Этот код работает всего 14 мс, что примерно в 20 раз быстрее, чем раньше, и в 160 раз быстрее, чем первая версия. Независимо от этого прироста производительности, мы все равно должны отметить, что никакой реальной работы не было сделано ни в одном из этих трех сценариев, поэтому второй или третий код имеют как хорошие, так и плохие стороны.
Проблема с третьим заключается в том, что изменения на изображении должны быть перенесены обратно в исходный источник данных. Итак, здесь у нас много передач памяти, с дублированием памяти на этом пути.
Самое быстрое решение - использовать
unsafe
вариант с кэшированием свойств. Маленькая настройка - использовать следующий код внутри unsafe
блока:byte* scan0 = (byte*)data.Scan0.ToPointer();
byte* scan = scan0;
var width = bmp.Width;
var height = bmp.Height;
for (var i = 0; i < width; i++)
{
for (var j = 0; j < height; j++)
scan += bpp;
}
Теперь код работает чуть больше 2 мс. Оказывается, что реальная оптимизация производительности была чем-то всегда лежащим перед нами - кэшированием доступа к свойствам C#, чтобы избежать (избыточных) дорогостоящих вычислений.
Комментарии
Отправить комментарий