numpyはFortranルーチンよりもはるかに高速なのはなぜですか?


82

シミュレーション(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ビットのfloatで実行したので、どういうわけかそれをもっと信頼しています)から外れていますが、有効数字の7桁程度にすぎません。

どうしてnumpyはこんなに速くなるのでしょうか?つまり、これらの値を見つけるには、配列のすべてのエントリを調べる必要がありますよね?私はFortranルーチンで非常に愚かなことをしているので、もっと時間がかかりますか?

編集:

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

  • はい、32ビットと64ビットのfloatを使用してFortranルーチンを実行しましたが、パフォーマンスに影響はありませんでした。
  • iso_fortran_env128ビットのフロートを提供するものを使用しました。
  • 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これを回避できますか?

これまでのすべての助けに感謝します。


10
128ビットフロートなしでFortranルーチンを試しましたか?私は実際にそれらをサポートするハードウェアを知らないので、それらはソフトウェアで行われなければならないでしょう。
user2357112は、Monica 2015

4
配列を使用して(特に、10億ではなく1つの読み取りを使用して)Fortranバージョンを試してみるとどうなりますか?
francescalus 2015年

9
Fortranでも配列演算子を使用することを検討しましたか?その後、あなたは試みることができるminval()maxval()sum()?さらに、IOをFortranの操作と混合していますが、Pythonでは混合していません-これは公正な比較ではありません;-)
Alexander Vogt

4
大きなファイルを含むものをベンチマークするときは、すべての実行で同じようにキャッシュされていることを確認してください。
Tom Zych 2015年

1
また、Fortranでは精度がかなり重要であり、コストがかかることにも注意してください。Fortranコードでこれらの明らかな問題をすべて修正した後でも、追加の精度が必要になり、速度が大幅に低下する可能性があります。
ルアン2015年

回答:


110

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直接アレイ上。


精度の問題の場合:倍精度値を使用し、次のようにオンザフライで変換を実行するだけです。

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

これは、SLANGE行列列に単精度行列1ノルムを使用します。ランタイムは、単精度配列関数を使用するアプローチよりもさらに高速であり、精度の問題は示されていません。


4
入力と計算を混合すると、なぜそれほど遅くなるのですか?どちらもファイル全体を読み取る必要があり、それがボトルネックになります。また、OSが先読みを行う場合、FortranコードはI / Oをそれほど待つ必要はありません。
バーマー2015年

3
@Barmarデータが毎回キャッシュにあるかどうかをチェックするための関数呼び出しのオーバーヘッドとロジックがまだあります。
overv 2015年

55

Pythonではるかに効率的なコードを記述し(そしてnumpyバックエンドの多くは最適化されたFortranとCで記述されている)、Fortranでひどく非効率的なコードを記述したため、numpyはより高速です。

Pythonコードを見てください。配列全体を一度にロードしてから、配列を操作できる関数を呼び出します。

あなたのFortranコードを見てください。一度に1つの値を読み取り、それを使用していくつかの分岐ロジックを実行します。

不一致の大部分は、Fortranで記述した断片化されたIOです。

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

この方法で計算された平均は、numpy.mean呼び出しと同じ精度を取得しますか?私はそれについていくつか疑問があります。
バクリウ2015年

1
@バクリウいいえ、違います。AlexanderVogtの回答と質問に関する私の編集を参照してください。
user35915 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.