Внутренняя реализация массивов
В CLR поддерживаются массивы двух типов:
Одномерные массивы с нулевым начальным индексом. Иногда их называют
SZ-массивами
(от английского single-dimensional, zero-based), или
векторами
.
Одномерные и многомерные массивы с неизвестным начальным индексом.
Рассмотрим их на примере следующего кода (результат его выполнения при-
водится в комментариях):
using System;
public sealed class Program {
public static void Main() {
Array a;
// Создание одномерного массива с нулевым
// начальным индексом и без элементов
продолжение
428
Глава.16 .Массивы
a = new String[0];
Console.WriteLine(a.GetType()); // "System.String[]"
// Создание одномерного массива с нулевым
// начальным индексом и без элементов
a = Array.CreateInstance(typeof(String),
new Int32[] { 0 }, new Int32[] { 0 });
Console.WriteLine(a.GetType()); // "System.String[]"
// Создание одномерного массива с начальным индексом 1 и без элементов
a = Array.CreateInstance(typeof(String),
new Int32[] { 0 }, new Int32[] { 1 });
Console.WriteLine(a.GetType()); // "System.String[*]" <-- ВНИМАНИЕ!
Console.WriteLine();
// Создание двухмерного массива с нулевым
// начальным индексом и без элементов
a = new String[0, 0];
Console.WriteLine(a.GetType()); // "System.String[,]"
// Создание двухмерного массива с нулевым
// начальным индексом и без элементов
a = Array.CreateInstance(typeof(String),
new Int32[] { 0, 0 }, new Int32[] { 0, 0 });
Console.WriteLine(a.GetType()); // "System.String[,]"
// Создание двухмерного массива с начальным индексом 1 и без элементов
a = Array.CreateInstance(typeof(String),
new Int32[] { 0, 0 }, new Int32[] { 1, 1 });
Console.WriteLine(a.GetType()); // "System.String[,]"
}
}
Рядом с каждой инструкцией
Console.WriteLine
в виде комментария пока-
зан результат действия. Для одномерных массивов с нулевой нижней границей
это
System.String[]
, если же индексация начинается с единицы, выводится уже
System.String[*]
. Знак
*
свидетельствует о том, что CLR знает о ненулевой
нижней границе. Так как в C# объявить переменную типа
String[*]
невозможно,
синтаксис этого языка запрещает обращение к одномерным массивам с ненулевой
нижней границей. Впрочем, обойти это ограничение можно с помощью методов
GetValue
и
SetValue
класса
Array
, но дополнительные затраты на вызов метода
снижают эффективность работы программы.
Для многомерных массивов, независимо от нижней границы, отображается один
и тот же тип:
System.String[,]
. Во время выполнения CLR рассматривает их как
массивы с ненулевой нижней границей. Логично было бы предположить, что имя
типа будет представлено как
System.String[*,*]
, но в CLR для многомерных
массивов не используется знак
*
. Ведь иначе он выводился бы во всех случаях,
создавая путаницу.
429
Внутренняя.реализация.массивов
Доступ к элементам одномерного массива с нулевой нижней границей осу-
ществляется немного быстрее, чем доступ к элементам многомерных массивов
или массивов с ненулевой нижней границей. Есть несколько причин такому по-
ведению. Во-первых, специальные команды для работы с одномерными массивами
с нулевой нижней границей (
newarr
,
ldelem
,
ldelema
,
ldlen
и
stelem
) позволяют
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
. Фактически при этом вызывается метод, но JIT-компилятор «знает», что
Length
является свойством класса
Array
, поэтому создает код, в котором метод вызывается
всего один раз, а полученный результат сохраняется в промежуточной переменной.
Именно ее значение проверяется на каждой итерации цикла. В результате такой
код работает очень быстро. Некоторые разработчики недооценивают возможности
JIT-компилятора и пишут «умный код», пытаясь помочь его работе. Однако такие
попытки практически всегда приводят к снижению производительности, а также
делают готовую программу непонятной и неудобной для редактирования. Поэтому
пусть свойство
Length
вызывается автоматически.
Кроме того, JIT-компилятор «знает», что цикл обращается к элементам массива
с нулевой нижней границей, указывая
Length
-
1
. Поэтому он в процессе выполне-
ния генерирует код, проверяющий, все ли элементы находятся в границах массива.
А именно, проверяется условие:
(0 >= a.GetLowerBound(0)) && ((Length – 1) <= a.GetUpperBound(0))
Проверка осуществляется до начала цикла. В случае положительного резуль-
тата компилятор не создает в теле цикла кода, проверяющего, не вышел ли индекс
элемента за границы диапазона. Именно за счет этого обеспечивается высокая
производительность доступа к массиву.
К сожалению, обращение к элементам многомерного массива или массива с не-
нулевой нижней границей происходит намного медленней. Ведь в этих случаях код
проверки индекса не выносится за пределы цикла и проверка осуществляется на
каждой итерации. Кроме того, компилятор добавляет код, вычитающий из текущего
430
Глава.16 .Массивы
индекса нижнюю границу массива. Это также замедляет работу программы даже
в случае многомерных массивов с нулевой нижней границей. Если вы серьезно оза-
бочены проблемой производительности, имеет смысл использовать нерегулярные
массивы (массивы массивов).
Кроме того, в C# и CLR возможен доступ к элементам массива при помощи
небезопасного (неверифицируемого) кода. В этом случае процедура проверки
индексов массива просто отключается. Данная техника применима только к мас-
сивам типа
SByte
,
Byte
,
Int16
,
UInt16
,
Int32
,
UInt32
,
Int64
,
UInt64
,
Char
,
Single
,
Double
,
Decimal
,
Boolean
, а также к массивам перечислимого типа или структуры
значимого типа с полями одного из вышеуказанных типов.
Эту мощную возможность следует использовать крайне осторожно, так как
она дает прямой доступ к памяти. При этом выход за границы массива не сопрово-
ждается появлением исключения; вместо этого происходит повреждение памяти,
нарушение безопасности типов и, скорее всего, в системы безопасности программы
появляется дефект. Поэтому сборке, содержащей небезопасный код, следует обе-
спечить полное доверие или же предоставить разрешение
Security
Permission
,
включив свойство
Skip
Verification
.
Следующий код демонстрирует три варианта доступа к двухмерному массиву,
включая безопасный доступ, доступ через нерегулярный массив и небезопасный
доступ:
using System;
using System.Diagnostics;
public static class Program {
private const Int32 c_numElements = 10000;
public static void Main() {
// Объявление двухмерного массива
Int32[,] a2Dim = new Int32[c_numElements, c_numElements];
// Объявление нерегулярного двухмерного массива (вектор векторов)
Int32[][] aJagged = new Int32[c_numElements][];
for (Int32 x = 0; x < c_numElements; x++)
aJagged[x] = new Int32[c_numElements];
// 1: Обращение к элементам стандартным, безопасным способом
Safe2DimArrayAccess(a2Dim);
// 2: Обращение к элементам с использованием нерегулярного массива
SafeJaggedArrayAccess(aJagged);
// 3: Обращение к элементам небезопасным методом
Unsafe2DimArrayAccess(a2Dim);
}
private static Int32 Safe2DimArrayAccess(Int32[,] a) {
431
Внутренняя.реализация.массивов
Int32 sum = 0;
for (Int32 x = 0; x < c_numElements; x++) {
for (Int32 y = 0; y < c_numElements; y++) {
sum += a[x, y];
}
}
return sum;
}
private static Int32 SafeJaggedArrayAccess(Int32[][] a) {
Int32 sum = 0;
for (Int32 x = 0; x < c_numElements; x++) {
for (Int32 y = 0; y < c_numElements; y++) {
sum += a[x][y];
}
}
return sum;
}
private static unsafe Int32 Unsafe2DimArrayAccess(Int32[,] a) {
Int32 sum = 0;
fixed (Int32* pi = a) {
for (Int32 x = 0; x < c_numElements; x++) {
Int32 baseOfDim = x * c_numElements;
for (Int32 y = 0; y < c_numElements; y++) {
sum += pi[baseOfDim + y];
}
}
}
return sum;
}
}
Метод
Unsafe2DimArrayAccess
имеет модификатор
unsafe
, который необходим
для инструкции
fixed
языка C#. При вызове компилятора следует установить
переключатель
/unsafe
или флажок
Allow.Unsafe.Code
на вкладке
Build
окна свойств
проекта в программе Microsoft Visual Studio.
Существуют ситуации, в которых «небезопасный» доступ оказывается опти-
мальным, но у него есть три серьезных недостатка:
код обращения к элементам массива менее читабелен и более сложен в написании
из-за присутствия инструкции
fixed
и вычисления адресов памяти;
ошибка в расчетах может привести к перезаписи памяти, не принадлежащей
массиву, в результате возможны разрушение памяти, нарушение безопасности
типов и потенциальные бреши в системе безопасности;
из-за высокой вероятности проблем CLR запрещает работу небезопасного
кода в средах с пониженным уровнем безопасности (таких, как Microsoft
Silverlight).
Do'stlaringiz bilan baham: |