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Проблема заключается в следующем: Что делать , если мы хотим выделить память из стека вызовов , который будет использоваться массивом элементарного (или неуправляемого) типа (например charintdouble, ...)? Прямо сейчас каждый массив естественно помещается в кучу, где он управляется сборщиком мусора.
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#, чтобы избежать (избыточных) дорогостоящих вычислений.

Комментарии

Популярные сообщения из этого блога

Аппроксимация функций рядом Тейлора

Алгоритмы: Алгоритм Флойда-Уоршелла

Основные структуры данных: Множества