Uruchomiłem ten sam test co ty, używając tylko Pythona 3:
$ docker run python:3-alpine3.6 python --version
Python 3.6.2
$ docker run python:3-slim python --version
Python 3.6.2
co powoduje różnicę przekraczającą 2 sekundy:
$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
5.834922112524509
Alpine używa innej implementacji libc
(podstawowej biblioteki systemowej) niż projekt musl ( lustrzany URL ). Istnieje wiele różnic między tymi bibliotekami . W rezultacie każda biblioteka może działać lepiej w niektórych przypadkach użycia.
Oto różnica między tymi poleceniami powyżej . Wyjście zaczyna się różnić od wiersza 269. Oczywiście w pamięci są różne adresy, ale poza tym jest bardzo podobne. Większość czasu jest oczywiście poświęcana na oczekiwanie na zakończenie python
polecenia.
Po zainstalowaniu strace
w obu kontenerach możemy uzyskać bardziej interesujący ślad (zmniejszyłem liczbę iteracji w teście do 10).
Na przykład glibc
ładuje biblioteki w następujący sposób (wiersz 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
Ten sam kod w 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
Nie twierdzę, że jest to kluczowa różnica, ale zmniejszenie liczby operacji we / wy w bibliotekach podstawowych może przyczynić się do lepszej wydajności. Z porównania widać, że wykonanie tego samego kodu Pythona może prowadzić do nieco innych wywołań systemowych. Prawdopodobnie najważniejszą rzeczą może być optymalizacja wydajności pętli. Nie mam wystarczających kwalifikacji, aby ocenić, czy problem z wydajnością jest spowodowany alokacją pamięci lub innymi instrukcjami.
glibc
z 10 iteracjami:
write(1, "0.032388824969530106\n", 210.032388824969530106)
musl
z 10 iteracjami:
write(1, "0.035214247182011604\n", 210.035214247182011604)
musl
jest wolniejszy o 0,0028254222124814987 sekund. Ponieważ różnica rośnie wraz z liczbą iteracji, zakładam, że różnica polega na alokacji pamięci obiektów JSON.
Jeśli ograniczymy test do samego importu json
, zauważymy, że różnica nie jest aż tak duża:
$ 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
Ładowanie bibliotek Pythona wygląda porównywalnie. Generowanie list()
powoduje większą różnicę:
$ 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
Oczywiście jest to najdroższa operacja json.dumps()
, która może wskazywać na różnice w alokacji pamięci między tymi bibliotekami.
Patrząc znów na benchmarku ,
musl
jest naprawdę nieznacznie wolniejszy w alokacji pamięci:
musl | glibc
-----------------------+--------+--------+
Tiny allocation & free | 0.005 | 0.002 |
-----------------------+--------+--------+
Big allocation & free | 0.027 | 0.016 |
-----------------------+--------+--------+
Nie jestem pewien, co należy rozumieć przez „duży przydział”, ale musl
jest prawie 2 razy wolniejszy, co może stać się znaczące, gdy powtórzy się takie operacje tysiące lub miliony razy.