Прямоугольные массивы .NET:как получить доступ в цикле?

StackOverflow https://stackoverflow.com/questions/405810

  •  03-07-2019
  •  | 
  •  

Вопрос

В принципе, у вас есть два способа сделать это:

for (int x = 0; x < UPPER_X; x++)
    for (int y = 0; y < UPPER_Y; y++)
    {        
        arr1[x, y] = get_value();
        arr2[y, x] = get_value();
    }

Единственная разница заключается в том, какую переменную изменить во внутреннем цикле:первый или второй.Я слышал, что результаты отличаются от языка к языку.

Каков правильный порядок для .NET?

Это было полезно?

Решение

Причина, по которой один работает быстрее другого, связана с кешем процессора и способом размещения данных в памяти.

Существует два обычных способа хранения двумерных данных в одномерном адресном пространстве, либо вы можете сохранить все данные для первой строки, затем для второй строки и так далее (иначе основной порядок строк), или вы можете сделать это по столбцам (иначе основной порядок столбцов).Ниже показано, какими будут ячейки памяти для массива 3x3 для обоих этих вариантов.

Строки:

1 2 3
4 5 6
7 8 9

Столбцы:

1 4 7
2 5 8
3 6 9

Когда вы обращаетесь к одной ячейке памяти, в кэш загружается целая строка кэша (которая, согласно Википедии, может составлять от 8 до 512 байт).Таким образом, если вы получите доступ к следующей ячейке памяти, она уже будет в кэше.Таким образом, последовательный доступ к памяти может быть намного быстрее, чем перемещение по адресному пространству.Таким образом, при работе с большими двумерными массивами может быть значительная разница в скорости выбора строк или столбцов в качестве переменной внутреннего цикла.

Другие советы

Чтобы быть уверенным, вам следует сопоставить свои конкретные обстоятельства.

Можно было бы подумать, что для прямоугольных массивов не будет никакой разницы (т. е.смежно выделенная память), НО в соответствии с этим Статья в MSDN в этом есть разница:

Вы можете получить еще лучшие результаты, преобразовав многомерный массив в одномерный массив.Если вы не возражаете против синтаксиса, это может быть тривиальным;просто используйте один индекс в качестве смещения.Например, следующее объявляет одномерный массив как используемый в качестве двумерного массива:

double[] myArray = new double[ROW_DIM * COLUMN_DIM];

Для индексации элементов этого массива используйте следующее смещение:

myArray[row * COLUMN_DIM + column];

Это, несомненно, будет быстрее, чем эквивалентный зубчатый или прямоугольный массив.

Итак, я провел тест, и результаты показали, что доступ к arr1 осуществляется в три раза быстрее.

Очень интересно, я никогда не переставал задумываться о том, что может быть такой огромной разницей, если просто обращаться к индексам массива "непоследовательно".Я попробовал это сам, а также обнаружил, что в следующем коде второй цикл занимает в 2-3 раза больше времени :

// Hmmm, how to insert blank lines in the code formatter???
static void Main(string[] args)
{
    Stopwatch timer = new Stopwatch();
    int arraySize = 10000;

    // First array, access X by Y
    int[,] xbyy = new int[arraySize, arraySize];


    timer.Start();
    for (int x = 0; x < arraySize; x++)
        for (int y = 0; y < arraySize; y++)
        {
            xbyy[x, y] = 15;
        }
    timer.Stop();
    TimeSpan duration = timer.Elapsed;
    string realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds",
                    duration.Minutes, duration.Seconds, duration.Milliseconds);
    Console.WriteLine("X by Y took " + realTimeFormat);



    // Seecond array, access Y by X
    int[,] ybyx = new int[arraySize, arraySize];

    timer.Start();
    for (int x = 0; x < arraySize; x++)
        for (int y = 0; y < arraySize; y++)
        {
            ybyx[y, x] = 15;
        }
    timer.Stop();
    duration = timer.Elapsed;
    realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds",
                duration.Minutes, duration.Seconds, duration.Milliseconds);
    Console.WriteLine("Y by X took " + realTimeFormat);


    Console.ReadLine();
}

Короче говоря, вот выданные фрагменты IL для цикла X by Y и цикла Y by X.

Начальный код перед циклированием,

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       290 (0x122)
  .maxstack  4
  .locals init ([0] class [System]System.Diagnostics.Stopwatch timer,
           [1] int32 arraySize,
           [2] int32[0...,0...] xbyy,
           [3] int32 x,
           [4] int32 y,
           [5] valuetype [mscorlib]System.TimeSpan duration,
           [6] string realTimeFormat,
           [7] int32[0...,0...] ybyx,
           [8] int32 V_8,
           [9] int32 V_9)
  IL_0000:  newobj     instance void [System]System.Diagnostics.Stopwatch::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldc.i4     0x2710
  IL_000b:  stloc.1

зацикливание X на Y

  IL_000c:  ldloc.1
  IL_000d:  ldloc.1
  IL_000e:  newobj     instance void int32[0...,0...]::.ctor(int32,
                                                             int32)
  IL_0013:  stloc.2
  IL_0014:  ldloc.0
  IL_0015:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
  IL_001a:  ldc.i4.0
  IL_001b:  stloc.3
  IL_001c:  br.s       IL_003d
  IL_001e:  ldc.i4.0
  IL_001f:  stloc.s    y
  IL_0021:  br.s       IL_0034
  IL_0023:  ldloc.2
  IL_0024:  ldloc.3
  IL_0025:  ldloc.s    y
  IL_0027:  ldc.i4.s   15
  IL_0029:  call       instance void int32[0...,0...]::Set(int32,
                                                           int32,
                                                           int32)
  IL_002e:  ldloc.s    y
  IL_0030:  ldc.i4.1
  IL_0031:  add
  IL_0032:  stloc.s    y
  IL_0034:  ldloc.s    y
  IL_0036:  ldloc.1
  IL_0037:  blt.s      IL_0023
  IL_0039:  ldloc.3
  IL_003a:  ldc.i4.1
  IL_003b:  add
  IL_003c:  stloc.3
  IL_003d:  ldloc.3
  IL_003e:  ldloc.1
  IL_003f:  blt.s      IL_001e
  IL_0041:  ldloc.0

зацикливание Y на X

  IL_0090:  ldloc.1
  IL_0091:  ldloc.1
  IL_0092:  newobj     instance void int32[0...,0...]::.ctor(int32,
                                                             int32)
  IL_0097:  stloc.s    ybyx
  IL_0099:  ldloc.0
  IL_009a:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
  IL_009f:  ldc.i4.0
  IL_00a0:  stloc.s    V_8
  IL_00a2:  br.s       IL_00c7
  IL_00a4:  ldc.i4.0
  IL_00a5:  stloc.s    V_9
  IL_00a7:  br.s       IL_00bc
  IL_00a9:  ldloc.s    ybyx
  IL_00ab:  ldloc.s    V_9
  IL_00ad:  ldloc.s    V_8
  IL_00af:  ldc.i4.s   15
  IL_00b1:  call       instance void int32[0...,0...]::Set(int32,
                                                           int32,
                                                           int32)
  IL_00b6:  ldloc.s    V_9
  IL_00b8:  ldc.i4.1
  IL_00b9:  add
  IL_00ba:  stloc.s    V_9
  IL_00bc:  ldloc.s    V_9
  IL_00be:  ldloc.1
  IL_00bf:  blt.s      IL_00a9
  IL_00c1:  ldloc.s    V_8
  IL_00c3:  ldc.i4.1
  IL_00c4:  add
  IL_00c5:  stloc.s    V_8
  IL_00c7:  ldloc.s    V_8
  IL_00c9:  ldloc.1
  IL_00ca:  blt.s      IL_00a4
  IL_00cc:  ldloc.0

Логический поток IL в чем-то похож.Основное отличие, которое я могу наблюдать, заключается в том, что в первом цикле удается использовать stloc и ldloc для позиций 2 и 3 для первого массива и индексной переменной X.К тому времени, когда он перешел ко второму циклу, он израсходовал maxstack и, таким образом, использовал инструкции stloc.s и ldloc.s .Я полагаю, что это разница между ссылками на переменные в стеке и в куче и способствует снижению производительности.

Теперь, если вы поменяете порядок, в котором тестируются циклы, так, чтобы цикл Y by X запускался первым для доступа к ссылкам на стек, вы увидите, что временные интервалы меняются на противоположные.


Обновить:Я был неправ, ссылаясь на адреса стека или кучи.Просто кажется, что на первые четыре переменные в методе более эффективно ссылаться с помощью выделенных кодов операций stloc.0, 1, 3, 4 и ldloc.0, 1, 3, 4.

http://weblogs.asp.net/mnolton/archive/2004/01/09/48992.aspx

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top