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:
iso_fortran_env
qui fournit des flottants de 128 bits.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.
Votre implémentation Fortran souffre de deux lacunes majeures:
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.
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