python vs bc dans l'évaluation de 6 ^ 6 ^ 6

29

J'évalue l'expression en 6^6^6utilisant pythonet bcséparément.

Le contenu du fichier python est print 6**6**6. Quand j'exécute time python test.py, j'obtiens la sortie

real        0m0.067s
user        0m0.050s
sys         0m0.011s

Et puis, j'ai exécuté la commande time echo 6^6^6 | bcqui m'a donné la sortie suivante

real        0m0.205s
user        0m0.197s
sys         0m0.005s

D'après ces résultats, il est clair que le temps sys pris par python et bc était de 11 ms et 5 ms respectivement. La commande bc a surpassé python au niveau du temps sys, mais en ce qui concerne l' utilisateur et le temps réel, python était presque 4 fois plus rapide que bc . Qu'est-ce qui aurait pu y aller. Je n'ai accordé aucune priorité aux processus en tant que tels. J'essaie de comprendre cette situation.

ganessh
la source
Donc, voulez-vous dire que le composant sys ne donne que le temps nécessaire aux charges et que le temps d'exécution sera donné dans le composant utilisateur de la sortie?
ganessh
Je ne suis vraiment pas sûr, c'est pourquoi j'ai posté un commentaire. C'est juste une supposition.
terdon
7
echo | bcimplique le lancement d'un sous-shell à cause du tube - c'est de là que vient probablement une partie de votre temps utilisateur supplémentaire. Pour en faire un test équitable, le script python doit lire à partir de stdin pour que vous puissiez time echo 6**6**6 | whatever.py.
goldilocks
1
Je préfère mettre la ligne de commande be dans un script et chronométrer l'exécution de cela. Ou utilisez echo 6^6^6 | time bc.
daniel kullmann
1
Remarque: en python, l' 6**6**6expression est réellement calculée au moment de la compilation . Cependant, puisque vous lancez le fichier directement au lieu de l'importer à partir d'un module, cela ne devrait pas avoir d'importance. Pour voir la différence insérée 10**12345678dans un a.pyfichier et essayer de l'importer depuis l'interpréteur interactif. Fermez ensuite l'interpréteur, redémarrez-le et importez à anouveau. La première fois, cela devrait prendre un temps considérable (car python compile le module), tandis que la deuxième fois qu'il charge le .pyc, qui devrait être instantané,
Bakuriu

Réponses:

25

Python importe un grand nombre de fichiers au démarrage:

% python -c 'import sys; print len(sys.modules)'
39

Chacun d'eux nécessite un nombre encore plus élevé de tentatives d'ouverture d'un fichier Python, car il existe de nombreuses façons de définir un module:

% python -vv -c 'pass'
# installing zipimport hook
import zipimport # builtin
# installed zipimport hook
# trying site.so
# trying sitemodule.so
# trying site.py
# trying site.pyc
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/sitemodule.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.py
# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.pyc matches /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.py
import site # precompiled from /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.pyc
# trying os.so
# trying osmodule.so
# trying os.py
# trying os.pyc
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/osmodule.so
# trying /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.py
# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.pyc matches /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.py
import os # precompiled from /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/os.pyc
    ...

Chaque "essai", à l'exception de ceux qui sont intégrés, nécessite un appel au niveau du système d'exploitation / système, et chaque "importation" semble déclencher environ 8 messages "d'essai". (Il y avait des moyens de réduire cela en utilisant zipimport, et chaque chemin dans votre PYTHONPATH peut nécessiter un autre appel.)

Cela signifie qu'il y a près de 200 appels système stat avant le démarrage de Python sur ma machine, et "time" affecte cela à "sys" plutôt qu'à "user", car le programme utilisateur attend que le système fasse des choses.

En comparaison, et comme l'a dit terdon, "bc" n'a pas un coût de démarrage aussi élevé. En regardant la sortie de dtruss (j'ai un Mac; "strace" pour un système d'exploitation basé sur Linux), je vois que bc ne fait aucun appel système open () ou stat (), sauf pour charger quelques partagés les bibliothèques sont le début, ce que Python fait bien sûr aussi. De plus, Python a plus de fichiers à lire avant d'être prêt à traiter quoi que ce soit.

L'attente du disque est lente.

Vous pouvez avoir une idée du coût de démarrage de Python en faisant:

time python -c pass

C'est 0,032s sur ma machine, tandis que 'print 6 ** 6 ** 6' est 0,072s, donc le coût de démarrage est 1 / 2ème du temps global et le calcul + conversion en décimal est l'autre moitié. Tandis que:

time echo 1 | bc

prend 0,005s, et "6 ^ 6 ^ 6" prend 0,184s donc l'exponentiation de bc est plus de 4x plus lente que celle de Python même si c'est 7x plus rapide pour commencer.

Andrew Dalke
la source
4
Vous avez un peu enterré le plomb là-bas. Vous voudrez peut-être déplacer le bit de fin vers le haut.
Riking
Juste par intérêt sur ma machine: time python -c 'pass' 0m0.025s, time python -c 'print 6 6 6' 0m0.087s mais time python -c 'x = 6 6 6' 0m0.028s Donc la plupart des l'heure sort le grand nombre.
Steve Barnes
Oui, la conversion en base 10 prend un temps quadratique en nombre de chiffres. Dans un cas extrême, essayez d'imprimer l'un des plus grands nombres premiers de Mersenne. C'est très rapide à calculer, mais il faut beaucoup de temps pour imprimer en base 10.
Andrew Dalke
11

J'ai trouvé une bonne réponse sur SO expliquant les différents domaines:

  • Le temps réel est l'horloge murale - le temps du début à la fin de l'appel. Il s'agit de tout le temps écoulé, y compris les tranches de temps utilisées par d'autres processus et le temps que le processus passe bloqué (par exemple, s'il attend que les E / S se terminent).

  • L'utilisateur est la quantité de temps CPU passé en code en mode utilisateur (en dehors du noyau) dans le processus. Il s'agit uniquement du temps CPU réel utilisé lors de l'exécution du processus. Les autres processus et le temps que le processus passe bloqué ne comptent pas dans ce chiffre.

  • Sys est la quantité de temps CPU passé dans le noyau au sein du processus. Cela signifie exécuter le temps CPU passé dans les appels système au sein du noyau, par opposition au code de bibliothèque, qui s'exécute toujours dans l'espace utilisateur. Comme «utilisateur», il s'agit uniquement du temps CPU utilisé par le processus. Voir ci-dessous pour une brève description du mode noyau (également appelé mode «superviseur») et du mécanisme d'appel système.

Ainsi, dans votre exemple spécifique, la version python est plus rapide en termes de temps réel nécessaire pour terminer. Cependant, l'approche python passe plus de temps dans l'espace noyau, faisant des appels aux fonctions du noyau. La bccommande ne passe pratiquement pas de temps dans l'espace du noyau et tout son temps est passé dans l'espace utilisateur, probablement en exécutant du bccode interne .

Cela ne fait aucune différence pour vous, la seule information qui vous intéresse vraiment realest le temps réel écoulé entre le lancement de la commande et l'obtention de sa sortie.

Vous devez également savoir que ces minuscules différences ne sont pas stables, elles dépendront également de la charge de votre système et changeront chaque fois que vous exécuterez la commande:

$ for i in {1..10}; do ( time python test.py > /dev/null ) 2>&1; done | grep user
user    0m0.056s
user    0m0.052s
user    0m0.052s
user    0m0.052s
user    0m0.060s
user    0m0.052s
user    0m0.052s
user    0m0.056s
user    0m0.048s
user    0m0.056s

$ for i in {1..10}; do ( time echo 6^6^6 | bc > /dev/null ) 2>&1; done | grep user
user    0m0.188s
user    0m0.188s
user    0m0.176s
user    0m0.176s
user    0m0.172s
user    0m0.176s
user    0m0.180s
user    0m0.172s
user    0m0.172s
user    0m0.172s
terdon
la source
10

Je vais l'expliquer sous un autre angle.

Pour être honnête, bca l'avantage car il n'a rien à lire sur le disque et n'a besoin que de ses blob / binaires tandis que python doit importer une série de modules + lire un fichier. Votre test pourrait donc être biaisé bc. Pour le tester, vous devez utiliser bc -q filefilecontient:

6^6^6
quit

Changer juste cela a modifié le temps d'utilisation echo:

bc  0.33s user 0.00s system 80% cpu 0.414 total

Pour utiliser le fichier:

bc -q some  0.33s user 0.00s system 86% cpu 0.385 total

(vous devrez utiliser la méthode de terdon pour remarquer de plus grandes différences, mais au moins nous savons qu'elles le sont)

Maintenant, du point de vue de python, python doit lire à partir du disque, compiler et exécuter à chaque fois le fichier, plus charger les modules en tant que points Andrew , ce qui rend le temps d'exécution plus lent. Si vous compilez le code octet du script python, vous remarquerez qu'il faut 50% moins de temps total pour exécuter le code:

python some.py > /dev/null  0.25s user 0.01s system 63% cpu 0.413 total

compilé:

./some.pyc  0.22s user 0.00s system 77% cpu 0.282 total

Comme vous pouvez le voir, plusieurs facteurs peuvent affecter l'exécution du temps entre différents outils.

Braiam
la source
3

J'ai eu l'avantage de lire les autres réponses. Pour commencer, les gens comme moi devraient savoir pourquoi nous avons affaire à un entier aussi énorme ici, c'est que les deux Pythonet bcfont une expansion d'exponentiation associative correcte , ce qui signifie que ce n'est pas 6^36nous évaluons mais plutôt 6^46656qui est considérablement plus grand. 1

En utilisant des variations sur les commandes suivantes, nous pouvons extraire une moyenne pour un élément spécifique de la sortie du timemot réservé et de la commande:

for i in {1..1000}; do (time echo 6^6^6 | bc > /dev/null) 2>&1; done | grep 'rea' | sed -e s/.*m// | awk '{sum += $1} END {print sum / NR}'

for i in {1..1000}; do (/usr/bin/time -v sh -c 'echo 6^6^6 | bc > /dev/null') 2>&1; done | grep 'Use' | sed -e s/.*:// | awk '{sum += $1} END {print sum / NR}'

Il est possible de suivre une autre voie et de supprimer entièrement le fichier de la comparaison. En outre, nous pouvons comparer le timing de bc avec quelque chose comme la dccommande, car historiquement le premier est un "processeur frontal" au second. Les commandes suivantes ont été chronométrées:

echo 6^6^6 | bc
echo 6 6 6 ^ ^ p | dc
echo print 6**6**6 | python2.7

Notez que la dccommande est associative à gauche pour l'exponentiation. 2

Nous avons quelques résultats avec time(bash) pour 1000 itérations (en secondes):

0.229678 real bc
0.228348 user bc
0.000569 sys bc
0.23306  real dc
0.231786 user dc
0.000395 sys dc
0.07 real python
0.065907 user python
0.003141 sys python

bcet dcoffrent des performances comparables dans ce contexte.

3 résultats moins précis de la commande /usr/bin/timeGNU time(la précision de l'échelle n'est pas valide ici mais les résultats sont similaires):

0.2224 user bc
0 sys bc
0.23 Elapsed bc
0.22998 user dc
0 sys dc
0.23 Elapsed dc
0.06008 user python
0 sys python
0.07 Elapsed python

Un avantage /usr/bin/timeest qu'il offre l' -voption qui fournit beaucoup plus d'informations qui pourraient éventuellement être utiles.

Il est également possible d'évaluer cela en interne pour ainsi dire avec le timeitmodule Python:

python2.7 -m timeit -n 1000 -r 1 'print 6**6**6' | grep 'loops'
1000 loops, best of 1: 55.4 msec per loop

C'est un peu plus rapide que ce que nous avons vu auparavant. Essayons l'interprète lui-même:

>>> import timeit
>>> import sys
>>> import os
>>> T = timeit.Timer("print 6**6**6")
>>> n = int(1000)
>>> f = open(os.devnull, 'w')
>>> sys.stdout = f
>>> t = t.timeit(n)
>>> sys.stdout = sys.__stdout__
>>> print t/n
0.0553743481636

C'est le plus rapide que j'ai vu.


Si nous évaluons une exponentiation moindre comme 6^6, alors la commande time donne des résultats surprenants - en utilisant les mêmes forcommandes de boucle que nous avons utilisées, nous avons maintenant:

0.001001 bc real
0.000304 user
0.000554 sys
0.014    python real i.e. 10x more than bc??
0.010432 user
0.002606 sys

Donc, avec un entier plus petit bcest tout d'un coup beaucoup plus rapide ?? Du redémarrage du système à la deuxième exécution, cela ne fait aucune différence. Pourtant, en même temps, si nous utilisons timeitpour Python, nous obtenons:

python2.7 -m timeit -n 100000 -r 1 'print 6**6' | grep loops  
100000 loops, best of 1: 0.468 usec per loop

Ce sont des microsecondes , pas des millisecondes, donc cela ne correspond pas aux résultats beaucoup plus lents utilisant la forboucle. Peut-être que d'autres outils sont nécessaires pour tester cela plus loin et comme d'autres l'ont expliqué, il y a plus que ce qui semble évident ici. Il semble que Python ait été plus rapide dans le scénario de la question, mais il n'est pas clair si des conclusions peuvent être tirées au-delà de cela ...


1. Inutile de dire que cela dépasse la portée de quelque chose comme l'expansion arithmétique de l'écho, c'est echo $((6**6**6))-à- dire bashqu'il se trouve également être associatif à droite pour cela, c'est-à-dire 6^6^6 = 6^(6^6).

2. Comparer avec ceci: 6 6 ^ 6 ^ p.

3. Il est possible que la commande GNU time fournisse plus d'informations lorsqu'elle est exécutée sur BSD UNIX (GNU time info document): la plupart des informations affichées par 'time' sont dérivées de l'appel système 'wait3'. Les nombres sont aussi bons que ceux retournés par 'wait3'. De nombreux systèmes ne mesurent pas toutes les ressources dont le «temps» peut rendre compte; ces ressources sont déclarées nulles. Les systèmes qui mesurent la plupart ou la totalité des ressources sont basés sur 4.2 ou 4.3BSD. Les versions ultérieures de BSD utilisent un code de gestion de mémoire différent qui mesure moins de ressources. - Sur les systèmes qui n'ont pas d'appel «wait3» qui renvoie des informations d'état, l'appel système «times» est utilisé à la place. Il fournit beaucoup moins d'informations que 'wait3', donc le temps de ces systèmes signale la plupart des ressources comme nulles.

Communauté
la source