Блог переехал. Актуальная версия поста находится по адресу: http://aakinshin.net/ru/blog/dotnet/arrays-access-performance/.
Платформа .NET поддерживает два способа задания многомерных массивов: прямоугольные (rectangular) и изломанные (jagged). Второй способ по сути представляет собой массив массивов. Это обстоятельство создаёт у многих программистов иллюзию того, что jagged-массивы должны работать медленнее, т.к. обращение к их элементам реализуется через многократные переходы по ссылкам в управляемой куче. Но на самом деле jagged-массивы могут работают быстрее (если речь идёт непосредственно о работе с массивами, а не о их инициализации), ведь они представляют собой комбинацию одномерных (single) массивов, работа с которыми в CLR весьма оптимизирована (за счёт IL-команд newarr, ldelem, ldelema, ldlen, stelem). Другим подходом к представлению многомерных данных является использование одномерного массива с ручным преобразованием координат (в массиве размерности N*M для обращения к элементу [i,j] будем писать [i*M+j]). Если производительности не хватает, то можно использовать неуправляемый код, но этот случай мы сейчас рассматривать не будем, остановимся на трёх вышеозначенных способах. Для замеров времени используется BenchmarkDotNet. Рассмотрим C# код, который замеряет время работы каждого варианта (полный вариант кода: MultidimensionalArrayProgram.cs, тестировать следует в Release mode without debugging). Данные результаты получены в сборке под x64 для процессора Intel Core i7-3632QM CPU 2.20GHz и параметров N=M=100, IterationCount=100000. Исследование вопроса о влиянии используемой архитектуры и параметров запуска на результат бенчмарка можно найти во второй части статьи.
private const int N = 100, M = 100, IterationCount = 100000;
private int[] single;
private int[][] jagged;
private int[,] rectangular;
public void Run()
{
var competition = new BenchmarkCompetition();
competition.AddTask("Single",
() => single = new int[N * M],
() => SingleRun(single));
competition.AddTask("Jagged",
() =>
{
jagged = new int[N][];
for (int i = 0; i < N; i++)
jagged[i] = new int[M];
},
() => JaggedRun(jagged));
competition.AddTask("Rectangular",
() => rectangular = new int[N, M],
() => RectangularRun(rectangular));
competition.Run();
}
private int SingleRun(int[] a)
{
int sum = 0;
for (int iteration = 0; iteration < IterationCount; iteration++)
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += a[i * M + j];
return sum;
}
private int JaggedRun(int[][] a)
{
int sum = 0;
for (int iteration = 0; iteration < IterationCount; iteration++)
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += a[i][j];
return sum;
}
private int RectangularRun(int[,] a)
{
int sum = 0;
for (int iteration = 0; iteration < IterationCount; iteration++)
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += a[i, j];
return sum;
}
Этот пример на моём ноутбуке даёт следующие результаты (запускать следует в Release mode):
Single: 542ms Jagged: 346ms Rectangular: 755msКак можно видеть, доступ к элементам jagged-массива всегда осуществляется заметно быстрее, чем доступ к элементам rectangular-массива. Работа с single-массивом будет происходить быстрее, чем с rectangular, но чуть медленнее, чем с jagged. Мне думается, что single работает чуть медленнее jagged в большей степени из-за следующей причины:
- На расчёт индекса i*M+j требуется время, не дающее оптимизации в сравнении с лишним вызовом
ldelem.ref.
Чтобы лучше разобраться рассмотрим IL-код каждого из методов в release режиме (для упрощения чтения из каждого метода был убран цикл итераций по
iteration).single.il
.method private hidebysig instance
int32 SingleAccessTest(int32[] a) cil managed
{
.maxstack 4
.locals init (
[0] int32 sum,
[1] int32 i,
[2] int32 j)
L_0000: ldc.i4.0 // Stack = [0]
L_0001: stloc.0 // sum = 0, Stack = []
L_0002: ldc.i4.0 // Stack = [0]
L_0003: stloc.1 // i = 0, Stack = []
L_0004: br.s L_0022 // GoTo L_0022
CY1 L_0006: ldc.i4.0 // Stack = [0]
L_0007: stloc.2 // j = 0, Stack = []
L_0008: br.s L_0019 // GoTo L_0019
CY2 L_000a: ldloc.0 // Stack = [sum]
L_000b: ldarg.1 // Stack = [sum, a]
L_000c: ldloc.1 // Stack = [sum, a, i]
L_000d: ldc.i4.s 100 // Stack = [sum, a, i, 100]
L_000f: mul // Stack = [sum, a, i * 100]
L_0010: ldloc.2 // Stack = [sum, a, i * 100, j]
L_0011: add // Stack = [sum, a, i * 100 + j]
L_0012: ldelem.i4 // Stack = [sum, a[i * 100 + j]]
L_0013: add // Stack = [sum + a[i * 100 + j]]
L_0014: stloc.0 // sum = sum + a[i * 100 + j], Stack = []
L_0015: ldloc.2 // Stack = [j]
L_0016: ldc.i4.1 // Stack = [j, 1]
L_0017: add // Stack = [j + 1]
L_0018: stloc.2 // j = j + 1, Stack = []
CY2 L_0019: ldloc.2 // Stack = [j]
L_001a: ldc.i4.s 100 // Stack = [j, 100]
L_001c: blt.s L_000a // GoTo L_000a (if j < 100), Stack = []
L_001e: ldloc.1 // Stack = [i]
L_001f: ldc.i4.1 // Stack = [i, 1]
L_0020: add // Stack = [i + 1]
L_0021: stloc.1 // i = i + 1, Stack = []
CY1 L_0022: ldloc.1 // Stack = [i]
L_0023: ldc.i4.s 100 // Stack = [i, 100]
L_0025: blt.s L_0006 // GoTo L_0006 (if i < 100), Stack = []
L_0027: ldloc.0 // Stack = [sum]
L_0028: ret // return sum, Stack = []
}
jagged.il
.method private hidebysig instance
int32 JaggedAccessTest(int32[][] a) cil managed
{
.maxstack 3
.locals init (
[0] int32 sum,
[1] int32 i,
[2] int32 j)
L_0000: ldc.i4.0 // Stack = [0]
L_0001: stloc.0 // sum = 0, Stack = []
L_0002: ldc.i4.0 // Stack = [0]
L_0003: stloc.1 // i = 0, Stack = []
L_0004: br.s L_001f // GoTo L_001f
CY1 L_0006: ldc.i4.0 // Stack = [0]
L_0007: stloc.2 // j = 0, Stack = []
L_0008: br.s L_0016 // Stack = []
CY2 L_000a: ldloc.0 // Stack = [sum]
L_000b: ldarg.1 // Stack = [sum, a]
L_000c: ldloc.1 // Stack = [sum, a, i]
L_000d: ldelem.ref // Stack = [sum, a[i]]
L_000e: ldloc.2 // Stack = [sum, a[i], j]
L_000f: ldelem.i4 // Stack = [sum, a[i][j]]
L_0010: add // Stack = [sum + a[i][j]]
L_0011: stloc.0 // sum = sum + a[i][j], Stack = []
L_0012: ldloc.2 // Stack = [j]
L_0013: ldc.i4.1 // Stack = [j + 1]
L_0014: add // Stack = [j + 1]
L_0015: stloc.2 // j = j + 1, Stack = []
CY2 L_0016: ldloc.2 // Stack = [j]
L_0017: ldc.i4.s 100 // Stack = [j, 100]
L_0019: blt.s L_000a // GoTo L_000a (if j < 100)
L_001b: ldloc.1 // Stack = [i]
L_001c: ldc.i4.1 // Stack = [i, 1]
L_001d: add // Stack = [i + 1]
L_001e: stloc.1 // i = i + 1, Stack = []
CY1 L_001f: ldloc.1 // Stack = [i]
L_0020: ldc.i4.s 100 // Stack = [i, 100]
L_0022: blt.s L_0006 // GoTo L_0006 (if i < 100)
L_0024: ldloc.0 // Stack = [sum]
L_0025: ret // return sum, Stack = []
}
rectangular.il
.method private hidebysig instance
int32 RectangularAccessTest(int32[0...,0...] a) cil managed
{
.maxstack 4
.locals init (
[0] int32 sum,
[1] int32 i,
[2] int32 j)
L_0000: ldc.i4.0 // Stack = [0]
L_0001: stloc.0 // sum = 0, Stack = []
L_0002: ldc.i4.0 // Stack = [0]
L_0003: stloc.1 // i = 0, Stack = []
L_0004: br.s L_0022 // GoTo L_0022
CY1 L_0006: ldc.i4.0 // Stack = [0]
L_0007: stloc.2 // j = 0, Stack = []
L_0008: br.s L_0019 // GoTo L_0019
CY2 L_000a: ldloc.0 // Stack = [sum]
L_000b: ldarg.1 // Stack = [sum, a]
L_000c: ldloc.1 // Stack = [sum, a, i]
L_000d: ldloc.2 // Stack = [sum, a, i, j]
L_000e: call instance int32 int32[0...,0...]::Get(int32, int32)
// Stack = [sum, a[i, j]]
L_0013: add // Stack = [sum + a[i, j]]
L_0014: stloc.0 // sum = sum + a[i, j], Stack = []
L_0015: ldloc.2 // Stack = [j]
L_0016: ldc.i4.1 // Stack = [j, 1]
L_0017: add // Stack = [j + 1]
L_0018: stloc.2 // j = j +1, Stack = []
CY2 L_0019: ldloc.2 // Stack = [j]
L_001a: ldc.i4.s 100 // Stack = [j, 100]
L_001c: blt.s L_000a // GoTo L_000a (if j < 100), Stack = []
L_001e: ldloc.1 // Stack = [i]
L_001f: ldc.i4.1 // Stack = [i, 1]
L_0020: add // Stack = [i + 1]
L_0021: stloc.1 // i = i + 1, Stack = []
CY1 L_0022: ldloc.1 // Stack = [i]
L_0023: ldc.i4.s 100 // Stack = [i, 100]
L_0025: blt.s L_0006 // GoTo L_0006 (if i < 100), Stack = []
L_0027: ldloc.0 // Stack = [sum]
L_0028: ret // return sum, Stack = []
}
Вот, что думает Эрик :)
ОтветитьУдалитьhttp://blogs.msdn.com/b/ruericlippert/archive/2009/08/17/9894401.aspx
Статья Эрика хороша, но он пишет немного о другом, а именно, об использовании конструкции вида int[,][]. Я такого не касался, а лишь сравнил быстродействие массивов вида int[]; int[,]; int[][].
Удалить