Pull to refresh

Comments 16

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

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


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

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


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


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

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

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


| 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. Вложенность циклов дешевле, чем многомерность массивов.
Я не люблю задуматься над такими вещами, и просто всегда сохраняю в переменную, даже если это медленнее чем использовать массив, то разница будет очень маленькая, а вот выигрыш может быть значительным.
Т.е. я предпочитаю не задумываясь чуть проиграть чем проиграть много.
Это +1 строка к размеру функции. Сложнее читать код, сложнее поддерживать.

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

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

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

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

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

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

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

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

Плюс, это разрушает идеоматичный цикл «по i от 0 до X-1», с непривычки заставляет задумываться над «что тут происходит?», потребуется обучение команды этой конструкции.
Если такая конструкция создаёт большую когнитивную нагрузку на команду, то я даже не знаю…
Я бы уточнил, что тут речь идет не столько о вложенных циклах, сколько о многомерных массивах.
Иначе, при вложенных циклах, но по одномерным массивам в вызове относительно медленного .GetLength(0/1) нет никакого смысла, вполне достаточно arr1.Length и arr2.Length
Sign up to leave a comment.

Articles