Eu executei o mesmo benchmark que você, 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 do sistema base) do projeto musl ( URL de espelho ). Existem muitas diferenças entre essas bibliotecas . Como resultado, cada biblioteca pode ter um desempenho melhor em determinados casos de uso.
Aqui está uma diferença entre os comandos acima . A saída começa a diferir da linha 269. É claro que existem endereços diferentes na memória, mas, caso contrário, é muito semelhante. Obviamente, a maior parte do tempo é gasta esperando o python
comando terminar.
Após a instalação strace
nos dois contêineres, podemos obter um rastreamento mais interessante (reduzi 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
O 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 operações de E / S nas bibliotecas principais pode contribuir para um melhor desempenho. Pelo diff, você pode ver que a execução do 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. Não estou qualificado o suficiente para julgar se o problema de desempenho é causado pela alocação de memória ou por 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 presumo que a diferença esteja na alocação de memória dos objetos JSON.
Se reduzirmos o benchmark para a importação exclusiva json
, notamos que a diferença não é tão grande:
$ 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 uma diferença maior:
$ 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 o benchmark ,
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 duas vezes mais lento, o que pode se tornar significativo quando você repete essas operações milhares ou milhões de vezes.