Что привело к тому, что Python 3.13-0b3 (скомпилированный с отключенным GIL) работал медленнее, чем 3.12.0?

Я провел простой тест производительности на Python 3.12.0 с Python 3.13.0b3, скомпилированным с флагом --disable-gil. Программа выполняет вычисления последовательности Фибоначчи, используя ThreadPoolExecutor или ProcessPoolExecutor. В документации PEP, представляющей отключенный GIL, говорится, что существуют некоторые накладные расходы, в основном из-за смещенного подсчета ссылок с последующей блокировкой каждого объекта (https://peps.python.org/pep-0703/# Performance). Но в нем говорится, что накладные расходы на тест производительности pyperformance составляют около 5-8%. Мой простой тест показывает значительную разницу в производительности. Действительно, Python 3.13 без GIL использует все процессоры. с ThreadPoolExecutor, но он намного медленнее, чем Python 3.12 с GIL. Основываясь на загрузке процессора и затраченном времени, мы можем сделать вывод, что с Python 3.13 мы выполняем в несколько раз больше тактов по сравнению с 3.12.

Код программы:

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import datetime
from functools import partial
import sys
import logging
import multiprocessing

logging.basicConfig(
    format='%(levelname)s: %(message)s',
)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
cpus = multiprocessing.cpu_count()
pool_executor = ProcessPoolExecutor if len(sys.argv) > 1 and sys.argv[1] == '1' else ThreadPoolExecutor
python_version_str = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
logger.info(f'Executor = {pool_executor.__name__}, python = {python_version_str}, cpus = {cpus}')


def fibonacci(n: int) -> int:
    if n < 0:
        raise ValueError("Incorrect input")
    elif n == 0:
        return 0
    elif n == 1 or n == 2:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

start = datetime.datetime.now()

with pool_executor(8) as executor:
    for task_id in range(30):
        executor.submit(partial(fibonacci, 30))

    executor.shutdown(wait=True)

end = datetime.datetime.now()
elapsed = end - start
logger.info(f'Elapsed: {elapsed.total_seconds():.2f} seconds')

Результаты теста:

# TEST Linux 5.15.0-58-generic, Ubuntu 20.04.6 LTS

INFO: Executor=ThreadPoolExecutor, python=3.12.0, cpus=2
INFO: Elapsed: 10.54 seconds

INFO: Executor=ProcessPoolExecutor, python=3.12.0, cpus=2
INFO: Elapsed: 4.33 seconds

INFO: Executor=ThreadPoolExecutor, python=3.13.0b3, cpus=2
INFO: Elapsed: 22.48 seconds

INFO: Executor=ProcessPoolExecutor, python=3.13.0b3, cpus=2
INFO: Elapsed: 22.03 seconds

Может ли кто-нибудь объяснить, почему я испытываю такую ​​разницу при сравнении накладных расходов с накладными расходами в тесте py Performance?

РЕДАКТИРОВАТЬ 1

  1. Я попробовал использовать pool_executor(cpus) вместо pool_executor(8) -> все равно получил аналогичные результаты.
  2. Я посмотрел это видео https://thewikihow.com/video_zWPe_CUR4yU и выполнил следующий тест: https://github.com/ArjanCodes/examples/blob/main/2024/gil/ main.py

Полученные результаты:

Version of python: 3.12.0a7 (main, Oct  8 2023, 12:41:37) [GCC 9.4.0]
GIL cannot be disabled
Single-threaded: 78498 primes in 6.67 seconds
Threaded: 78498 primes in 7.89 seconds
Multiprocessed: 78498 primes in 5.85 seconds

Version of python: 3.13.0b3 experimental free-threading build (heads/3.13.0b3:7b413952e8, Jul 27 2024, 11:19:31) [GCC 9.4.0]
GIL is disabled
Single-threaded: 78498 primes in 61.42 seconds
Threaded: 78498 primes in 32.29 seconds
Multiprocessed: 78498 primes in 39.85 seconds

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

РЕДАКТИРОВАТЬ 2

Как предложил @ekhumoro, я настроил сборку со следующими флагами:
./configure --disable-gil --enable-optimizations
и кажется, что флаг --enable-optimizations имеет существенное значение в рассматриваемых тестах. Предыдущая сборка была сделана со следующей конфигурацией:
./configure --with-pydebug --disable-gil.

Результаты испытаний:

Эталон Фибоначчи:

INFO: Executor=ThreadPoolExecutor, python=3.12.0, cpus=2
INFO: Elapsed: 10.25 seconds

INFO: Executor=ProcessPoolExecutor, python=3.12.0, cpus=2
INFO: Elapsed: 4.27 seconds

INFO: Executor=ThreadPoolExecutor, python=3.13.0, cpus=2
INFO: Elapsed: 6.94 seconds

INFO: Executor=ProcessPoolExecutor, python=3.13.0, cpus=2
INFO: Elapsed: 6.94 seconds

Тест простых чисел:

Version of python: 3.12.0a7 (main, Oct  8 2023, 12:41:37) [GCC 9.4.0]
GIL cannot be disabled
Single-threaded: 78498 primes in 5.77 seconds
Threaded: 78498 primes in 7.21 seconds
Multiprocessed: 78498 primes in 3.23 seconds

Version of python: 3.13.0b3 experimental free-threading build (heads/3.13.0b3:7b413952e8, Aug  3 2024, 14:47:48) [GCC 9.4.0]
GIL is disabled
Single-threaded: 78498 primes in 7.99 seconds
Threaded: 78498 primes in 4.17 seconds
Multiprocessed: 78498 primes in 4.40 seconds

Таким образом, общий выигрыш от перехода от многопроцессорности Python 3.12 к многопоточности Python 3.12 no-gil заключается в значительной экономии памяти (у нас действительно есть только один процесс).

Когда мы сравниваем нагрузку на процессор для машины только с двумя ядрами:

[Фибоначчи] Многопоточность Python 3.13 по сравнению с многопоточностью Python 3.12: (6,94 - 4,27) / 4,27 * 100% ~= 63% накладных расходов.

[Простые числа] Многопоточность Python 3.13 по сравнению с многопоточностью Python 3.12: (4,17 - 3,23) / 3,23 * 100% ~= 29% накладных расходов.

Я могу лишь частично воспроизвести это в Arch-Linux, используя Python-3.12.4 и Python-3.13.0rc1 с ядром Linux-6.8.9. (В моей системе установлен четырехъядерный процессор AMD Ryzen 3 1200). Используя ваш сценарий, я получаю следующий результат: Py312 ThreadPoolExecutor 10,88 секунды; Py312 ProcessPoolExecutor 1,44 секунды; Py313 ThreadPoolExecutor 3,72 секунды; Py313 ProcessPoolExecutor 3,84 секунды. Таким образом, примерно в 3 раза быстрее для пула потоков, но примерно в 2,5 раза медленнее для пула процессов.

ekhumoro 03.08.2024 11:39

Для сравнения: можете ли вы измерить производительность в одном потоке без использования параллельного? Кроме того, при создании процессов не больше, чем количество ядер процессора? (т.е. 2)

ken 03.08.2024 11:47

У меня при использовании одного потока: Py312 — 5,5 секунды; Py313 — 15,1 секунды. Таким образом, это подтверждает накладные расходы многопоточности, но все равно показывает замедление в 3 раза для Python-3.13. (Используется ли одновременное использование или нет, не имеет существенного значения).

ekhumoro 03.08.2024 12:19

Спасибо за проведение теста на ваших машинах. Я обновил основной пост, добавив дополнительную информацию в раздел «РЕДАКТИРОВАТЬ» в конце сообщения.

K4liber 03.08.2024 13:13

@K4liber Я не могу воспроизвести ваши результаты, используя связанный тестовый сценарий. Для меня однопоточный процесс примерно на 25% медленнее, но многопоточный в 4 раза быстрее, а многопоточный примерно такой же. Как вы установили Python-3.13? Вам действительно следует скомпилировать последний релиз-кандидат со стандартными флагами сборки Python для вашей платформы (плюс --enable-optimizations и --disable-gil). Вы можете отобразить текущие флаги следующим образом: python3.13 -c 'import sysconfig; print(" ".join(sysconfig.get_config_var( "CONFIG_ARGS").split()))'.

ekhumoro 03.08.2024 14:08

@ekhumoro спасибо! Я пропустил флаг --enable-optimizations. Я отредактировал основной пост. Пожалуйста, создайте ответ, чтобы мы могли подчеркнуть решение.

K4liber 03.08.2024 15:14

@K4liber Нет проблем. Еще более существенная проблема заключается в том, что вы тестировали с помощью отладочной сборки. Теперь я добавил ответ по этому поводу.

ekhumoro 03.08.2024 15:38
Почему в 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 может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
7
189
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

ваш код представляет собой высокорекурсивную функцию, имеющую экспоненциальную временную сложность. Каждый вызов fibonacci(30) может привести к множеству избыточных вычислений. это неэффективный способ вычисления чисел Фибоначчи, поскольку он в основном связан с использованием ЦП и требует больших вычислительных затрат.

https://thewikihow.com/video_Q83nN97LVOU

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

K4liber 03.08.2024 11:11

GIL предотвращает одновременное выполнение байт-кода Python несколькими собственными потоками. Это делает многопоточные задачи, связанные с ЦП, менее эффективными, но упрощает управление памятью, поскольку одновременно может выполняться только один поток. Хотя удаление GIL обеспечивает настоящий параллелизм, связанные с этим накладные расходы могут перевесить преимущества для определенных типов задач, особенно тех, которые привязаны к ЦП и требуют частых обновлений общего состояния.

gill bates 03.08.2024 11:14

@ekhumoro это мой источник - dev.to/ohdylan/…

gill bates 03.08.2024 12:01

@gillbates спасибо за информацию, с общей концепцией я знаком. Тем не менее, мне интересно, почему существует такая значительная разница, а не небольшая, как описано в PEP. Ответ, вероятно, кроется где-то в деталях реализации с отключенным gil и различными тестами.

K4liber 03.08.2024 13:22

вы обращаетесь к правильной вещи, но я бы скорее назвал это неэффективностью огромных накладных расходов (из-за выбора обработки, сформулированной рекурсией), которая взрывается как в EXPTIME, так и в EXPSPACE (как и все данные «сохранения состояния», для рекурсии погружение, должно где-то храниться + в каком-то дополнительном, невычисляющем устройстве, просто накладные расходы на сохранение и извлечение, время добавляется к итоговой неэффективности), а не вычислительные затраты. Порядок тестов в случаях if/elif скорее противоречил производительности, отвлекая внимание от любых проблем, связанных с блокировкой GIL. В любом случае, GIL мертв, да здравствует Python!

user3666197 03.08.2024 20:04
Ответ принят как подходящий

Судя по последним изменениям вопросов, похоже, что версия Python-3.13, использованная для тестирования, была построена с включенным режимом отладки и без включенной оптимизации. В частности, первый флаг может оказать большое влияние на тестирование производительности, тогда как второй будет иметь гораздо меньшее, но все же существенное влияние. В общем, лучше избегать каких-либо выводов о проблемах с производительностью при тестировании сборок Python для разработки.

Спасибо за помощь. Меня немного смущает название этого поста, оно звучит немного пассивно-агрессивно по отношению к разработчикам CPython, которые отлично поработали с реализацией no-gil. С другой стороны, название — это своего рода кликбейт, который может привести к увеличению числа разработчиков, тестирующих сборки без gil.

K4liber 03.08.2024 18:30

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

Похожие вопросы

Как я могу получить группу с наибольшей полосой отрицательных чисел в столбце и добавить еще одно условие для фильтрации групп?
Как ввести подсказку для оболочки класса или класса, метод которого назначен не в определении в Python3?
Есть ли способ заменить элемент с плавающей запятой в массиве строковым элементом в Python?
Как навсегда добавить путь в PATH пользователя?
Запустите асинхронную функцию из функции синхронизации в уже запущенном цикле событий
Python: набор фильтров/список кортежей на основе свойства мощности
Различение однородных и гетерогенных кортежей в перегрузках функций Python
Как лучше всего вернуть группу с наибольшей полосой отрицательных чисел в столбце?
Как гарантировать, что вставки принимают только значения, определенные в Enum?
Есть ли способ «извлечь» переменную возвращаемого значения из функции и использовать ее в другом месте, не вызывая исходную функцию?