web-dev-qa-db-fra.com

Pourquoi 2 * x * x est-il plus rapide que 2 * (x * x) dans Python 3.x, pour les entiers?)

La multiplication suivante Python 3.x prend en moyenne entre 1,66 et 1,77:

import time
start_time = time.time()
num = 0
for x in range(0, 10000000):
    # num += 2 * (x * x)
    num += 2 * x * x
print("--- %s seconds ---" % (time.time() - start_time))

si je remplace 2 * x * x par 2 *(x * x), il faut compter entre 2.04 et 2.25. Comment venir?

En revanche, c'est l'inverse en Java: 2 * (x * x) est plus rapide en Java. Java: Pourquoi 2 * (i * i) est-il plus rapide que 2 * i * i en Java?

J'ai couru chaque version du programme 10 fois, voici les résultats.

   2 * x * x        |   2 * (x * x)
---------------------------------------
1.7717654705047607  | 2.0789272785186768
1.735931396484375   | 2.1166207790374756
1.7093875408172607  | 2.024367570877075
1.7004504203796387  | 2.047525405883789
1.6676218509674072  | 2.254328966140747
1.699510097503662   | 2.0949244499206543
1.6889283657073975  | 2.0841963291168213
1.7243537902832031  | 2.1290600299835205
1.712965488433838   | 2.1942825317382812
1.7622807025909424  | 2.1200053691864014
38
Waqas Gondal

Tout d’abord, notez que nous ne voyons pas la même chose dans Python 2.x:

>>> timeit("for i in range(1000): 2*i*i")
51.00784397125244
>>> timeit("for i in range(1000): 2*(i*i)")
50.48330092430115

Cela nous porte donc à croire que cela est dû à la façon dont les entiers ont changé dans Python 3: spécifiquement, Python 3 utilise long (arbitrairement grand entiers) partout.

Pour les assez petits nombres entiers (y compris ceux que nous considérons ici), CPython utilise en réalité le O(MN) algorithme de multiplication chiffre-école des chiffres) (pour les entiers plus grands passent à algorithme de Karatsuba ). Vous pouvez le voir vous-même dans source .

Le nombre de chiffres dans x*x Est environ le double de celui de 2*x Ou x (depuis log (x2) = 2 log (x)). Notez qu'un "chiffre" dans ce contexte n'est pas un chiffre de base 10, mais une valeur de 30 bits (qui sont traités comme des chiffres uniques dans l'implémentation de CPython). Par conséquent, 2 Est une valeur à un chiffre et x et 2*x Sont des valeurs à un chiffre pour toutes les itérations de la boucle, mais x*x Est égal à deux. -digit pour x >= 2**15. Par conséquent, pour x >= 2**15, 2*x*x Ne nécessite que des multiplications à un chiffre alors que 2*(x*x) nécessite un simple à un chiffre et un à deux chiffres. multiplication (puisque x*x a 2 chiffres de 30 bits).

Voici un moyen direct de voir ceci (Python 3):

>>> timeit("a*b", "a,b = 2, 123456**2", number=100000000)
5.796971936999967
>>> timeit("a*b", "a,b = 2*123456, 123456", number=100000000)
4.3559221399999615

Encore une fois, comparez ceci à Python 2, qui n'utilise pas d'entiers de longueur arbitraire partout:

>>> timeit("a*b", "a,b = 2, 123456**2", number=100000000)
3.0912468433380127
>>> timeit("a*b", "a,b = 2*123456, 123456", number=100000000)
3.1120400428771973

(Une remarque intéressante: si vous regardez la source, vous verrez que l’algorithme a en fait un cas particulier pour les nombres au carré (ce que nous faisons ici), mais cela ne suffit pas encore pour surmonter le fait que 2*(x*x) nécessite simplement de traiter plus de chiffres.)

28
arshajii

La représentation interne des entiers en Python est spéciale, elle utilise des slots de 30 bits:

In [6]: sys.getsizeof(2**30-1)
Out[6]: 28 # one slot + heading

In [7]: sys.getsizeof(2**30)
Out[7]: 32 # two slots 

Donc tout se passe comme si Python compte en base B = 2**30 = 1 073 741 824 ~1 billion.

Pour un humain qui veut calculer 2 * 4 * 4, deux manières:

  • (2 * 4) * 4 = 8 * 4 = 32 = 30 + 2 est immédiat si vous connaissez vos tables d'ajout.
  • 2 * (4 * 4) = 2 * 16 = 2 * 10 + 2 * 6 = (2 * 10 + 10) + 2 = 30 + 2 puisqu'il faut mettre l'opération à la baisse.

Python a le même problème. Si x est un nombre tel que 2x < B < x² , laisser x² = aB+b , avec a,b <B. est stocké dans deux emplacements, que j’ai noté (a|b). Les calculs conduisent à (sans gérer porte ici):

   (x*x)*2 =>  (a|b)*2 => (2*a|2*b)
   (2*x)*x =>  (2x)*x =>(2a|2b)

dans le premier cas, le 2* l'opération est effectuée deux fois, contre un seul dans le premier cas. Cela explique la différence.

7
B. M.

Si votre référence est correcte (sans vérification), cela peut venir du fait que Python entiers peuvent être deux choses différentes: des entiers natifs quand ils sont petits (avec un calcul rapide), et gros entiers quand ils augmentent en taille (calcul plus lent). La première syntaxe garde la taille plus petite après la première opération alors que la deuxième syntaxe peut conduire à deux opérations impliquant de grands entiers.

4
Thomas Baruchel

D'après ce que je peux dire, cela revient à un peu plus d'accès mémoire dans la version à l'aide de 2 * (x * x). J'ai imprimé le code binaire désassemblé et il semble prouver que:

Partie pertinente de 2 * x * x:

7          28 LOAD_FAST                1 (num)
           30 LOAD_CONST               3 (2)
           32 LOAD_FAST                2 (x)
           34 BINARY_MULTIPLY
           36 LOAD_FAST                2 (x)
           38 BINARY_MULTIPLY
           40 INPLACE_ADD
           42 STORE_FAST               1 (num)
           44 JUMP_ABSOLUTE           24

Partie pertinente de 2 * (x * x):

  7          28 LOAD_FAST                1 (num)
             30 LOAD_CONST               3 (2)
             32 LOAD_FAST                2 (x)
             34 LOAD_FAST                2 (x)
             36 BINARY_MULTIPLY                 <=== 1st multiply x*x in a temp value
             38 BINARY_MULTIPLY                 <=== then multiply result with 2
             40 INPLACE_ADD
             42 STORE_FAST               1 (num)
             44 JUMP_ABSOLUTE           24
2
Eric Fortin