web-dev-qa-db-fra.com

Pourquoi Python est si lent pour une simple boucle for?

Nous réalisons des implémentations kNN et SVD en Python. D'autres ont choisi Java. Nos temps d'exécution sont très différents. J'ai utilisé cProfile pour voir où je fais des erreurs mais tout est plutôt bien en fait. Oui, j'utilise aussi numpy. Mais je voudrais poser une question simple.

total = 0.0
for i in range(9999): # xrange is slower according 
    for j in range(1, 9999):            #to my test but more memory-friendly.
        total += (i / j)
print total

Cet extrait prend 31h40 sur mon ordinateur.

La version Java de ce code prend 1 seconde ou moins sur le même ordinateur. La vérification de type est un problème majeur pour ce code, je suppose. Mais je devrais faire beaucoup d’opérations comme celle-ci pour mon projet et je pense que 9999 * 9999 n’est pas un si gros chiffre.

Je pense que je fais des erreurs parce que je sais que Python est utilisé par de nombreux projets scientifiques. Mais pourquoi ce code est-il si lent et comment puis-je gérer des problèmes plus importants?

Devrais-je utiliser un compilateur JIT tel que Psyco?

MODIFIER

Je dis aussi que ce problème de boucle n’est qu’un exemple. Le code n'est pas aussi simple que cela et il peut être difficile de mettre en pratique vos améliorations/exemples de code.

Une autre question est: puis-je implémenter de nombreux algorithmes d'exploration de données et d'apprentissage automatique avec numpy et scipy si je l'utilise correctement? 

35
Baskaya

Je pense que je fais des erreurs parce que je sais que Python est utilisé par de nombreux projets scientifiques.

Ils utilisent beaucoup SciPy (NumPy étant le composant le plus important, mais l’écosystème développé autour de l’API de NumPy est encore plus important) qui accélère énormément tous les types d’opérations nécessaires à ces projets. Il y a ce que vous faites mal: vous n'écrivez pas votre code critique en C. Python est excellent pour le développement en général, mais des modules d'extension bien placés sont une optimisation vitale en soi (du moins lorsque vous ' recréer les chiffres). Python est un langage vraiment merdique pour implémenter des boucles internes serrées.

L'implémentation par défaut (et pour le moment la plus répandue et la plus largement prise en charge) est un interpréteur simple en bytecode. Même les opérations les plus simples, comme une division entière, peuvent prendre des centaines de cycles de traitement, plusieurs accès en mémoire (la vérification de type est un exemple courant), plusieurs appels de fonction C, etc. au lieu de quelques-uns (voire un seul, dans le cas d'entiers division) instruction. De plus, le langage est conçu avec de nombreuses abstractions qui ajoutent une surcharge. Votre boucle alloue 9999 objets sur le tas si vous utilisez xrange - beaucoup plus si vous utilisez range (9999 * 9999 entier moins environ 256 * 256 pour les petits entiers mis en cache). De plus, la version xrange appelle une méthode à chaque itération pour avancer; la version range le serait également si l'itération sur les séquences n'avait pas été optimisée spécifiquement. Cependant, il faut tout un envoi de bytecode, qui est lui-même extrêmement complexe (comparé à une division entière, bien sûr).

Il serait intéressant de voir ce qu'est une EJE (je recommanderais PyPy à Psyco, ce dernier n'est plus développé activement et sa portée est de toute façon très limitée - cela pourrait bien fonctionner pour cet exemple simple). Après une infime fraction d'itérations, il devrait produire une boucle de code machine quasi optimale complétée par quelques gardes - simples comparaisons d'entiers, sauts en cas d'échec - pour maintenir la correction au cas où vous auriez une chaîne dans cette liste. Java peut faire la même chose, mais plus tôt (il n’a pas besoin de tracer d’abord) et avec moins de gardes (du moins si vous utilisez ints). C'est pourquoi c'est tellement plus rapide.

31
user395760

Parce que vous parlez de code scientifique, jetez un oeil à numpy. Ce que vous faites a probablement déjà été fait (ou plutôt, il utilise LAPACK pour des choses comme SVD). Lorsque vous entendez parler de l'utilisation de python pour le code scientifique, les gens ne font probablement pas référence à son utilisation comme vous le faites dans votre exemple.

Comme exemple rapide:

(Si vous utilisez python3, votre exemple utilisera la division float. Mon exemple suppose que vous utilisez python2.x et donc la division entière. Sinon, spécifiez i = np.arange(9999, dtype=np.float), etc.)

import numpy as np
i = np.arange(9999)
j = np.arange(1, 9999)
print np.divide.outer(i,j).sum()

Pour donner une idée du timing ... (j'utiliserai ici la division en virgule flottante au lieu de la division entière comme dans votre exemple):

import numpy as np

def f1(num):
    total = 0.0
    for i in range(num): 
        for j in range(1, num):
            total += (float(i) / j)
    return total

def f2(num):
    i = np.arange(num, dtype=np.float)
    j = np.arange(1, num, dtype=np.float)
    return np.divide.outer(i, j).sum()

def f3(num):
    """Less memory-hungry (and faster) version of f2."""
    total = 0.0
    j = np.arange(1, num, dtype=np.float)
    for i in xrange(num):
        total += (i / j).sum()
    return total

Si on compare les timings:

In [30]: %timeit f1(9999)
1 loops, best of 3: 27.2 s per loop

In [31]: %timeit f2(9999)
1 loops, best of 3: 1.46 s per loop

In [32]: %timeit f3(9999)
1 loops, best of 3: 915 ms per loop
14
Joe Kington

L'avantage de Python est qu'il y a beaucoup plus de flexibilité (par exemple, les classes sont des objets) par rapport à Java (où vous n'avez que ce mécanisme de réflexion).

Ce qui n'est pas mentionné ici est Cython . Cela permet d'introduire des variables typées et de trans-compiler votre exemple en C/C++. Alors c'est beaucoup plus rapide. J'ai aussi changé les limites de la boucle ...

from __future__ import division

cdef double total = 0.00
cdef int i, j
for i in range(9999):
    for j in range(1, 10000+i):
        total += (i / j)

from time import time
t = time()
print("total = %d" % total)
print("time = %f[s]" % (time() - t))

Suivi par

$ cython loops.pyx
$ gcc -I/usr/include/python2.7 -shared -pthread -fPIC -fwrapv -Wall -fno-strict-aliasing -O3 -o loops.so loops.c
$ python -c "import loops"

donne

total = 514219068
time = 0.000047[s]
5
Harald Schilly

Vous constaterez que les compréhensions de liste ou les expressions de générateur sont nettement plus rapides. Par exemple:

total = sum(i / j for j in xrange(1, 9999) for i in xrange(9999))

Cela s’exécute en ~ 11 secondes sur ma machine contre ~ 26 pour votre code original. Toujours un ordre de grandeur plus lent que le Java, mais cela correspond plus à ce que vous attendez.

En passant, votre code original peut être légèrement accéléré en initialisant total à 0 plutôt que 0.0 pour utiliser un entier plutôt qu'une addition en virgule flottante. Vos divisions ont toutes des résultats entiers, il est donc inutile de faire la somme des résultats en un flottant.

Sur ma machine, Psyco a en fait ralentit les expressions du générateur à peu près à la même vitesse que votre boucle d'origine (dont il n'accélère pas du tout).

4
kindall

C'est un phénomène connu: le code python est dynamique et interprété, le code Java est typé et compilé de manière statique. Pas de surprises là-bas.

Les raisons invoquées par les gens pour préférer python sont souvent:

  • base de code plus petite
  • moins de redondance (plus de SEC)
  • code de nettoyage

Cependant, si vous utilisez une bibliothèque écrite en C (à partir de python), les performances peuvent être bien meilleures (comparez: pickle à cpickle).

4
Matt Fenwick

Utiliser la compréhension de la liste de kindall

total = sum(i / j for j in xrange(1, 9999) for i in xrange(9999))

est de 10,2 secondes et en utilisant pypy 1.7, il est de 2,5 secondes. C'est drôle parce que Pypy accélère la version originale à 2,5 secondes également. Donc, pour les listes pypy, la compréhension serait une optimisation prématurée;). Bon boulot pypy!

4
Pawel

Les boucles Python for sont statiquement typées et interprétées. Non compilé. Java est plus rapide car il dispose de fonctionnalités d’accélération JIT supplémentaires que Python ne possède pas.

 http://en.wikipedia.org/wiki/Just-in-time_compilation

Pour illustrer toute la différence que JIT fait avec Java, regardez ce programme python qui prend environ 5 minutes:

if __=='__main__':
    total = 0.0
    i=1
    while i<=9999:
        j=1
        while j<=9999:
            total=1
            j+=1
        i+=1
    print total

Bien que ce programme Java fondamentalement équivalent prenne environ 23 millisecondes:

public class Main{
    public static void main(String args[]){
        float total = 0f; 

        long start_time = System.nanoTime();
        int i=1;

        while (i<=9999){
            int j=1;
            while(j<=9999){
                total+=1;
                j+=1;
            }
            i+=1;
        }
        long end_time = System.nanoTime();

        System.out.println("total: " + total);
        System.out.println("total milliseconds: " + 
           (end_time - start_time)/1000000);
    }
}

Pour ce qui est de faire quelque chose dans une boucle for, Java nettoie l'horloge de Python en accélérant de 1 à 1000 ordres de grandeur. 

Morale de l'histoire: il faut absolument éviter le python de base pour les boucles si une performance rapide est requise. Cela pourrait être dû au fait que Guido van Rossum souhaite encourager les utilisateurs à utiliser des structures conviviales pour plusieurs processeurs, telles que l’épissage de tableaux, qui fonctionnent plus rapidement que Java.

3
spiderman

Faire des calculs scientifiques avec python signifie souvent utiliser un logiciel de calcul écrit en C/C++ dans les parties les plus cruciales, avec python comme langage de script interne, comme e.x. Sage (qui contient aussi beaucoup de code python).

Je pense que cela peut être utile: http://blog.dhananjaynene.com/2008/07/performance-comparison-c-Java-python-Ruby-jython-jruby-groovy/

Comme vous pouvez le constater, psyco/PyPy peut apporter une certaine amélioration, mais il serait probablement beaucoup plus lent que C++ ou Java. 

0
Krystian

Je ne sais pas si la recommandation a été faite, mais j'aime bien remplacer les boucles avec compréhension de liste. C'est plus rapide, plus propre et plus pythonique.

http://www.pythonforbeginners.com/basics/list-comprehensions-in-python

0
Jesse Watson