Читая Хабр, я наткнулся на статью "Стоит ли сохранять длину массива в локальную переменную в 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 дней с момента публикации). Может кто-то заметит эту заметку и скопирует ее туда в виде комментария или вроде того.

Комментарии (12)


  1. AntonLozovskiy
    01.02.2019 21:03

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


    1. imanushin
      01.02.2019 22:13
      +2

      вызов метода/свойства всегда занимает больше тактов процессора, чем просто обращение к локальной переменной

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


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

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


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


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


  1. Nomad1
    01.02.2019 21:54
    +16

    0. Где IL код, где бенчмарки?
    1. При использовании multi-dimensional array JIT не может сам сделать подобную оптимизацию и вынести длину в внешнее число, потому как не знает, что результат GetLength не меняется. Если вдруг вы задумались о производительности, то используйте в этой ситуации jagged array (массив массивов), где гарантирована статическая длина каждого массива. Почитать об этом можно в огромном количестве мест.
    2. При всех своих плюсах jagged array состоит из разрозненных блоков памяти, а не хранится в одном участке. Для высокой скорости надо бы использовать одномерный массив размера [a*b]. Естественно, для адресации придется использовать умножение или увеличивать внутренний счетчик. Разница в скорости между multi-dimensional, jagged и single-dimensional настолько отличается, что мизерные отличия с выносом длины в внешнюю переменную стираются.
    3. Не экономьте на спичках! Забудьте то, что я сказал, забудьте эту и предыдущую статью. Пишите рабочий, красивый и документированный код, а не оптимизированный, нечитабельный и с глюками!


    1. sergeyZ
      02.02.2019 10:59
      +3

      Код бенчмарка 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;
          }
      }


  1. ARad
    02.02.2019 15:58
    +1

    Я не люблю задуматься над такими вещами, и просто всегда сохраняю в переменную, даже если это медленнее чем использовать массив, то разница будет очень маленькая, а вот выигрыш может быть значительным.
    Т.е. я предпочитаю не задумываясь чуть проиграть чем проиграть много.


    1. qw1
      03.02.2019 12:37

      Это +1 строка к размеру функции. Сложнее читать код, сложнее поддерживать.


      1. aikixd
        03.02.2019 13:52

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


        1. Nomad1
          03.02.2019 18:32
          +1

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


          1. aikixd
            04.02.2019 14:24

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


      1. a-tk
        03.02.2019 14:17

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

        А так уже и не такая большая разница.


        1. qw1
          03.02.2019 21:49
          +1

          Согласен, но у автора не так.

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


          1. a-tk
            03.02.2019 22:55

            Если такая конструкция создаёт большую когнитивную нагрузку на команду, то я даже не знаю…