У меня есть простой метод, который преобразует массив из одного типа в другой.Я хотел выяснить, какой метод самый быстрый.Но до сих пор я получаю разные результаты, из которых я не могу прийти к выводу, какой метод действительно быстрее и на каком уровне.
Поскольку преобразование заключается только в выделении памяти, чтении массива и преобразовании значений, я удивлен, что значения не являются более стабильными.Я хотел знать, как я могу делать точные измерения, которые имеют смысл и не переходить из одного дня в другой.Разница составляет около 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;
}
}
}
Тогда я получаю
- Время: 1257 1388 1180
- Время: 1331 1428 1267
- Время: 1337 1435 1267
- Время: 1208 1414 1145
Из этого выглядит, что тупой безопасный вариант быстрее любого небезопасного варианта, хотя устранение небезопасных методов с помощью проверки границ должно сделать его как минимум настолько быстрымесли не быстрееПросто для забавы я также скомпилировал тот же самый IL-код через LCG (DynamicMethod), который, кажется, даже медленнее, чем любой из этих методов, хотя дополнительные затраты на вызов делегата здесь не играют такой большой роли.
Цикл for выполняет этот код 10 миллионов раз, что должно давать стабильные результаты.Почему я вижу здесь какие-либо различия?Использование реального времени в качестве приоритета процесса также не помогло (исполняемый файл psexec -realtime).Как я могу получить надежные номера?
Мои тесты включали
- Машины с двумя четырехъядерными процессорами
- 32/64-разрядные версии Windows 7
- .NET Framework 3.5 / 4.0
- 32/64-битные версии исполняемого файла.
Если я использую профилировщик, я не уверен, что он будет искажать измерения еще больше.Поскольку он время от времени прерывает мое приложение, чтобы получить стеки вызовов, он, безусловно, уничтожит любую локальность кэша, которая может повысить производительность.Если есть какой-либо подход с лучшей локализацией кэша (данных), я не смогу найти его с помощью профилировщика.Время ОС я делаю сейчас выборку моих измерений.Поскольку для одного потока у меня есть временное окно 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());
Значения этих тестов все еще различаются (<10%), но я думаю, что если я создам гистограмму с моими значениями и отбросим 10% самых высоких значений, которыескорее всего, вызвано ОС, GC, ... Я могу получить действительно стабильные цифры, которым я могу доверять.</p>
SampleSize: 100, мин. 25, макс. 86400, средний 28,614631
- SampleSize: 100, мин. 24, макс 86027, средний 28,762608
- SampleSize: 100, мин. 25, макс. 49523, среднее значение 32,102037
- SampleSize: 100, мин. 24, максимум 48687, среднее значение 32,030088
Редакт. 2: Гистограммы показывают, что измеренные значения не случайны. Они выглядят как распределение Ландау , которое должно дать мне правильные алгоритмы приближения стабильных значений. Я бы хотел, чтобы в .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<long,int>[] 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<long,int>[] GenerateHistogram(long [] input)
{
Dictionary<long, int> counts = new Dictionary<long, int>();
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;
}
}
}