Рассмотрим следующие две разные функции 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
: Итак, на уровне ассемблера естьне происходит никакого волшебства, которое может сделать код быстрее.Есть ли другое возможное объяснение этого эффекта?И как это проверить?
А вот результаты с BenchmarkDotNet (параметр N
имеет размер _dataCol
, _dataRow
всегда имеет размер N ^ 2):
И результаты сравнения ComputeA
и ComputeC
:
JIT-сборка для ComputeA
(слева) и ComputeC
(справа):
Разница довольно мала: в блоке 2 переменная tmp
установлена на 0
(хранится в регистре xmml
), а в блоке 6, tmp
добавляется к возвращаемому результату value
.Так что в целом ничего удивительного.Просто время выполнения волшебно;)