У меня есть простой метод, который преобразует массив из одного типа в другой. Я хотел узнать, какой метод самый быстрый. Но до сих пор я получаю разные результаты, из которых я не могу сделать вывод, какой метод действительно быстрее с какой разницей.
Поскольку преобразование касается только выделения памяти, чтения массива и преобразования значений, я удивлен, что значения не являются более стабильными. Я хотел знать, как я могу проводить точные измерения, которые значимы и не меняются от одного дня к другому. Разница составляет около 20% от одного дня к другому.
Конечно, существуют различия между JITer .NET 3.5 и 4.0, режимом отладки и выпуска, не запускающим исполняемый файл под отладчиком (отключает оптимизацию JIT, пока вы не отключите его), генерацией кода компилятора C # между DEBUG и RELEASE (в основном операции nop и больше временных переменных в коде IL).
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace PerfTest
{
class Program
{
const int RUNS = 10 * 1000 * 1000;
static void Main(string[] args)
{
int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };
var s2 = Stopwatch.StartNew();
for (int i = 0; i < RUNS; i++)
{
float[] arr = Cast(array);
}
s2.Stop();
GC.Collect();
var s3 = Stopwatch.StartNew();
for (int i = 0; i < RUNS; i++)
{
float[] arr = Cast2(array);
}
s3.Stop();
GC.Collect();
var s4 = Stopwatch.StartNew();
for (int i = 0; i < RUNS; i++)
{
var arr = CastSafe(array);
}
s4.Stop();
Console.WriteLine("Times: {0} {1} {2}", s2.ElapsedMilliseconds, s3.ElapsedMilliseconds, s4.ElapsedMilliseconds);
}
// Referece cast implementation to check performance
public static unsafe float[] Cast(int[] input)
{
int N = input.Length;
float[] output = new float[N];
fixed (int* pIStart = &input[0])
{
int* pI = pIStart;
fixed (float* pOStart = &output[0])
{
float* pO = pOStart;
for (int i = 0; i < N; i++)
{
*pO = (float)*pI;
pI++;
pO++;
}
}
}
return output;
}
// Referece cast implementation to check performance
public static unsafe float[] Cast2(int[] input)
{
int N = input.Length;
float[] output = new float[N];
fixed (int* pIStart = &input[0])
{
int* pI = pIStart;
fixed (float* pOStart = &output[0])
{
float* pO = pOStart;
for (int i = 0; i < N; i++)
{
pO[i] = (float) pI[i];
}
}
}
return output;
}
public static float[] CastSafe(int[] input)
{
int N = input.Length;
float[] output = new float[N];
for (int i = 0; i < input.Length; i++)
{
output[i] = (float)input[i];
}
return output;
}
}
}
Я получаю тогда
Отсюда все выглядит глупо безопасный вариант быстрее, чем любой небезопасный вариант, хотя проверка границ устранение небезопасных методов должно сделать его как минимум таким же быстрым, если не быстрее. Ради интереса я также скомпилировал тот же код IL через LCG (DynamicMethod), который, кажется, даже медленнее, чем любой из этих методов, хотя дополнительные затраты на вызов делегата, похоже, не играют здесь такой большой роли.
Цикл for выполняет этот код 10 миллионов раз, что должно дать стабильные результаты. Почему я вообще вижу здесь различия? Использование реального времени в качестве приоритета процесса также не помогло (исполняемый файл psexec -realtime). Как я могу получить надежные цифры?
Мои тесты включали
Если я используйте профилировщик, не уверен, что он еще больше исказит измерения. Поскольку он время от времени прерывает мое приложение, чтобы получить стеки вызовов, он обязательно уничтожит любую локализацию кеша, которая может повысить производительность. Если есть какой-либо подход с лучшей локализацией кеша (данных), я не смогу найти его с помощью профилировщика.
Edit1: Чтобы принять во внимание, что у меня нет ОС реального времени, я сейчас пробую свои измерения. Поскольку для одного потока у меня есть временное окно в 15 мс, предоставленное планировщику Windows, я могу не использовать планировщик, если я измеряю менее 15 мс. Если я измерю слишком быстро, я получу очень маленькое количество тиков, которое мне мало что скажет.
Для получения стабильных значений мне нужен достаточно длительный промежуток времени, чтобы позволить ОС делать то, что она делает на регулярной основе. Эмпирические тесты показали, что 30+ секунд - это хороший промежуток времени, на который следует выполнить одно измерение.
Затем этот временной интервал делится на интервалы времени выборки, которые намного меньше 15 мс. Затем я получу информацию о времени для каждого образца. Из образцов я могу извлечь минимум / максимум и среднее значение. Таким образом, я также могу увидеть эффекты начальной инициализации. Теперь код выглядит так
class Program
{
const int RUNS = 100 * 1000 * 1000; // 100 million runs will take about 30s
const int RunsPerSample = 100; // 100 runs for on sample is about 0,01ms << 15ms
static void Main(string[] args)
{
int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };
long[] sampleTimes = new long [RUNS/RunsPerSample];
int sample = 0;
for (int i = 0; i < RUNS; i+=RunsPerSample)
{
var sw = Stopwatch.StartNew();
for (int j = i; j < i+RunsPerSample; j++)
{
float[] arr = Cast(array);
}
sw.Stop();
sampleTimes[sample] = sw.ElapsedTicks;
sample++;
}
Console.WriteLine("SampleSize: {0}, Min {1}, Max {2}, Average {3}",
RunsPerSample, sampleTimes.Min(), sampleTimes.Max(), sampleTimes.Average());
Значения этих тестов все еще различаются (
SampleSize: 100, Min 25, Max 86400, Average 28,614631
Edit2: Гистограммы показывают, что измеренные значения не случайны. Они выглядят как распределение Ландау , которое должно дать мне стабильные значения с правильными алгоритмами аппроксимации. Я бы хотел, чтобы в .NET существовало что-то вроде ROOT , где я мог бы интерактивно подогнать правильную функцию распределения к своим данным и получить результаты.
Код для генерации гистограммы с MSChart ] ниже:
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace ConsoleApplication4
{
public partial class Histogram : Form
{
public Histogram(long [] sampleTimes)
{
InitializeComponent();
Series histogramSeries = cHistogram.Series.Add("Histogram");
// Set new series chart type and other attributes
histogramSeries.ChartType = SeriesChartType.Column;
histogramSeries.BorderColor = Color.Black;
histogramSeries.BorderWidth = 1;
histogramSeries.BorderDashStyle = ChartDashStyle.Solid;
var filtered = RemoveHighValues(sampleTimes, 40);
KeyValuePair[] histoData = GenerateHistogram(filtered);
ChartArea chartArea = cHistogram.ChartAreas[histogramSeries.ChartArea];
chartArea.AxisY.Title = "Frequency";
chartArea.AxisX.Minimum = histoData.Min( x=>x.Key );
chartArea.AxisX.Maximum = histoData.Max( x=>x.Key );
foreach (var v in histoData)
{
histogramSeries.Points.Add(new DataPoint(v.Key, v.Value));
}
chartArea.AxisY.Minimum = 0;
chartArea.AxisY.Maximum = histoData[0].Value + 100;
}
// Count the occurence of each value of input and return an array with the value as key and its count as value
// as ordered list starting with the highest counts.
KeyValuePair[] GenerateHistogram(long [] input)
{
Dictionary counts = new Dictionary();
foreach (var value in input)
{
int old = 0;
if (!counts.TryGetValue(value, out old))
{
counts[value] = 0;
}
counts[value] = ++old;
}
var orderedCounts = (from x in counts
orderby x.Value descending
select x).ToArray();
return orderedCounts;
}
long[] RemoveHighValues(long[] input, int maxDifference)
{
var min = input.Min();
var max = input.Max();
long[] filtered = input;
while (max - min > maxDifference) // remove all values wich differ by more than maxDifference ticks
{
filtered = input.Where(x => x < max).ToArray();
max = filtered.Max();
}
return filtered;
}
}
}