Fiz um teste simples de desempenho em python 3.12.0
contra python 3.13.0b3
compilado com um --disable-gil
sinalizador. O programa executa cálculos de uma sequência de Fibonacci usando ThreadPoolExecutor
ou ProcessPoolExecutor
. Os documentos sobre o PEP que introduzem o GIL desativado dizem que há um pouco de sobrecarga principalmente devido à contagem de referência tendenciosa seguida de bloqueio por objeto ( https://peps.python.org/pep-0703/#performance ). Mas diz que a sobrecarga no benchmark pyperformance está em torno de 5-8%. Meu benchmark simples mostra uma diferença significativa no desempenho. Na verdade, o python 3.13 sem GIL utiliza todas as CPUs com a, ThreadPoolExecutor
mas é muito mais lento que o python 3.12 com GIL. Com base na utilização da CPU e no tempo decorrido, podemos concluir que com o python 3.13 realizamos várias vezes mais ciclos de clock em comparação com o 3.12.
Código do programa:
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')
Resultado dos testes:
# 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
Alguém pode explicar por que sinto tanta diferença ao comparar a sobrecarga com a do benchmark pyperformance?
EDITAR 1
- Eu tentei
pool_executor(cpus)
em vez depool_executor(8)
-> ainda obtive resultados semelhantes. - Assisti a este vídeo https://www.youtube.com/watch?v=zWPe_CUR4yU e executei o seguinte teste: https://github.com/ArjanCodes/examples/blob/main/2024/gil/main.py
Resultados:
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
então, mais um teste em minha máquina quando terminamos com um desempenho várias vezes mais lento. Por falar nisso. No vídeo podemos ver resultados de sobrecarga semelhantes aos descritos no PEP.
EDITAR 2
Como sugeriu @ekhumoro, configurei a compilação com os seguintes sinalizadores:
./configure --disable-gil --enable-optimizations
e parece que o --enable-optimizations
sinalizador faz uma diferença significativa nos benchmarks considerados. A build anterior foi feita com a seguinte configuração:
./configure --with-pydebug --disable-gil
.
Resultados dos testes:
Referência de Fibonacci:
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
Referência de números primos:
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
Portanto, o ganho geral da mudança do multiprocessamento python 3.12 para o multithreading python 3.12 no-gil é uma economia significativa de memória (temos apenas um único processo).
Quando comparamos a sobrecarga da CPU para a máquina com apenas 2 núcleos:
[Fibonacci] Multi-threading Python 3.13 contra multiprocessamento Python 3.12: (6,94 - 4,27) / 4,27 * 100% ~ = 63% de sobrecarga
[Números primos] Multithreading Python 3.13 versus multiprocessamento Python 3.12: (4,17 - 3,23) / 3,23 * 100% ~= 29% de sobrecarga