Когда стоит сохранять длину массива в локальную переменную в C#

Читая Хабр, я наткнулся на статью "Стоит ли сохранять длину массива в локальную переменную в C#?" (которая была в разделе «лучшее»). Мне кажется глупый вопрос, не совсем корректные измерения (почему нет измерений для вложенных циклов?) и странный вывод.

Длину массива в С# стоит сохранять в отдельную переменную в случае когда у нас несколько вложенных циклов, ниже пример.

Вот простой тестовый код без сохранения длины массива в переменную:

Random rnd1 = new Random(DateTime.UtcNow.Millisecond);
int[,] arr1 = new int[Int16.MaxValue, Byte.MaxValue];
for (int i = 0; i < arr1.GetLength(0); i++)
{
    for (int j = 0; j < arr1.GetLength(1); j++)
    {
        arr1[i, j] = rnd1.Next(Int32.MinValue, Int32.MaxValue);
    }
}

Вот тот же код c сохранением длины массива в переменную:

Random rnd1 = new Random(DateTime.UtcNow.Millisecond);
int[,] arr1 = new int[Int16.MaxValue, Byte.MaxValue];
int len1 = arr1.GetLength(0), len2 = arr1.GetLength(1);
for (int i = 0; i < len1; i++)
{
    for (int j = 0; j < len2; j++)
    {
        arr1[i, j] = rnd1.Next(Int32.MinValue, Int32.MaxValue);
    }
}

Код с сохранением длины массива в переменную (второй вариант) выполняется примерно на 15% быстрее.

Подобный ответ можно найти в более-менее толстых книжках по C# или .Net, но при этом умный человек постит это на Хабре и никто в комментариях не указал ему что длину массива в С# сохраняют в переменную обычно для вложенных циклов и там это действительно имеет смысл.

Я просто хотел оставить там комментарий, но без регистрации не смог, а после регистрации оказалось — что я и после регистрации не могу оставить там комментарий (так как прошло более 10 дней с момента публикации). Может кто-то заметит эту заметку и скопирует ее туда в виде комментария или вроде того.
Поделиться публикацией

Комментарии 15

    0
    Вообще все логично, вызов метода/свойства всегда занимает больше тактов процессора, чем просто обращение к локальной переменной, особенно, если методы/свойства виртуальные. а в случаях, когда они много раз вызываются, вообще даже говорить нечего
      +4
      вызов метода/свойства всегда занимает больше тактов процессора, чем просто обращение к локальной переменной

      Если нет inline и оптимизаций, заточенных на определенный код.


      если методы/свойства виртуальные

      Сейчас JIT'ы умеют делать девиртуализацию.


      Более того, есть даже вот такое поведение (в статье про Java).


      Поэтому есть главное правило: нельзя теоритезировать в вопросах производительности. Единственный ответ — это цифры бенчмарка. Всё остальное — красивые слова, не более.

      +18
      0. Где IL код, где бенчмарки?
      1. При использовании multi-dimensional array JIT не может сам сделать подобную оптимизацию и вынести длину в внешнее число, потому как не знает, что результат GetLength не меняется. Если вдруг вы задумались о производительности, то используйте в этой ситуации jagged array (массив массивов), где гарантирована статическая длина каждого массива. Почитать об этом можно в огромном количестве мест.
      2. При всех своих плюсах jagged array состоит из разрозненных блоков памяти, а не хранится в одном участке. Для высокой скорости надо бы использовать одномерный массив размера [a*b]. Естественно, для адресации придется использовать умножение или увеличивать внутренний счетчик. Разница в скорости между multi-dimensional, jagged и single-dimensional настолько отличается, что мизерные отличия с выносом длины в внешнюю переменную стираются.
      3. Не экономьте на спичках! Забудьте то, что я сказал, забудьте эту и предыдущую статью. Пишите рабочий, красивый и документированный код, а не оптимизированный, нечитабельный и с глюками!
        +4
        Код бенчмарка BenchmarkDotNet
        [RyuJitX64Job]
        public class ForLoopBench
        {
            private const int N = 1;
            private const int X = Int16.MaxValue;
            private const int Y = Byte.MaxValue;
        
            private Random rnd = new Random(DateTime.UtcNow.Millisecond);
        
            [Benchmark]
            public void TwoDimArray_ForLoop_NestedLoop()
            {
                int[,] arr = new int[X, Y];
                for (int i = 0; i < arr.GetLength(0); i++)
                {
                    for (int j = 0; j < arr.GetLength(1); j++)
                    {
                        arr[i, j] = i + j;
                    }
                }
            }
        
            [Benchmark]
            public void TwoDimArray_ForLoop_NestedLoop_LenVar()
            {
                int[,] arr = new int[X, Y];
                int len1 = arr.GetLength(0), len2 = arr.GetLength(1);
                for (int i = 0; i < len1; i++)
                {
                    for (int j = 0; j < len2; j++)
                    {
                        arr[i, j] = i + j;
                    }
                }
            }
        
            [Benchmark]
            public void TwoDimArray_ForLoop_Flat()
            {
                int[,] arr = new int[X, Y];
                for (int i = 0; i < arr.GetLength(0) * arr.GetLength(1); i++)
                {
                    arr[i % arr.GetLength(0), i % arr.GetLength(1)] = i;
                }
            }
        
            [Benchmark]
            public void TwoDimArray_ForLoop_Flat_LenVar()
            {
                int[,] arr = new int[X, Y];
                var len1 = arr.GetLength(0);
                var len2 = arr.GetLength(1);
                var len = len1 * len2;
                for (int i = 0; i < len; i++)
                {
                    arr[i % len1, i % len2] = i;
                }
            }
        
            [Benchmark]
            public int[] OneDimArray_ForLoop_Flat()
            {
                int[] arr = new int[X * Y];
        
                for (int i = 0; i < arr.Length; i++)
                {
                    arr[i] = i;
                }
                return arr;
            }
        
            [Benchmark]
            public int[] OneDimArray_ForLoop_NestedLoop()
            {
                int[] arr = new int[X * Y];
        
                for (int i = 0; i < arr.Length / X; i++)
                {
                    for (int j = 0; j < arr.Length / Y; j++)
                    {
                        arr[i + j * Y] = i + j * Y;
                    }
                }
                return arr;
            }
        
            [Benchmark]
            public int[] OneDimArray_ForLoop_NestedLoop_Rand()
            {
                int[] arr = new int[X * Y];
        
                for (int i = 0; i < arr.Length / X; i++)
                {
                    for (int j = 0; j < arr.Length / Y; j++)
                    {
                        arr[i + j * Y] = rnd.Next(Int32.MinValue, Int32.MaxValue);
                    }
                }
                return arr;
            }
        }

        Резултьтаты:


        | Method                                |      Mean |     Error |    StdDev |
        |-------------------------------------- |-----------|-----------|-----------|
        | TwoDimArray_ForLoop_NestedLoop        | 165.63 ms | 9.2170 ms | 27.176 ms |
        | TwoDimArray_ForLoop_NestedLoop_LenVar |  44.66 ms | 0.8797 ms |  1.233 ms |
        | TwoDimArray_ForLoop_Flat              | 512.00 ms | 9.9678 ms | 11.866 ms |
        | TwoDimArray_ForLoop_Flat_LenVar       | 164.14 ms | 3.2359 ms |  5.917 ms |
        | OneDimArray_ForLoop_Flat              |  29.80 ms | 1.4352 ms |  4.232 ms |
        | OneDimArray_ForLoop_NestedLoop        |  91.30 ms | 1.7957 ms |  2.688 ms |
        | OneDimArray_ForLoop_NestedLoop_Rand   | 335.05 ms | 6.7007 ms | 16.811 ms |

        Платформа
        BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.471 (1803/April2018Update/Redstone4)
        Intel Core i5-2430M CPU 2.40GHz (Sandy Bridge), 1 CPU, 4 logical and 2 physical cores
          [Host]    : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3260.0
          RyuJitX64 : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3260.0
        
        Job=RyuJitX64  Jit=RyuJit  Platform=X64  

        Выводы:


        1. Самое дорогое в коде автора — вызов rnd.Next() (80%)
        2. Nomad1 прав — одномерный массив выгоднее, и не нужно отдельной переменной для длины (30 ms против 45 ms).
        3. Вложенность циклов дешевле, чем многомерность массивов.
        0
        Я не люблю задуматься над такими вещами, и просто всегда сохраняю в переменную, даже если это медленнее чем использовать массив, то разница будет очень маленькая, а вот выигрыш может быть значительным.
        Т.е. я предпочитаю не задумываясь чуть проиграть чем проиграть много.
          +1
          Это +1 строка к размеру функции. Сложнее читать код, сложнее поддерживать.
            0

            Если вы используете for то значит производительность важнее чем читабельность. По той же причине, не согласен с автором предыдущего комментария. Если вы забрались в дебри где вам понадобился for, то стоит потратить время на бенчмарк.

              +1
              Коллега, наверное вы имеете в виду, что почти всегда можно использовать foreach. К сожалению, вы забываете, что записать в массив данные через foreach нельзя (кроме самых извращенных случаев). В этой ситуации for оказывается не дебрями, а самым читабельным методом для инициализации массивов, особенно многомерных.
                0

                Почему вы вообще работаете с массивами? Для каждой задачи нужно выбрать, а еще лучше реализовать твой тип коллекции, кторый нужен для задачи. Массивы очень гибкий и опасный элемент, на равне с указателями. Для любой повседневной задачи есть более идиоматичные и безопасные решения.

                  +1
                  Позволю себе с вами не согласиться. Видимо все дело в субъективном определении «повседневной задачи».
                  Массив это безопасная коллекция фиксированной длины, полностью реализующая IEnumerable и частично IList/ICollection (Insert, Remove, Add не доступны). Ее использование диктуется ее свойствами и массивы незаменимы при работе с изображениями, матрицами/тензорами, буферами и вообще любыми данными фиксированной длины и с рандомным доступом.
                  Небезопасность начинается при маршалинге, unsafe трюках (указатель на первый элемент), использовании Array.Copy и прочих методов. Но никто не заставляет вас этим пользоваться для повседневных задач. Конечно же, с массивами можно легко написать кривой код, если везде использовать их вместо списков и других коллекций, это плохой стиль и premature optimization. Но и для сравнения, System.String тоже позволяет и прямую адресацию, и указатели на отдельные символы, и реализует IEnumerable, и вообще это immutable тип и при каждой операции создает свои копии, но не стоит утверждать, что из-за этого надо его выкинуть и всегда пользоваться StringBuilder.
                    0

                    В моей повседневной работе есть массивы, unsafe, Array.Copy и прочие приятности. Но мне кажется это, все же, исключение. В основном люди работают с покупками, пользователями, постами и комментариями, чем с массивами структур или байтов.
                    Массивы проблематичны тем, что у них фиксированный размер и они изменяемы. В них нельзя настроить доступ к элементами. Скажем залочить запись, если пропал инет.
                    Если вы работаете с высокоуровневым кодом, то массив вам скорей всего не нужен. У вас будут коллекции, списки, queues (не уверен как это правильно перевести) и тд.

                      0

                      К тому же, даже в свой задаче, в которой есть нетривиальное количество небезопасного кода и серьезные требования к быстродействию, я все равно стараюсь не использовать массивы без крайней необходимости. Один неверный шаг и найти баг уже будет практически невозможно.

              +1
              for (int i = 0, n = array.Length; i < n; ++i)
              for (int i = 0; i < array.Length; ++i)

              А так уже и не такая большая разница.
                +1
                Согласен, но у автора не так.

                Плюс, это разрушает идеоматичный цикл «по i от 0 до X-1», с непривычки заставляет задумываться над «что тут происходит?», потребуется обучение команды этой конструкции.
                  0
                  Если такая конструкция создаёт большую когнитивную нагрузку на команду, то я даже не знаю…

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое