Eu executei o mesmo benchmark que você usou usando apenas o Python 3:
$ docker run python:3-alpine3.6 python --version
Python 3.6.2
$ docker run python:3-slim python --version
Python 3.6.2
resultando em mais de 2 segundos de diferença:
$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
5.834922112524509
A Alpine está usando uma implementação diferente de libc
(biblioteca básica do sistema) de musl .
Existem muitas diferenças entre essas bibliotecas .
Como resultado, cada biblioteca pode ter um melhor desempenho em certos casos de uso.
Aqui está uma comparação de straces entre os comandos acima . A saída começa a diferir da linha 269. Claro que existem endereços diferentes na memória, mas de outra forma é muito semelhante. A maior parte do tempo é obviamente gasta esperando o comando python
terminar.
Depois de instalar strace
em ambos os contêineres, podemos obter um rastreamento mais interessante (reduzimos o número de iterações no benchmark para 10).
Por exemplo, glibc
está carregando bibliotecas da seguinte maneira (linha 182):
openat(AT_FDCWD, "/usr/local/lib/python3.6", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 205 entries */, 32768) = 6824
getdents(3, /* 0 entries */, 32768) = 0
mesmo código em musl
:
open("/usr/local/lib/python3.6", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
getdents64(3, /* 62 entries */, 2048) = 2040
getdents64(3, /* 61 entries */, 2048) = 2024
getdents64(3, /* 60 entries */, 2048) = 2032
getdents64(3, /* 22 entries */, 2048) = 728
getdents64(3, /* 0 entries */, 2048) = 0
Não estou dizendo que essa é a principal diferença, mas reduzir o número de E / S nas bibliotecas principais pode contribuir para um melhor desempenho. A partir do diff, você pode ver que executar o mesmo código Python pode levar a chamadas de sistema ligeiramente diferentes. Provavelmente, o mais importante poderia ser feito na otimização do desempenho do loop. Eu não estou qualificado o suficiente para julgar se o problema de desempenho é causado por alocação de memória ou alguma outra instrução.
-
glibc
com 10 iterações:write(1, "0.032388824969530106\n", 210.032388824969530106)
-
musl
com 10 iterações:write(1, "0.035214247182011604\n", 210.035214247182011604)
musl
é mais lento em 0.0028254222124814987 segundos.
À medida que a diferença aumenta com o número de iterações,
Eu suponho que a diferença está na alocação de memória dos objetos JSON.
Se reduzirmos o valor de referência apenas para importar json
, notamos que a diferença não é tão grande assim:
$ BENCHMARK="import timeit; print(timeit.timeit('import json;', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.03683806210756302
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.038280246779322624
Carregar bibliotecas python parece comparável. Gerar list()
produz maior diferença:
$ BENCHMARK="import timeit; print(timeit.timeit('list(range(10000))', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.5666235145181417
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.6885563563555479
Obviamente, a operação mais cara é json.dumps()
, o que pode apontar para diferenças na alocação de memória entre essas bibliotecas.
Olhando novamente para a referência ,
musl
é realmente um pouco mais lento na alocação de memória:
musl | glibc
-----------------------+--------+--------+
Tiny allocation & free | 0.005 | 0.002 |
-----------------------+--------+--------+
Big allocation & free | 0.027 | 0.016 |
-----------------------+--------+--------+
Não sei ao certo o que significa "grande alocação", mas musl
é quase 2 × mais lento, o que pode se tornar significativo quando você repetir essas operações milhares ou milhões de vezes.