
- •Сборки (assembly) в среде .Net. Проблема версионности сборок и ее решение.
- •Номер версии в .Net
- •Сведения о версии
- •Номер версии сборки
- •Информационная версия сборки
- •Общая система типов данных в среде .Net. Размерные и ссылочные типы данных. Типы, переменные и значения
- •Пользовательские типы
- •Система общих типов cts
- •Ссылочные типы
- •Типы литеральных значений
- •Неявные типы, анонимные типы и типы, допускающие значение null
- •Упаковка и распаковка размерных типов данных в среде .Net.
- •Производительность
- •Упаковка–преобразование
- •Распаковка-преобразование
- •Ссылочные типы данных. Объектная модель в среде .Net и языке c#.
- •Модели ручной и автоматической утилизации динамической памяти, их сравнительная характеристика. Модель с ручным освобождением памяти
- •Модель с автоматической «сборкой мусора»
- •Модель автоматической утилизации динамической памяти, основанная на сборке мусора. Проблема недетерминизма.
- •Модель автоматической утилизации динамической памяти, основанная на аппаратной поддержке (тегированной памяти).
- •Сборка мусора в среде .Net. Построение графа достижимых объектов.
- •Сборка мусора в среде .Net. Механизм поколений объектов.
- •Модель детерминированного освобождения ресурсов в среде .Net. Интерфейс iDisposable и его совместное использование с завершителем (методом Finalize).
- •«Мягкие ссылки» и кэширование данных в среде .Net.
- •Краткие и длинные слабые ссылки
- •Краткая ссылка
- •Длинная ссылка
- •Правила использования слабых ссылок
- •Динамические массивы в среде .Net и языке c#.
- •Приведение типов в массивах
- •Все массивы неявно реализуют /Enumerable, /Collection и iList
- •Передача и возврат массивов
- •Создание массивов с ненулевой нижней границей
- •Производительность доступа к массиву
- •Небезопасный доступ к массивам и массивы фиксированного размера
- •Делегаты в среде .Net и механизм их работы. Знакомство с делегатами
- •Использование делегатов для обратного вызова статических методов
- •Использование делегатов для обратного вызова экземплярных методов
- •Правда о делегатах
- •Использование делегатов для обратного вызова множественных методов (цепочки делегатов)
- •Поддержка цепочек делегатов в с#
- •Расширенное управление цепочкой делегатов
- •Упрощение синтаксиса работы с делегатами в с#
- •Упрощенный синтаксис № 1: не нужно создавать объект-делегат
- •Упрощенный синтаксис № 2: не нужно определять метод обратного вызова
- •Упрощенный синтаксис № 3: не нужно определять параметры метода обратного вызова
- •Упрощенный синтаксис № 4: не нужно вручную создавать обертку локальных переменных класса для передачи их в метод обратного вызова
- •Делегаты и отражение
- •События в среде .Net; реализация событий посредством делегатов. События
- •Этап 1: определение типа, который будет хранить всю дополнительную информацию, передаваемую получателям уведомления о событии
- •Этап 2: определение члена-события
- •Этап 3: определение метода, ответственного за уведомление зарегистрированных объектов о событии
- •Этап 4: определение метода, транслирующего входную информацию в желаемое событие
- •Как реализуются события
- •Создание типа, отслеживающего событие
- •События и безопасность потоков
- •Явное управление регистрацией событий
- •Конструирование типа с множеством событий
- •Исключительные ситуации и реакция на них в среде .Net. Достоинства
- •Механика обработки исключений
- •Блок try
- •Блок catch
- •Блок finally
- •Генерация исключений
- •Определение собственных классов исключений
- •Исключения в платформе .Net Framework
- •Исключения и традиционные методы обработки ошибок
- •Управление исключениями средой выполнения
- •Фильтрация исключений среды выполнения
- •21 Средства многопоточного программирования в среде .Net. Автономные потоки. Пул потоков.
- •Создание и использование потоков
- •Запуск и остановка потоков
- •Методы управления потоками
- •Безопасные точки
- •Свойства потока
- •Потоки Windows в clr
- •К вопросу об эффективном использовании потоков
- •Пул потоков в clr
- •Ограничение числа потоков в пуле
- •22. Асинхронные операции в среде .Net. Асинхронный вызов делегатов.
- •23. Синхронизация программных потоков в среде .Net. Блокировки.
- •Двойная блокировка
- •Класс ReaderWriterLock
- •Использование объектов ядра Windows в управляемом коде
- •Вызов метода при освобождении одного объекта ядра
- •24. Синхронизация программных потоков в среде .Net. Атомарные (Interlocked-операции). Семейство lnterlocked-методов
- •25. Прерывание программных потоков в среде .Net. Особенности исключительной ситуации класса ThreadAbortException.
- •26. Мониторы в среде .Net. Ожидание выполнения условий с помощью методов Wait и Pulse. Класс Monitor и блоки синхронизации
- •«Отличная» идея
- •Реализация «отличной» идеи
- •Использование класса Monitor для управления блоком синхронизации
- •Способ синхронизации, предлагаемый Microsoft
- •Упрощение кода c# при помощи оператора lock
- •Способ синхронизации статических членов, предлагаемый Microsoft
- •Почему же «отличная» идея оказалась такой неудачной
- •Целостность памяти, временный доступ к памяти и volatile-поля
- •Временная запись и чтение
- •Поддержка volatile-полей в с#
- •27. Асинхронный вызов делегатов.
- •Общие типы (Generics)
- •Инфраструктура обобщений
- •Открытые и закрытые типы
- •Обобщенные типы и наследование
- •Проблемы с идентификацией и тождеством обобщенных типов
- •«Распухание» кода
- •Обобщенные интерфейсы
- •Обобщенные делегаты
- •Обобщенные методы
- •Логический вывод обобщенных методов и типов
- •Обобщения и другие члены
- •Верификация и ограничения
- •Основные ограничения
- •Дополнительные ограничения
- •Ограничения конструктора
- •Другие вопросы верификации
- •Приведение переменной обобщенного типа
- •Присвоение переменной обобщенного типа значения по умолчанию
- •Сравнение переменной обобщенного типа с null
- •Сравнение двух переменных обобщенного типа
- •Использование переменных обобщенного типа в качестве операндов
- •Преимущества использования общих типов
- •29. Итераторы в среде .Net. Создание и использование итераторов.
- •Общие сведения о итераторах
Производительность доступа к массиву
Внутренние механизмы CLR поддерживают два типа массивов:
одномерные массивы с нулевым начальным индексом. Их иногда называют SZ-масивами (от английских слов single-dimensional, zero-based) или векторами;
одномерные и многомерные массивы с неизвестным начальным индексом.
Познакомиться с разными видами массивов можно, выполнив следующий код (результат работы приводится в комментариях):
using System;
public sealed class Program
{
public static void Main()
{
Array a;
// Создаем одномерный массив с нулевым начальным индексом и без элементов,
а = new String[0];
Console.WriteLine(a.GetType()); // "System.String[]"
// Создаем одномерный массив с нулевым начальным индексом и без элементов,
а = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 0 });
Console.WriteLine(a.GetTypeO); // "System.String[]"
// Создаем одномерный массив с нулевым начальным индексом и без элементов,
а = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 1 });
Console.WriteLine(a.GetType()); // "System. String[*]" <- ОБРАТИТЕ ВНИМАНИЕ!
Console.WriteLine();
// Создаем двухмерный массив с нулевым начальным индексом и без элементов,
а = new String[0, 0];
Console.WriteLine(a.GetТуре()); // "System.String[,]"
// Создаем двухмерный массив с нулевым начальным индексом и без элементов,
а = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 0, 0 });
Console.WriteLine(a.GetType()); // "System.String[,]"
// Создаем двухмерный массив с начальным индексом "1" и без элементов,
а = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 1, 1 });
Console.WriteLine(a.GetTypeO); // "System.String[,]"
}
}
Рядом с каждым оператором ConsoleWriteLine приводится комментарий с результатом работы. В одномерных массивах с нулевой нижней границей выводится «System.String[]», а для одномерных с нижней границей «1» — «System.String[*]». Звездочка означает, что CLR «в курсе», что это массив с ненулевой границей.
Заметьте: С# не позволяет определить переменную типа String[*], поэтому, используя синтаксис С#, нельзя обратиться к одномерным массивам с ненулевой нижней границей. Вы вправе для этого воспользоваться методами GetValue и SetValue типа Array, но это намного снижает эффективность из-за дополнительных затрат на вызов метода.
Независимо от нижней границы — «0» или «1» — для многомерных массивов отображается одинаковый тип «System.String[,]». Во время выполнения CLR трактует многомерные массивы так же, как и массивы с ненулевой нижней границей. Можно было бы подумать, что имя типа должно выглядеть так: System.String[*,*], однако CLR не использует звездочки по отношению к многомерным массивам, потому что постоянное наличие звездочек приводило бы в замешательство большинство разработчиков.
Доступ к элементам одномерных массивов с нулевой нижней границей выполняется намного быстрее, чем массивов с ненулевой нижней границей или многомерных массивов. На то есть несколько причин. Во-первых, есть специальные IL-команды (такие как newarr, Idelem, Idelema, Idlen и stelem) для работы с одномерными массивам с нулевой нижней границей, которые позволяют JIT-компилятору генерировать оптимизированный машинный код. В частности, JIT-компилятор сгенерирует код, предполагающий, что речь идет о массиве с нулевой нижней границей, а это означает, что при доступе к элементу не нужно отнимать смещение. Во-вторых, в стандартных ситуациях JIT-компилятор «умеет» вынести из цикла лишний код проверки границ, выполняя его лишь раз, до выполнения цикла. Вот пример:
using System;
public static class Program
{
public static void Main()
{
Int32[] a = new Int32[5];
for(Int32 index = 0; index < a.Length; index++)
{
// Выполняем какие-то операции с a[index].
}
}
}
Первое, на что надо обратить внимание в этом коде, — вызов свойства Length массива в проверочном операторе цикла for. Так как Length — свойство, то получение размера фактически является вызовом метода. Однако JIT-компилятор «знает», что Length — свойство класса Array, поэтому генерирует код, который вызывает свойство только раз и сохраняет результат во временной переменной, которая проверяется в каждой итерации цикла. Результат — созданный компилятором код выполняется очень «быстро». Честно говоря, некоторые разработчики недооценивают «сообразительность» JIT-компилятора и в попытке помочь компилятору пишут «хитрый код». Однако любые ухищрения практически наверняка негативно сказываются на производительности, а сам код становится менее читабельным, да и поддерживать его сложнее. Лучше положиться на свойство Length и не заниматься самодеятельностью, кешируя его в локальной переменной.
Второе, на что стоит обратить внимание, — JIT-компилятор знает, что цикл обращается к элементам массива с нулевой нижней границей, указывая Length - 1, поэтому генерирует код, который во время выполнения при каждом доступе к массиву проверяет границы массива. В частности, JIT-компилятор создает код, проверяющий следующее условие:
(( Length - 1) <= a.GetUpperBound(0))
Проверка выполняется непосредственно перед циклом. В случае положительного результата JIT-компилятор не генерирует в цикле код, каждый раз при доступе к массиву проверяющий, не вышел ли индекс за пределы разрешенного диапазона. Это сильно ускоряет доступ к массиву в цикле.
К сожалению, как уже говорилось, обращение к элементам массива с ненулевой нижней границей или многомерного массива выполняется намного медленнее. Здесь JIT-компилятор не выносит код проверки границ за пределы цикла, так что при каждом доступе к массиву проверяется правильность указания индекса. Кроме того, JIT-компилятор добавляет код, вычитающий нижнюю границу массива из текущего индекса, что также замедляет работу программы, даже для многомерных массивов с нулевой нижней границей.
Так что, если вас серьезно волнует проблема производительности, можно подумать об использовании вложенных (jagged) массивов.
С# и CLR также позволяют обращаться к массиву, используя небезопасный (неверифицируемый) код, в сущности, как метод отключения проверки границ индекса при доступе к массиву.
Заметьте: метод работы с массивом без проверки индексов применим только к массивам, где элементами являются SByte, Byte, Intl6, Uint 16, Int32, UInt32, Int64, UInt64, Char, Single, Double, Decimal, Boolean, перечислимый тип или структура значимого типа, полями которой являются любые из перечисленных выше типов.
Это очень мощный метод, который следует использовать крайне осторожно, потому что при этом предоставляется прямой доступ к памяти. Если при доступе к памяти выйти за границы массива, исключения не возникнет — вы «всего-навсего» повредите память, нарушите безопасность типов и, вполне вероятно, откроете зияющую брешь в защите программы! Поэтому сборке, содержащей небезопасный код, нужно либо обеспечить полное доверие, либо как минимум предоставить разрешение SecurityPermission с установленным свойством Skip Verification.
Следующий код С# демонстрирует три метода (безопасный доступ, использование вложенного массива и опасный доступ) доступа к двумерному массиву:
using System;
using System.Diagnostics;
public static class Program
{
public static void Main()
{
const Int32 numElements = 100;
const Int32 testCount = 10000;
Stopwatch sw;
// Объявляем двумерный массив.
Int32[,] a2Dim = new Int32[numElements, numElements];
// Объявляем двумерный массив как вложенный (вектор векторов).
Int32[][] aJagged = new Int32[numElements][];
for (Int32 x = 0; x < numElements; x++)
aJagged[x] = new Int32[numElements];
// 1: обращаемся ко всем элементам массива стандартным, безопасным методом.
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
Safe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}; Safe2DimArrayAccess", sw.Elapsed);
// 2: обращаемся ко всем элементам с использованием метода
// вложенных массивов.
sw = Stopwatch.StartNewQ;
for (Int32 test = 0; test < testCount; test++)
SafeJaggedArrayAccess(aJagged);
Console.WriteLine("{0}; SafeJaggedArrayAccess", sw.Elapsed);
// 3: обращаемся ко всем элементам с использованием небезопасного метода.
sw = Stopwatch.StartNewQ;
for (Int32 test = 0; test < testCount; test++)
Unsafe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}: Unsafe2DimArrayAccess", sw.Elapsed);
}
private static void Safe2DimArrayAccess(Int32[,] a)
{
for (Int32 x = 0; x < a.GetLength(O); x++)
{
for (Int32 у = 0; у < a.GetLength; y++)
{
Int32 element = a[x, y];
}
}
}
private static void SafeJaggedArrayAccess(Int32[][] a)
{
for (Int32 x = 0; x < a.GetLength(O); x++)
{
for (Int32 у = 0; у < a[x].GetLength(0); y++)
{
Int32 element = a[x][y];
}
}
}
private static unsafe void Unsafe2DimArrayAccess(Int32[,] a)
{
Int32 dimOLowIndex = 0; // a.GetLowerBound(O);
Int32 dimOHighlndex = a.GetUpperBound(0);
Int32 dimUowIndex = 0; // a.GetLowerBound(l);
Int32 dimlHighlndex = a.GetUpperBound(l);
Int32 dimOElements = dimOHighlndex - dimOLowIndex;
fixed (Int32* pi = &a[0, 0])
{
for (Int32 x = dimOLowIndex; x <= dimOHighlndex; x++)
{
Int32 baseOfDim = x * dimOElements;
for (Int32 у = dimUowIndex; у <= dimlHighlndex; y++)
{
Int32 element = pi[baseOfDim + y];
}
}
}
}
}
Метод Unsafe2DimArrayAccessотмечен модификаторомunsafe, который необходим для оператораfixedязыка С#. При компиляции этого кода нужно задать параметр /unsafeили установить флажокAllowUnsafeCodeна вкладкеBuildокна свойств проекта вMicrosoftVisualStudio.
Выполнив программу на своей машине я получил:
00:00:02.7977902: Safe2DimArrayAccess
00:00:02.2690798: SafeJaggedArrayAccess
00:00:00.2265131: Unsafe2DimArrayAccess
Как видите, безопасный метод самый медленный. Доступ к вложенным массивам занимает чуть меньше времени, однако следует знать, что на создание вложенного массива уходит больше времени, чем на создание многомерного массива из-за того, что при создании вложенного массива в куче создается отдельный объект для каждого измерения, что требует регулярного внимания со стороны сборщика мусора. Придется выбирать из двух вариантов: если нужно создавать много «многомерных массивов» и обращаться к их элементам нечасто, эффективнее создать многомерный массив. Если «многомерный массив» создается лишь раз, а затем выполняется многократное обращение к его элементам, вложенный массив обеспечит лучшую производительность. Ясно, что вторая ситуация встречается чаще всего.
Метод доступа к массиву с использованием небезопасного кода обеспечивает большую скорость — время доступа почти на порядок меньше, чем в случае с вложенными массивами, — при этом работа ведется с реальным двумерным массивом, что значительно экономит ресурсы по сравнению с созданием вложенного массива. «Небезопасный» метод выигрывает вчистую, но у него есть три серьезных недостатка:
код обращения к элементам массива сложнее для чтения и записи, чем в других методах из-за использования оператора fixed языка С# и вычисления адресов памяти;
в случае ошибки в расчетах адреса памяти возможна перезапись памяти, не принадлежащей массиву. Это может привести к неправильным вычислительным операциям, разрушению памяти, нарушению безопасности типов и возникновению бреши в защите;
из-за высокой вероятности проблем CLR разрешает небезопасному коду работать только при наличии разрешений со стороны администратора или конечного пользователя. По умолчанию такое разрешение предоставляется всем сборкам, установленным на локальной машине. А вот сборке, загружаемой из интрасети или Интернета, такое разрешение не предоставляется, и при попытке загрузки такой сборки с небезопасным кодом CLR генерирует исключение. Такое поведение по умолчанию можно изменить посредством инструмента CASPol.exe, который поставляется в составе .NET Framework.