シミュレーション(Fortranで作成)から温度分布を表す512 ^ 3配列を取得します。配列は、サイズが約1/2Gのバイナリファイルに格納されます。この配列の最小値、最大値、平均値を知る必要があり、いずれにしてもFortranコードをすぐに理解する必要があるので、試してみることにし、次の非常に簡単なルーチンを思いつきました。
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)
これには、使用するマシン上のファイルごとに約25秒かかります。それはかなり長いと感じたので、先に進んで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)
今、私はもちろんこれがもっと速くなると思っていましたが、本当に吹き飛ばされました。同一の条件下では1秒もかかりません。平均は、Fortranルーチンが見つけたもの(128ビットの浮動小数点数で実行したため、どういうわけかそれをより信頼しています)とは異なりますが、有効桁数は7桁程度です。
どうしてnumpyはそんなに速くできますか?これらの値を見つけるには、配列のすべてのエントリを調べる必要がありますよね? Fortranルーチンで非常に長い時間がかかるように、非常に愚かなことをしていますか?
編集:
コメントの質問に答えるには:
iso_fortran_env
128ビットの浮動小数点数を提供します。編集2:
@Alexander Vogtと@caseyが提案したものを実装しましたが、numpy
と同じくらい高速ですが、@ Luaanが指摘したように精度の問題があります。 32ビットのfloat配列を使用すると、sum
で計算される平均は20%オフになります。やること
...
real,allocatable :: tmp (:,:,:)
double precision,allocatable :: tmp2(:,:,:)
...
tmp2=tmp
mean=sum(tmp2)/size(tmp)
...
この問題は解決しますが、計算時間は増加します(大幅ではありませんが、顕著です)。この問題を回避するより良い方法はありますか?ファイルからシングルを直接ダブルに読み込む方法が見つかりませんでした。 numpy
はどのようにこれを回避しますか?
これまでのすべての助けてくれてありがとう。
Fortranの実装には、2つの大きな欠点があります。
この実装はあなたのものと同じ操作を実行し、私のマシンでは20倍高速です:
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
アイデアは、ファイル全体を1つの配列tmp
に一度に読み込むことです。次に、関数 MAXVAL
、 MINVAL
、および SUM
onを使用できます。配列を直接。
精度の問題:単に倍精度値を使用し、その場で変換を実行する
mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))
計算時間がわずかに増加するだけです。要素単位およびスライス単位で操作を実行しようとしましたが、デフォルトの最適化レベルで必要な時間を増やすだけでした。
-O3
、要素単位の加算は、配列演算よりも約3%優れています。倍精度と単精度の操作の違いは、私のマシンでは平均2%未満です(個々の実行ははるかに逸脱しています)。
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
これは、行列列で単精度行列1-norm SLANGE
を使用します。ランタイムは、単精度の配列関数を使用するアプローチよりもさらに高速であり、精度の問題を示していません。
より効率的なコードをpython(およびnumpyバックエンドの多くは最適化されたFortranおよびCで記述されています)およびFortranでひどく非効率的なコードを記述したため、numpyは高速です。
pythonコードを見てください。一度に配列全体をロードしてから、配列を操作できる関数を呼び出します。
Fortranコードを見てください。一度に1つの値を読み取り、それを使用して分岐ロジックを実行します。
不一致の大部分は、Fortranで記述した断片化されたIOです。
Fortranは、pythonを書いたのとほぼ同じ方法で記述できます。Fortranの方がはるかに高速に実行できます。
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