Микробенчмарк C #: почему сброс значения агрегации ускоряет циклы for? - PullRequest
0 голосов
/ 22 ноября 2018

Рассмотрим следующие две разные функции ComputeA и ComnputeB:

using System;
using System.Diagnostics;

namespace BenchmarkLoop
{
    class Program
    {
        private static double[] _dataRow;
        private static double[] _dataCol;

        public static double ComputeA(double[] col, double[] row)
        {
            var rIdx = 0;
            var value = 0.0;

            for (var i = 0; i < col.Length; ++i)
            {
                for (var cIdx = 0; cIdx < col.Length; ++cIdx, ++rIdx)
                    value += col[cIdx] * row[rIdx];
            }

            return value;
        }

        public static double ComputeB(double[] col, double[] row)
        {
            var rIdx = 0;
            var value = 0.0;

            for (var i = 0; i < col.Length; ++i)
            {
                value = 0.0;
                for (var cIdx = 0; cIdx < col.Length; ++cIdx, ++rIdx)
                    value += col[cIdx] * row[rIdx];
            }

            return value;
        }

        public static double ComputeC(double[] col, double[] row)
        {
            var rIdx = 0;
            var value = 0.0;

            for (var i = 0; i < col.Length; ++i)
            {
                var tmp = 0.0;
                for (var cIdx = 0; cIdx < col.Length; ++cIdx, ++rIdx)
                    tmp += col[cIdx] * row[rIdx];
                value += tmp;
            }

            return value;
        }

        static void Main(string[] args)
        {
            _dataRow = new double[2500];
            _dataCol = new double[50];

            var random = new Random();
            for (var i = 0; i < _dataRow.Length; i++)            
                _dataRow[i] = random.NextDouble();
            for (var i = 0; i < _dataCol.Length; i++)
                _dataCol[i] = random.NextDouble();

            var nRuns = 1000000;

            var stopwatch = new Stopwatch();
            stopwatch.Start();
            for (var i = 0; i < nRuns; i++)
                ComputeA(_dataCol, _dataRow);
            stopwatch.Stop();
            var t0 = stopwatch.ElapsedMilliseconds;

            stopwatch.Reset();
            stopwatch.Start();
            for (int i = 0; i < nRuns; i++)
                ComputeC(_dataCol, _dataRow);
            stopwatch.Stop();
            var t1 = stopwatch.ElapsedMilliseconds;

            Console.WriteLine($"Time ComputeA: {t0} - Time ComputeC: {t1}");
            Console.ReadKey();
        }
    }
}

Они отличаются только «сбросом» значения переменной перед каждым вызовом внутреннего цикла.Я запустил несколько различных типов тестов, все с включенным «Оптимизированным кодом», с 32-битным и 64-битным и разным размером массивов данных.Всегда ComputeB примерно на 25% быстрее.Я могу воспроизвести эти результаты также с BenchmarkDotNet.Но я не могу их объяснить.Любая идея?Я также проверил полученный ассемблерный код с Intel VTune Amplifier 2019: для обеих функций результат JIT одинаков, плюс дополнительная строка для сброса value: JIT result Итак, на уровне ассемблера естьне происходит никакого волшебства, которое может сделать код быстрее.Есть ли другое возможное объяснение этого эффекта?И как это проверить?

А вот результаты с BenchmarkDotNet (параметр N имеет размер _dataCol, _dataRow всегда имеет размер N ^ 2): BenchmarkDotNet results

И результаты сравнения ComputeA и ComputeC: enter image description here

JIT-сборка для ComputeA (слева) и ComputeC (справа): enter image description here

Разница довольно мала: в блоке 2 переменная tmp установлена ​​на 0 (хранится в регистре xmml), а в блоке 6, tmp добавляется к возвращаемому результату value.Так что в целом ничего удивительного.Просто время выполнения волшебно;)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...