Я реализовал две версии поразрядной сортировки (версия, которая позволяет сортировать целые числа, значения которых доходят до n², где n — размер сортируемого списка) в Python для сравнения со стандартной сортировкой (Timsort). Я использую PyPy для более справедливого сравнения.
Удивительно, но моя реализация поразрядной сортировки, даже без использования хэш-карты (вместо этого используя массив прямого доступа), работает медленнее, чем стандартная сортировка, даже для больших размеров входных данных. Мне не хватает микрооптимизации, поскольку O(n) в конечном итоге должно превзойти O(nlogn). Мне нужен совет, как добиться лучшей производительности. Я делаю это в учебных целях, поэтому мне не нужны встроенные модули, библиотеки или код, скомпилированный на C, для вызова из Python.
Могу ли я внести какие-нибудь микрооптимизации? Действительно ли мой код O(n)?
Мой код выполняется до 10 секунд на процессоре AMD Ryzen 9 7950X:
import matplotlib.pyplot as plt
import random
import time
from collections import defaultdict
def radix_sort(arr, size):
least_sig_digit = defaultdict(list)
for num in arr:
q, r = divmod(num, size)
least_sig_digit[r].append(q)
highest_sig_digit = defaultdict(list)
for k in range(size): # k goes in order of lowest significant digit
for q in least_sig_digit[k]:
highest_sig_digit[q].append(q*size+k)
i: int = 0
for k in range(size):
for num in highest_sig_digit[k]:
arr[i] = num
i += 1
return arr
def radix_sort_no_hashmap(arr, size):
least_sig_digit = [[] for _ in range(size)]
for num in arr:
q, r = divmod(num, size)
least_sig_digit[r].append(q)
highest_sig_digit = [[] for _ in range(size)]
for k in range(size): # k goes in order of lowest significant digit
for q in least_sig_digit[k]:
highest_sig_digit[q].append(q*size+k)
i: int = 0
for k in range(size):
for num in highest_sig_digit[k]:
arr[i] = num
i += 1
return arr
def benchmark_sorting_algorithms():
sizes = [1000, 10000, 100000, 200000, 1000000, 2000000, 3000000, 4000000, 5000000, 6000000, 10000000]
radix_times = []
radix_sort_no_hashmap_times = []
std_sort_times = []
for size in sizes:
array = random.sample(range(1, size**2), size)
new_arr = array.copy()
start_time = time.time()
a = radix_sort(new_arr, size)
radix_times.append(time.time() - start_time)
new_arr = array.copy()
start_time = time.time()
b = radix_sort_no_hashmap(new_arr, size)
radix_sort_no_hashmap_times.append(time.time() - start_time)
new_arr = array.copy()
start_time = time.time()
c = sorted(new_arr)
std_sort_times.append(time.time() - start_time)
for k in range(len(array)):
assert a[k] == b[k] == c[k]
return sizes, radix_times, std_sort_times, radix_sort_no_hashmap_times
sizes, radix_times, std_sort_times, radix_sort_no_hashmap_times = benchmark_sorting_algorithms()
plt.figure(figsize=(12, 6))
plt.plot(sizes, radix_times, label='Radix Sort (O(n))')
plt.plot(sizes, std_sort_times, label='Standard Sort (O(nlogn))')
plt.plot(sizes, radix_sort_no_hashmap_times, label='Radix Sort (O(n)) - No Hashmap')
plt.xlabel('Input size (n)')
plt.xscale('log')
plt.ylabel('Time (seconds)')
plt.yscale('log')
plt.title('Radix Sort vs Standard Sort')
plt.legend()
plt.grid(True)
plt.show()
Тот же вопрос был опубликован здесь, в CPP по совету @user24714692.
@MarkRansom, есть ли какие-то вещи, которые они используют для повышения эффективности, которые я мог бы использовать?
В частности, в Python не будет ли встроенная сортировка быстрее, чем любая рукописная сортировка? Потому что первый реализован на языке более низкого уровня, а второй должен перемещать объекты Python и т. д.
@mark Но это все еще линейноарифмично и довольно сложно.
@Gassa Какой язык нижнего уровня?
@Gassa Вот почему я использую pypy, он должен уметь JIT-компиляцию
@FluidMechanicsPotentialFlows Ага, значит, в PyPy они имеют одинаковый уровень компиляции. Ладно, тогда я отказываюсь от своей точки зрения.
@FluidMechanicsPotentialFlows Я предлагаю добавить тег pypy, так как это может быть связано с особенностями PyPy. (Я, например, понятия не имею, что такое быстрое/медленное в PyPy, какие «ошибки» эффективности можно там допустить.)
@nocomment спасибо за предложение. Я также приветствую любые предложения по ускорению работы с Python. Я просто не включил его, поскольку .sort Python настолько хорош, что я не верю, что смогу победить его без JIT.
Исходный код TimSort должен быть в свободном доступе, вы можете изучить его, чтобы увидеть, что они делают. Вероятно, это гораздо больше, чем просто одна вещь. Например, я знаю, что он обнаруживает предварительно отсортированные последовательности и пропускает их в дальнейших проходах.
@MarkRansom При таких случайных данных такие предварительно отсортированные последовательности короткие и не заслуживают внимания.






Вот версия, которая кажется примерно в 1,5-2,5 раза быстрее вашей в текущем CPython и каком-то старом PyPy (надеюсь, вы сможете включить ее в свой тест/график). Я думаю, что на C++ это может быть даже более полезно.
Это позволяет избежать создания большого количества list объектов. Вместо этого он использует связанные списки в виде arr-индексов, хранящихся в трех длинных списках. Один связанный список на остаток/частное: first[r] и last[r] обозначают концы связанного списка для остатка r, а next[i] сообщает следующий элемент в списке. First/Last/Next то же самое, но для частного q.
def radix_sorted(arr):
n = len(arr)
# Sort by x%n
first = [-1] * n
last = [-1] * n
next = [-1] * n
for i, x in enumerate(arr):
r = x % n
if first[r] == -1:
first[r] = i
else:
next[last[r]] = i
last[r] = i
# Sort by x//n
First = [-1] * n
Last = [-1] * n
Next = [-1] * n
for r in range(n):
i = first[r]
while i != -1:
x = arr[i]
q = x // n
if First[q] == -1:
First[q] = i
else:
Next[Last[q]] = i
Last[q] = i
i = next[i]
# Output
out = []
for q in range(n):
i = First[q]
while i != -1:
out.append(arr[i])
i = Next[i]
return out
# Demo / testing
import random
n = 10**6
arr = random.sample(range(n**2), n)
print(sorted(arr) == radix_sorted(arr))
По сюжету ОП:
Впервые я увидел связанные списки таким образом. Я предполагаю, что это быстрее (но, вероятно, не лучше с точки зрения памяти), чем определение класса LinkedNode в Python, потому что массивы настолько оптимизированы? Спасибо за предложение. Сюжет с вашим методом можно найти здесь: i.imgur.com/10RzPnX.png. Это работает очень хорошо и очень быстро. Я добавил больше размеров, чтобы лучше оценить различия. Усвоенный урок: когда люди говорят об амортизированной сложности O(1) вместо .append, это на самом деле не так хорошо, как в худшем случае O(1) (за исключением случаев, когда O(1) имеет огромные постоянные члены, конечно).
На самом деле кажется, что это всегда быстрее (при использовании PyPy), чем стандартная сортировка. На всякий случай мне следует использовать timeit, чтобы уменьшить дисперсию при более низких значениях.
@FluidMechanicsPotentialFlows Спасибо за сюжет. Я не знаю, насколько быстрым будет класс LinkedNode, но он тоже может быть быстрым. Возможно, использование слотов поможет. Я использовал свои простые списки, ориентируясь на C++. Не могли бы вы попробовать?
Да, конечно. Я попытался определить класс LinkedNode на C++, так как после нашего обсуждения мне это было интересно. Прежде чем я это опубликую, не могли бы вы взглянуть на это? Pastebin.com/y8V95hd7 Это действительно медленнее, чем все другие методы (потому что объекты распределяются в куче, я полагаю? Но если я этого не сделаю, я получу SEGFAULT).
При использовании вашей идеи (три массива pastebin.com/9Bviux9C , массивы также размещаются в куче, а не в стеке) это работает очень хорошо (не лучше, чем встроенный, но намного лучше, чем то, что у нас было до этого) ; я обновлю графики, но если вы заметите, что происходит что-то странное, дайте мне знать! Возврат вывода по значению, среди прочего, мне кажется подозрительным.
@FluidMechanicsPotentialFlows Мне кажется, все в порядке, но я не привык к C++, поэтому не уверен в этом.
Я отредактировал другой пост со всеми дополнительными результатами.
TimSort в Python чрезвычайно эффективен, вы должны быть счастливы, просто находясь рядом.