web-dev-qa-db-ja.com

どうしてnumpyはFortranルーチンよりずっと速くできますか?

シミュレーション(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ルーチンで非常に長い時間がかかるように、非常に愚かなことをしていますか?

編集:

コメントの質問に答えるには:

  • はい、Fortranルーチンを32ビットと64ビットの浮動小数点数で実行しましたが、パフォーマンスには影響しませんでした。
  • iso_fortran_env 128ビットの浮動小数点数を提供します。
  • ただし、32ビットの浮動小数点数を使用すると、平均値がかなりずれてしまうため、精度が本当に問題になります。
  • 私は両方のルーチンを異なるファイルで異なる順序で実行したので、私が推測する比較ではキャッシュは公平だったはずですか?
  • 私は実際にオープンMPを試しましたが、同時に異なる位置のファイルから読み取りました。あなたのコメントと回答を読んだことは、今では本当にばかげているように聞こえます。配列操作を試してみるかもしれませんが、それは必要ないかもしれません。
  • ファイルのサイズは実際には1/2Gです。これはタイプミスでした、ありがとう。
  • ここで、配列の実装を試します。

編集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はどのようにこれを回避しますか?

これまでのすべての助けてくれてありがとう。

80
user35915

Fortranの実装には、2つの大きな欠点があります。

  • IOと計算(およびエントリごとにファイルから読み取る)とを組み合わせます。
  • ベクトル/行列演算を使用しません。

この実装はあなたのものと同じ操作を実行し、私のマシンでは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に一度に読み込むことです。次に、関数 MAXVALMINVAL 、および 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 を使用します。ランタイムは、単精度の配列関数を使用するアプローチよりもさらに高速であり、精度の問題を示していません。

110
Alexander Vogt

より効率的なコードを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
55
casey