Подтвердить что ты не робот

Неожиданная кривая производительности от сортировки слияния CPython

Я реализовал простой алгоритм сортировки слиянием в Python. Алгоритм и тестовый код приведены ниже:

import time
import random
import matplotlib.pyplot as plt
import math
from collections import deque

def sort(unsorted):
    if len(unsorted) <= 1:
        return unsorted
    to_merge = deque(deque([elem]) for elem in unsorted)
    while len(to_merge) > 1:
        left = to_merge.popleft()
        right = to_merge.popleft()
        to_merge.append(merge(left, right))
    return to_merge.pop()

def merge(left, right):
    result = deque()
    while left or right:
        if left and right:
            elem = left.popleft() if left[0] > right[0] else right.popleft()
        elif not left and right:
            elem = right.popleft()
        elif not right and left:
            elem = left.popleft()
        result.append(elem)
    return result

LOOP_COUNT = 100
START_N = 1
END_N = 1000

def test(fun, test_data):
    start = time.clock()
    for _ in xrange(LOOP_COUNT):
        fun(test_data)
    return  time.clock() - start

def run_test():
    timings, elem_nums = [], []
    test_data = random.sample(xrange(100000), END_N)
    for i in xrange(START_N, END_N):
        loop_test_data = test_data[:i]
        elapsed = test(sort, loop_test_data)
        timings.append(elapsed)
        elem_nums.append(len(loop_test_data))
        print "%f s --- %d elems" % (elapsed, len(loop_test_data))
    plt.plot(elem_nums, timings)
    plt.show()

run_test()

Насколько я вижу, все в порядке, и в результате я должен получить хорошую кривую N * logN. Но картина немного отличается:

enter image description here

Вещи, которые я пытался исследовать:

  1. PyPy. Кривая в порядке.
  2. Отключил GC с помощью модуля gc. Неправильное предположение. Вывод отладки показал, что он даже не запускается до конца теста.
  3. Профилирование памяти с помощью meliae - ничего особенного или подозрительного. ' У меня была другая реализация (рекурсивная, использующая ту же функцию слияния), она действует аналогичным образом. Чем больше полных циклов тестирования я создаю - тем больше "скачков" в кривой.

Итак, как это поведение можно объяснить и, мы надеемся, исправить?

UPD: изменены списки на collection.deque

UPD2: добавлен полный тестовый код

UPD3: я использую Python 2.7.1 на ОС Ubuntu 11.04, используя четырехъядерный ноутбук с частотой 2 Гц. Я пытался выключить большинство других процессов: количество пиков уменьшилось, но по крайней мере один из них все еще был.

4b9b3361

Ответ 1

Вы просто подбираете влияние других процессов на своей машине.

Вы выполняете свою функцию сортировки 100 раз для размера ввода 1 и записываете общее время, затраченное на это. Затем вы запускаете его 100 раз для размера ввода 2 и записываете общее время. Вы продолжаете делать это, пока не достигнете размера ввода 1000.

Предположим, что время от времени ваша ОС (или вы сами) начинаете делать что-то более интенсивно. Пусть говорят, что этот "всплеск" длится до тех пор, пока вам понадобится запустить функцию сортировки 5000 раз. Это означает, что время выполнения будет выглядеть медленным для 5000/100 = 50 последовательных размеров ввода. Спустя некоторое время происходит другой всплеск, и другой диапазон входных размеров выглядит медленным. Это именно то, что вы видите в своем графике.

Я могу думать об одном способе избежать этой проблемы. Запустите функцию сортировки только один раз для каждого размера ввода: 1, 2, 3,..., 1000. Повторите этот процесс 100 раз, используя те же 1000 входов (это важно, см. Объяснение в конце). Теперь возьмите минимальное время, затраченное на каждый размер ввода, в качестве конечной точки данных для диаграммы.

Таким образом, ваши всплески должны влиять только на каждый размер входного сигнала всего несколько раз из 100 прогонов; и поскольку вы берете минимум, они, вероятно, вообще не повлияют на итоговый график.

Если ваши спайки действительно очень длинные и частые, вы, конечно, можете захотеть увеличить количество повторений за текущий 100 за каждый размер входного файла.

Глядя на ваши шипы, я замечаю, что выполнение замедляется ровно 3 раза во время всплеска. Я предполагаю, что ОС дает вашему процессу python один слот из трех при высокой нагрузке. Правильно ли мое предположение или нет, рекомендуемый подход должен решить проблему.

EDIT:

Я понял, что я не уточнил одну точку в моем предлагаемом решении вашей проблемы.

Следует ли использовать один и тот же ввод в каждом из ваших 100 запусков для заданного размера ввода? Или следует использовать 100 различных (случайных) входов?

Поскольку я рекомендовал взять минимум времени выполнения, входы должны быть одинаковыми (иначе вы получите неправильный вывод, так как вы будете оценивать сложность алгоритма наилучшего варианта, а не среднюю сложность!).

Но когда вы берете те же самые входы, вы создаете некоторый шум в своей диаграмме, поскольку некоторые входы просто быстрее других.

Таким образом, лучшим решением является решение проблемы загрузки системы, не создавая проблемы только одного входа на каждый размер входного файла (это, очевидно, псевдокод):

seed = 'choose whatever you like'
repeats = 4
inputs_per_size = 25
runtimes = defaultdict(lambda : float('inf'))
for r in range(repeats):
  random.seed(seed)
  for i in range(inputs_per_size):
    for n in range(1000):
      input = generate_random_input(size = n)
      execution_time = get_execution_time(input)
      if runtimes[(n, i)] > execution_time:
        runtimes[(n,i)] = execution_time
for n in range(1000):
  runtimes[n] = sum(runtimes[(n,i)] for i in range(inputs_per_size))/inputs_per_size

Теперь вы можете использовать runtimes [n] для построения вашего сюжета.

Конечно, если ваша система супер-шумна, вы можете изменить (repeats, inputs_per_size) из (4,25), скажем, (10,10) или даже (25,4).

Ответ 2

Я могу воспроизвести шипы с помощью вашего кода:

spikes in measured time performance

Вы должны выбрать соответствующую функцию синхронизации (time.time() vs. time.clock() - from timeit import default_timer), количество повторений в тесте (сколько времени проходит каждый тест) и количество тестов, чтобы выбрать минимальное время от, Это дает вам лучшую точность и меньшее влияние на результаты. Прочтите примечание от timeit.Timer.repeat() docs:

Его соблазн рассчитать среднее и стандартное отклонение от результата вектор и сообщить об этом. Однако это не очень полезно. В типичный случай, самое низкое значение дает нижнюю оценку того, насколько быстро ваш машина может запускать данный фрагмент кода; более высокие значения в результате вектор, как правило, не вызваны изменчивостью скорости Pythons, но другими процессами, мешающими вашей точности времени. Таким образом, min() результата, вероятно, единственного числа, которое вас должно заинтересовать. После этого вы должны посмотреть на весь вектор и применить общий смысл, а не статистика.

timeit модуль может выбрать для вас соответствующие параметры:

$ python -mtimeit -s 'from m import testdata, sort; a = testdata[:500]' 'sort(a)'

Здесь кривая производительности timeit:

time peformance of sorting functions

На рисунке показано, что поведение sort() соответствует O(n*log(n)):

|------------------------------+-------------------|
| Fitting polynom              | Function          |
|------------------------------+-------------------|
| 1.00  log2(N)   +  1.25e-015 | N                 |
| 2.00  log2(N)   +  5.31e-018 | N*N               |
| 1.19  log2(N)   +      1.116 | N*log2(N)         |
| 1.37  log2(N)   +      2.232 | N*log2(N)*log2(N) |

Чтобы создать цифру, я использовал make-figures.py:

$ python make-figures.py --nsublists 1 --maxn=0x100000 -s vkazanov.msort -s vkazanov.msort_builtin 

где:

# adapt sorting functions for make-figures.py
def msort(lists):
    assert len(lists) == 1
    return sort(lists[0]) # `sort()` from the question

def msort_builtin(lists):
    assert len(lists) == 1
    return sorted(lists[0]) # builtin

Списки ввода описаны здесь (примечание: вход сортируется, поэтому встроенная функция sorted() показывает ожидаемую производительность O(N)).