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

Я реализовал две версии поразрядной сортировки (версия, которая позволяет сортировать целые числа, значения которых доходят до 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.

TimSort в Python чрезвычайно эффективен, вы должны быть счастливы, просто находясь рядом.

Mark Ransom 18.06.2024 20:21

@MarkRansom, есть ли какие-то вещи, которые они используют для повышения эффективности, которые я мог бы использовать?

FluidMechanics Potential Flows 18.06.2024 20:58

В частности, в Python не будет ли встроенная сортировка быстрее, чем любая рукописная сортировка? Потому что первый реализован на языке более низкого уровня, а второй должен перемещать объекты Python и т. д.

Gassa 18.06.2024 21:07

@mark Но это все еще линейноарифмично и довольно сложно.

no comment 18.06.2024 21:07

@Gassa Какой язык нижнего уровня?

no comment 18.06.2024 21:08

@Gassa Вот почему я использую pypy, он должен уметь JIT-компиляцию

FluidMechanics Potential Flows 18.06.2024 21:10

@FluidMechanicsPotentialFlows Ага, значит, в PyPy они имеют одинаковый уровень компиляции. Ладно, тогда я отказываюсь от своей точки зрения.

Gassa 18.06.2024 21:11

@FluidMechanicsPotentialFlows Я предлагаю добавить тег pypy, так как это может быть связано с особенностями PyPy. (Я, например, понятия не имею, что такое быстрое/медленное в PyPy, какие «ошибки» эффективности можно там допустить.)

no comment 18.06.2024 21:38

@nocomment спасибо за предложение. Я также приветствую любые предложения по ускорению работы с Python. Я просто не включил его, поскольку .sort Python настолько хорош, что я не верю, что смогу победить его без JIT.

FluidMechanics Potential Flows 18.06.2024 22:18

Исходный код TimSort должен быть в свободном доступе, вы можете изучить его, чтобы увидеть, что они делают. Вероятно, это гораздо больше, чем просто одна вещь. Например, я знаю, что он обнаруживает предварительно отсортированные последовательности и пропускает их в дальнейших проходах.

Mark Ransom 18.06.2024 22:31

@MarkRansom При таких случайных данных такие предварительно отсортированные последовательности короткие и не заслуживают внимания.

no comment 18.06.2024 22:36
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
11
169
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вот версия, которая кажется примерно в 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) имеет огромные постоянные члены, конечно).

FluidMechanics Potential Flows 20.06.2024 10:33

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

FluidMechanics Potential Flows 20.06.2024 10:50

@FluidMechanicsPotentialFlows Спасибо за сюжет. Я не знаю, насколько быстрым будет класс LinkedNode, но он тоже может быть быстрым. Возможно, использование слотов поможет. Я использовал свои простые списки, ориентируясь на C++. Не могли бы вы попробовать?

no comment 20.06.2024 13:02

Да, конечно. Я попытался определить класс LinkedNode на C++, так как после нашего обсуждения мне это было интересно. Прежде чем я это опубликую, не могли бы вы взглянуть на это? Pastebin.com/y8V95hd7 Это действительно медленнее, чем все другие методы (потому что объекты распределяются в куче, я полагаю? Но если я этого не сделаю, я получу SEGFAULT).

FluidMechanics Potential Flows 20.06.2024 14:58

При использовании вашей идеи (три массива pastebin.com/9Bviux9C , массивы также размещаются в куче, а не в стеке) это работает очень хорошо (не лучше, чем встроенный, но намного лучше, чем то, что у нас было до этого) ; я обновлю графики, но если вы заметите, что происходит что-то странное, дайте мне знать! Возврат вывода по значению, среди прочего, мне кажется подозрительным.

FluidMechanics Potential Flows 20.06.2024 14:59

@FluidMechanicsPotentialFlows Мне кажется, все в порядке, но я не привык к C++, поэтому не уверен в этом.

no comment 20.06.2024 15:25

Я отредактировал другой пост со всеми дополнительными результатами.

FluidMechanics Potential Flows 20.06.2024 16:40

Другие вопросы по теме