web-dev-qa-db-fra.com

Comment numpy peut-il être tellement plus rapide que ma routine Fortran?

J'obtiens un tableau 512 ^ 3 représentant une distribution de température à partir d'une simulation (écrit en Fortran). Le tableau est stocké dans un fichier binaire d'environ 1/2G. J'ai besoin de connaître le minimum, le maximum et la moyenne de ce tableau et comme je devrai bientôt comprendre le code Fortran de toute façon, j'ai décidé de l'essayer et j'ai proposé la routine très simple suivante.

  integer gridsize,unit,j
  real mini,maxi
  double precision mean

  gridsize=512
  unit=40
  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp
  mini=tmp
  maxi=tmp
  mean=tmp
  do j=2,gridsize**3
      read(unit=unit) tmp
      if(tmp>maxi)then
          maxi=tmp
      elseif(tmp<mini)then
          mini=tmp
      end if
      mean=mean+tmp
  end do
  mean=mean/gridsize**3
  close(unit=unit)

Cela prend environ 25 secondes par fichier sur la machine que j'utilise. Cela m'a semblé assez long et j'ai donc continué et j'ai fait ce qui suit en Python:

    import numpy

    mmap=numpy.memmap('T.out',dtype='float32',mode='r',offset=4,\
                                  shape=(512,512,512),order='F')
    mini=numpy.amin(mmap)
    maxi=numpy.amax(mmap)
    mean=numpy.mean(mmap)

Maintenant, je m'attendais à ce que ce soit plus rapide bien sûr, mais j'étais vraiment époustouflé. Cela prend moins d'une seconde dans des conditions identiques. La moyenne s'écarte de celle trouvée par ma routine Fortran (que j'ai également exécutée avec des flottants 128 bits, donc je lui fais plus confiance) mais uniquement au 7e chiffre significatif.

Comment numpy peut-il être si rapide? Je veux dire que vous devez regarder chaque entrée d'un tableau pour trouver ces valeurs, non? Suis-je en train de faire quelque chose de très stupide dans ma routine Fortran pour que ça prenne beaucoup plus de temps?

MODIFIER:

Pour répondre aux questions dans les commentaires:

  • Oui, j'ai également exécuté la routine Fortran avec des flottants 32 bits et 64 bits, mais cela n'a eu aucun impact sur les performances.
  • J'ai utilisé iso_fortran_env qui fournit des flottants de 128 bits.
  • Cependant, en utilisant des flottants 32 bits, ma moyenne est assez faible, donc la précision est vraiment un problème.
  • J'ai exécuté les deux routines sur des fichiers différents dans un ordre différent, donc la mise en cache aurait dû être juste dans la comparaison, je suppose?
  • J'ai en fait essayé d'ouvrir MP, mais de lire le fichier à différentes positions en même temps. Après avoir lu vos commentaires et réponses, cela semble vraiment stupide maintenant et cela a également rendu la routine beaucoup plus longue. Je pourrais essayer les opérations de la baie, mais ce ne sera peut-être même pas nécessaire.
  • Les fichiers sont en fait de taille 1/2G, c'était une faute de frappe, merci.
  • Je vais essayer l'implémentation du tableau maintenant.

EDIT 2:

J'ai implémenté ce que @Alexander Vogt et @casey ont suggéré dans leurs réponses, et c'est aussi rapide que numpy mais maintenant j'ai un problème de précision comme @Luaan a souligné que je pourrais obtenir. En utilisant un tableau flottant 32 bits, la moyenne calculée par sum est de 20%. Faire

...
real,allocatable :: tmp (:,:,:)
double precision,allocatable :: tmp2(:,:,:)
...
tmp2=tmp
mean=sum(tmp2)/size(tmp)
...

Résout le problème mais augmente le temps de calcul (pas beaucoup, mais sensiblement). Existe-t-il un meilleur moyen de contourner ce problème? Je ne pouvais pas trouver un moyen de lire des singles du fichier directement en double. Et comment numpy évite-t-il cela?

Merci pour toute l'aide jusqu'ici.

80
user35915

Votre implémentation Fortran souffre de deux lacunes majeures:

  • Vous mélangez IO et les calculs (et lisez le fichier entrée par entrée).
  • Vous n'utilisez pas d'opérations vectorielles/matricielles.

Cette implémentation effectue la même opération que la vôtre et est plus rapide d'un facteur 20 sur ma machine:

program test
  integer gridsize,unit
  real mini,maxi,mean
  real, allocatable :: tmp (:,:,:)

  gridsize=512
  unit=40

  allocate( tmp(gridsize, gridsize, gridsize))

  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp

  close(unit=unit)

  mini = minval(tmp)
  maxi = maxval(tmp)
  mean = sum(tmp)/gridsize**3
  print *, mini, maxi, mean

end program

L'idée est de lire le fichier entier dans un tableau tmp en une seule fois. Ensuite, je peux utiliser les fonctions MAXVAL , MINVAL , et SUM sur le tableau directement.


Pour le problème de précision: utilisez simplement des valeurs de double précision et effectuez la conversion à la volée comme

mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))

n'augmente que légèrement le temps de calcul. J'ai essayé d'effectuer l'opération par élément et par tranches, mais cela n'a fait qu'augmenter le temps requis au niveau d'optimisation par défaut.

À -O3, l'addition élément par élément est de 3% meilleure que l'opération tableau. La différence entre les opérations double précision et simple précision est inférieure à 2% sur ma machine - en moyenne (les parcours individuels s'écartent de beaucoup plus).


Voici une implémentation très rapide avec LAPACK:

program test
  integer gridsize,unit, i, j
  real mini,maxi
  integer  :: t1, t2, rate
  real, allocatable :: tmp (:,:,:)
  real, allocatable :: work(:)
!  double precision :: mean
  real :: mean
  real :: slange

  call system_clock(count_rate=rate)
  call system_clock(t1)
  gridsize=512
  unit=40

  allocate( tmp(gridsize, gridsize, gridsize), work(gridsize))

  open(unit=unit,file='T.out',status='old',access='stream',&
       form='unformatted',action='read')
  read(unit=unit) tmp

  close(unit=unit)

  mini = minval(tmp)
  maxi = maxval(tmp)

!  mean = sum(tmp)/gridsize**3
!  mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))
  mean = 0.d0
  do j=1,gridsize
    do i=1,gridsize
      mean = mean + slange('1', gridsize, 1, tmp(:,i,j),gridsize, work)
    enddo !i
  enddo !j
  mean = mean / gridsize**3

  print *, mini, maxi, mean
  call system_clock(t2)
  print *,real(t2-t1)/real(rate)

end program

Celui-ci utilise la norme 1 de matrice simple précision SLANGE sur les colonnes de la matrice. Le temps d'exécution est encore plus rapide que l'approche utilisant des fonctions de tableau à précision simple - et ne montre pas le problème de précision.

110
Alexander Vogt

Le numpy est plus rapide car vous avez écrit du code beaucoup plus efficace en python (et une grande partie du backend numpy est écrit en Fortran et C optimisé) et du code terriblement inefficace en Fortran.

Regardez votre code python. Vous chargez le tableau entier à la fois, puis appelez des fonctions qui peuvent fonctionner sur un tableau.

Regardez votre code fortran. Vous lisez une valeur à la fois et faites une logique de branchement avec.

La majorité de votre divergence est le fragment IO que vous avez écrit en Fortran.

Vous pouvez écrire le Fortran à peu près de la même manière que vous avez écrit le python et vous verrez qu'il fonctionne beaucoup plus rapidement de cette façon.

program test
  implicit none
  integer :: gridsize, unit
  real :: mini, maxi, mean
  real, allocatable :: array(:,:,:)

  gridsize=512
  allocate(array(gridsize,gridsize,gridsize))
  unit=40
  open(unit=unit, file='T.out', status='old', access='stream',&
       form='unformatted', action='read')
  read(unit) array    
  maxi = maxval(array)
  mini = minval(array)
  mean = sum(array)/size(array)
  close(unit)
end program test
55
casey