Почему секундомер неправильно измеряет мой метод?

avatar
Yuval Amir
1 июля 2021 в 19:51
137
2
-1

Я пытаюсь проверить разницу в производительности моих методов быстрой сортировки и сортировки слиянием, но по какой-то причине первый цикл цикла всегда показывает ровно 0,003 миллисекунды, а остальные 0 миллисекунд.

public static void RunDiagnostics(int[] rangeOfLengthsToTest, int numOfTestsPerLength)
{
    Stopwatch stopwatch = new Stopwatch();
    int[] array;
    double totalQuicksortTime, totalMergesortTime;
    for (int i = rangeOfLengthsToTest[0]; i <= rangeOfLengthsToTest[1]; i++)
    {
        totalQuicksortTime = 0;
        totalMergesortTime = 0;

        for (int k = 0; k < numOfTestsPerLength; k++)
        {
            array = GetArray(i, new int[] { -9999, 9999 });
            stopwatch.Start();
            QuickSort((int[])array.Clone());
            stopwatch.Stop();
            totalQuicksortTime += stopwatch.ElapsedMilliseconds;
            stopwatch.Restart();
            MergeSort((int[])array.Clone());
            stopwatch.Stop();
            totalMergesortTime += stopwatch.ElapsedMilliseconds;
        }

        Console.WriteLine($"Quicksort took an average of {totalQuicksortTime / numOfTestsPerLength} milliseconds to sort arrays with a length of {i}.");
        Console.WriteLine($"Mergesort took an average of {totalMergesortTime / numOfTestsPerLength} milliseconds to sort arrays with a length of {i}.");
        Console.WriteLine();
    }
}

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

изменить: Вот мои методы (извините, если они немного бесполезны):

static void Main(string[] args)
    {
            RunDiagnostics(new int[] { 3, 12 }, 1000);
    }
    
public static int[] GetArray(int length, int[] RangeOfNumbers)
    {
        Random rng = new Random();
        int[] arr = new int[length];
        for (int i = 0; i < arr.Length; i++)
            arr[i] = rng.Next(RangeOfNumbers[0], RangeOfNumbers[1]);
        return arr;
    }
    
    public static void QuickSort(int[] array)
            {
                QuickSort(0, array.Length - 1);
                void QuickSort(int left, int right)
                {
                    if (right > left)
                    {
                        if (right - left == 1)
                        {
                            if (array[left] > array[right])
                                Swap(left, right);
                        }
                        else
                        {
                            Swap(GetStartingPivotIndex(), right);
                            int pivot = Partition(left, right - 1);
                            QuickSort(left, pivot - 1);
                            QuickSort(pivot + 1, right);
                        }
                    }
    
                    int GetStartingPivotIndex()
                    {
                        if (array[left] > array[right])
                        {
                            if (array[left + (right - left) / 2] > array[right])
                                return right;
                            else
                                return left + (right - left) / 2;
                        }
                        else
                        {
                            if (array[left + (right - left) / 2] > array[left])
                                return left;
                            else
                                return left + (right - left) / 2;
                        }
                    }
    
                    int Partition(int low, int high)
                    {
                        while (low != high)
                        {
                            if (array[low] < array[right])
                                low++;
                            else if (array[high] > array[right])
                                high--;
                            else
                                Swap(low, high);
                        }
                        Swap(low, right);
                        return low;
                    }
    
                    void Swap(int index1, int Index2)
                    {
                        int temp = array[index1];
                        array[index1] = array[Index2];
                        array[Index2] = temp;
                    }
                }
            }
    
    public static void MergeSort(int[] array)
            {
                MergeSort(array);
                int[] MergeSort(int[] array)
                {
                    int[] array1 = array.Take(array.Length / 2).ToArray();
                    int[] array2 = array.Skip(array.Length / 2).ToArray();
    
                    if (array1.Length > 1)
                        MergeSort(array1);
                    if (array2.Length > 1)
                        MergeSort(array2);
    
                    int c1 = 0;
                    int c2 = 0;
                    bool flag1 = false;
                    bool flag2 = false;
    
                    for (int i = 0; i < array.Length; i++)
                    {
                        if (flag1 && !flag2)
                        {
                            array[i] = array2[c2];
                            if (c2 == array2.Length - 1)
                                flag2 = true;
                            else c2++;
                        }
                        else if (flag2 && !flag1)
                        {
                            array[i] = array1[c1];
                            if (c1 == array1.Length - 1)
                                flag1 = true;
                            else c1++;
                        }
                        else if (!flag1 && !flag2)
                        {
                            if (array1[c1] < array2[c2])
                            {
                                array[i] = array1[c1];
                                if (c1 == array1.Length - 1)
                                    flag1 = true;
                                else c1++;
                            }
                            else
                            {
                                array[i] = array2[c2];
                                if (c2 == array2.Length - 1)
                                    flag2 = true;
                                else c2++;
                            }
                        }
                    }
                    return array;
                }
            }
Источник
Muhammad Azeez
1 июля 2021 в 20:00
0

Помимо того, что сказал @Austin, если вы хотите правильно измерить производительность, подумайте о том, чтобы попробовать BenchmarkDotNet

Yuval Amir
1 июля 2021 в 20:07
0

@Austin Остин Я добавил свои методы (извините, если это беспорядок. Все еще новичок в этом деле).

Heretic Monkey
1 июля 2021 в 20:08
0

Если rangeOfLengthsToTest содержит только два значения, почему бы просто не взять два int? Кроме того, если секундомер работал, вы включаете время array.Clone() и приведение результата к int[] для обоих видов. Конечно, они могут быть одинаковыми для обоих видов, но они могут отличаться. Я предлагаю убрать его и дважды позвонить по GetArray. Я бы также просто использовал метод из одной из криптобиблиотек, чтобы получить массив случайных чисел.

JosephDaSilva
1 июля 2021 в 20:09
4

stopwatch.ElapsedMilliseconds возвращает целочисленное значение и возвращает ноль, если истекшее время меньше одной миллисекунды. Попробуйте вместо этого использовать stopwatch.Elapsed.TotalMilliseconds, который представляет собой значение с плавающей запятой и обеспечивает точность менее миллисекунды.

Tim Schmelter
1 июля 2021 в 20:09
1

@HereticMonkey: Даже если бы они стоили одинаково, разница в % между обоими методами сортировки тогда фальсифицируется, поэтому я бы никогда не стал измерять несвязанные вещи, если это возможно.

Yuval Amir
1 июля 2021 в 20:11
0

@Austin также добавил main. В нем действительно нет ничего интересного.

Yuval Amir
1 июля 2021 в 20:13
0

@JosephDaSilva Это было так. Спасибо.

LarsTech
1 июля 2021 в 20:51
0

Не помещайте ответы в вопрос. Просто опубликуйте свой собственный в соответствующем поле.

mason
1 июля 2021 в 20:53
1

Не пишите свой собственный код для измерения производительности. Скорее всего, вы ошибетесь. BenchmarkDotNet специально предназначен для тестирования производительности и помогает вам правильно протестировать его, чтобы вы могли быть уверены в результате.

Yuval Amir
1 июля 2021 в 20:55
0

@mason В основном это должно было быть просто простым способом понять различия, но когда я увидел, что это не работает, я хотел понять, почему.

Yuval Amir
1 июля 2021 в 21:23
0

@LarsTech должен ли я по-прежнему оставлять реализацию своих методов в ответе, даже если они не связаны с проблемой? Похоже, это не будет полезно для тех, у кого такая же проблема в будущем...

Ответы (2)

avatar
StriplingWarrior
1 июля 2021 в 21:22
1

Это не проблема с секундомером.

При первом запуске кода сортировки это занимает гораздо больше времени, чем в последующие разы. Это может произойти из-за JIT-компиляции, кэширования и подобных вещей, которые полностью находятся вне вашего контроля. Требуется достаточно много времени, чтобы значение ElapsedMilliseconds стало осмысленным целочисленным значением (например, 3), поэтому, когда вы разделите его на 1000, вы получите число в разряде тысяч (например, .003).

Каждый раз, когда запускается код сортировки, это занимает меньше миллисекунды. Таким образом, все операции += добавляют к сумме ноль. Сумма всех этих нулей равна нулю.

Изменение += stopwatch.ElapsedMilliseconds на += stopwatch.Elapsed.TotalMilliseconds; устранит эту конкретную проблему и даст вам примерно такие результаты:

Быстрой сортировке требовалось в среднем 0,003297300000000042 миллисекунды для сортировки массивов длиной 3.
Сортировка слиянием занимала в среднем 0,00199869999999999453 миллисекунды для сортировки массивов длиной 3.
Быстрая сортировка занимала в среднем 0,00131759999999999856 миллисекунд для сортировки массивов длиной 4.
Сортировка слиянием занимала в среднем 0,001030500000000005 миллисекунд для сортировки массивов длиной 4.
Быстрая сортировка занимала в среднем 0,001468300000000015 миллисекунд для сортировки массивов длиной 5.
Сортировка слиянием занимала в среднем 0,00114029999999999956 миллисекунд для сортировки массивов длиной 5.

Однако есть и другие проблемы, которые нужно исправить.

  • Вы включаете в свои результаты время, потраченное на клонирование массива.
  • Start() следует заменить на Restart(): сейчас n-й запуск QuickSort включает n-1-й запуск Mergesort в свое время.
  • Вся стратегия сложения тысяч отдельных периодов выполнения по-прежнему подвергает вас ошибкам округления: они просто меньше при double, чем при int. Вы видите все эти 0000000 и 999999 в результатах? Лучшая стратегия, как правило, состоит в том, чтобы запустить одну и ту же сортировку несколько раз, а затем посмотреть, сколько общего времени прошло.

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

Yuval Amir
1 июля 2021 в 21:30
0

Благодарю вас! @JosephDaSilva уже объяснил мне, что мне нужно использовать секундомер.Elapsed.TotalMilliseconds, но я не понимал, что включаю время из сортировки слиянием.

StriplingWarrior
1 июля 2021 в 22:02
0

@YuvalAmir: нет проблем. Также обратите внимание, что ваша реализация MergeSort страдает от Take, Skip и ToArray. Попробуйте вместо этого использовать Span<int> и Slice. Вы обнаружите, что это происходит намного быстрее, особенно при включенной оптимизации компилятора. И ваш алгоритм QuickSort может попасть в бесконечный цикл, когда в массиве есть два экземпляра одного и того же значения.

avatar
Yuval Amir
1 июля 2021 в 21:21
0

Как указал @JosephDaSilva, stopwatch.ElapsedMilliseconds возвращает целочисленное значение, которое означает, что оно будет округлено до 0, если оно было меньше миллисекунды. Следует использовать stopwatch.Elapsed.TotalMilliseconds, так как он возвращает значение с плавающей запятой, которое не будет округлено в меньшую сторону.