numpy autour / rint lent par rapport à astype (int)

Donc, si j’ai quelque chose comme x=np.random.rand(60000)*400-200 . Le %timeit dit:

  • x.astype(int) prend 0.14ms
  • np.rint(x) et np.around(x) prennent 1,01 ms

Notez que dans les cas rint et around , vous devez toujours dépenser les 0,14 ms supplémentaires pour créer un astype(int) final astype(int) (en supposant que ce que vous souhaitiez au final).

Question: ai-je raison de penser que le matériel le plus moderne est capable d’effectuer les deux opérations dans le même temps? Si tel est le cas, pourquoi Numpy prend-il 8 fois plus de temps pour l’arrondi?

En l’occurrence, je ne suis pas très pointilleux quant à l’exactitude de l’arithmétique, mais je ne vois pas comment en tirer parti avec numpy (je fais de la biologie compliquée, pas de la physique des particules).

np.around(x).astype(int) et x.astype(int) ne produisent pas les mêmes valeurs. Le premier arrondit même (c’est la même chose que ((x*x>=0+0.5) + (x*x<0-0.5)).astype(int) ) alors que le dernier arrondit à zéro. cependant,

 y = np.trunc(x).astype(int) z = x.astype(int) 

montre y==z mais le calcul de y est beaucoup plus lent. Ce sont donc les fonctions np.trunc et np.around qui sont lentes.

 In [165]: x.dtype Out[165]: dtype('float64') In [168]: y.dtype Out[168]: dtype('int64') 

Donc np.trunc(x) arrondit vers zéro de double à double. Alors astype(int) doit convertir double en int64.

En interne, je ne sais pas ce que font python ou numpy, mais je sais comment je ferais cela en C. Parlons de certains matériels. Avec SSE4.1, il est possible de faire des rondes, des planchers, des plafonds et des tronçons de double à double en utilisant:

 _mm_round_pd(a, 0); //round: round even _mm_round_pd(a, 1); //floor: round towards minus infinity _mm_round_pd(a, 2); //ceil: round towards positive infinity _mm_round_pd(a, 3); //trunc: round towards zero 

mais numpy doit également prendre en charge les systèmes sans SSE4.1, de sorte qu'il devrait être construit sans SSE4.1 ainsi qu'avec SSE4.1, puis utiliser un répartiteur.

Mais faire cela du double directement à int64 en utilisant SSE / AVX n’est pas efficace avant AVX512. Cependant, il est possible d'arrondir double à int32 efficacement en utilisant uniquement SSE2:

 _mm_cvtpd_epi32(a); //round double to int32 then expand to int64 _mm_cvttpd_epi32(a); //trunc double to int32 then expand to int64 

Ceci convertit deux doubles en deux int64.

Dans votre cas, cela fonctionnerait bien puisque la plage est certainement dans int32. Mais à moins que python ne sache que la plage s’intègre dans int32, elle ne peut pas en déduire qu’elle devrait donc arrondir ou tronquer à int64, ce qui est lent. De plus, encore une fois, numpy devrait construire pour prendre en charge SSE2.

Mais vous auriez peut-être pu utiliser un seul tableau à virgule flottante pour commencer. Dans ce cas, vous auriez pu faire:

 _mm_cvtps_epi32(a); //round single to int32 _mm_cvttps_epi32(a) //trunc single to int32 

Ceux-ci convertissent quatre singles en quatre int32.

Donc, pour répondre à votre question, SSE2 peut arrondir ou tronquer efficacement de double à int32. AVX512 sera en mesure d'arrondir ou de tronquer de double à int64 de manière efficace en utilisant _mm512_cvtpd_epi64(a) ou _mm512_cvttpd_epi64(a) . SSE4.1 peut arrondir / tronquer / sol / plafond de flotteur à flotteur ou doubler efficacement.

Comme l’a souligné @jme dans les commentaires, les fonctions rint et around doivent déterminer s’il faut arrondir les fractions à l’entier supérieur ou inférieur. En revanche, la fonction astype sera toujours arrondie afin de pouvoir supprimer immédiatement les informations décimales. Un certain nombre d’autres fonctions font la même chose. En outre, vous pouvez améliorer la vitesse en utilisant un nombre inférieur de bits pour l’entier. Cependant, vous devez faire attention à pouvoir intégrer toute la gamme de vos données d’entrée.

 %%timeit np.int8(x) 10000 loops, best of 3: 165 µs per loop 

Notez que cela ne stocke pas les valeurs en dehors de la plage -128 à 127 car il s’agit de 8 bits. Certaines valeurs de votre exemple ne sont pas comsockets dans cette plage.

Parmi tous les autres que j’ai essayés, np.intc semble être le plus rapide:

 %%timeit np.int16(x) 10000 loops, best of 3: 186 µs per loop %%timeit np.intc(x) 10000 loops, best of 3: 169 µs per loop %%timeit np.int0(x) 10000 loops, best of 3: 170 µs per loop %%timeit np.int_(x) 10000 loops, best of 3: 188 µs per loop %%timeit np.int32(x) 10000 loops, best of 3: 187 µs per loop %%timeit np.trunc(x) 1000 loops, best of 3: 940 µs per loop 

Vos exemples, sur ma machine:

 %%timeit np.around(x) 1000 loops, best of 3: 1.48 ms per loop %%timeit np.rint(x) 1000 loops, best of 3: 1.49 ms per loop %%timeit x.astype(int) 10000 loops, best of 3: 188 µs per loop