numpy怎么能比我的Fortran例程快得多?

use*_*915 81 python arrays performance fortran numpy

我得到一个512 ^ 3数组,表示模拟的温度分布(用Fortran编写).该数组存储在大小约为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)
Run Code Online (Sandbox Code Playgroud)

在我使用的机器上,每个文件大约需要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)
Run Code Online (Sandbox Code Playgroud)

现在,我预计这会更快,但我真的被吹走了.在相同条件下只需不到一秒钟.平均值偏离我的Fortran例程发现的那个(我也使用128位浮点运行,所以我不知何故更多地信任它),但仅限于第7位有效数字左右.

numpy怎么这么快?我的意思是你必须查看数组的每个条目才能找到这些值,对吧?我在Fortran程序中做了一件非常愚蠢的事情,因为它花了这么长时间吗?

编辑:

要回答评论中的问题:

  • 是的,我也使用32位和64位浮点运行Fortran例程,但它对性能没有影响.
  • 我用过iso_fortran_env它提供128位浮点数.
  • 使用32位浮点数我的意思是相当多,所以精度确实是一个问题.
  • 我以不同的顺序在不同的文件上运行这两个例程,所以缓存在比较中应该是公平的吗?
  • 我实际上试过打开MP,但同时从不同位置的文件中读取.阅读完你的评论和答案后,这听起来真的很愚蠢,它使常规也需要更长的时间.我可能试一试数组操作,但也许甚至不需要.
  • 文件实际上是1/2G大小,这是一个错字,谢谢.
  • 我现在将尝试数组实现.

编辑2:

我实现了@Alexander Vogt和@casey在他们的答案中提出的建议,它的速度一样快,numpy但现在我有一个精确的问题,因为@Luaan指出我可能会得到.使用32位浮点数组,计算出的平均值sum为20%.干

...
real,allocatable :: tmp (:,:,:)
double precision,allocatable :: tmp2(:,:,:)
...
tmp2=tmp
mean=sum(tmp2)/size(tmp)
...
Run Code Online (Sandbox Code Playgroud)

解决了这个问题但增加了计算时间(不是很多,但显着).有没有更好的方法来解决这个问题?我找不到从文件中直接读单打的方法.怎么numpy避免这个?

感谢目前为止所有的帮助.

Ale*_*ogt 110

您的Fortran实现存在两个主要缺点:

  • 您混合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
Run Code Online (Sandbox Code Playgroud)

我们的想法是将整个文件一次性读入一个数组tmp.然后,我可以使用的功能MAXVAL,MINVALSUM在阵列上直接.


对于准确性问题:只需使用双精度值并即时进行转换

mean = sum(real(tmp, kind=kind(1.d0)))/real(gridsize**3, kind=kind(1.d0))
Run Code Online (Sandbox Code Playgroud)

只是略微增加了计算时间.我尝试在切片中执行元素操作,但这只会增加默认优化级别所需的时间.

-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
Run Code Online (Sandbox Code Playgroud)

SLANGE在矩阵列上使用单精度矩阵1范数.运行时甚至比使用单精度数组函数的方法更快 - 并且没有显示精度问题.

  • 为什么混合输入和计算会减慢这么多?他们都必须读取整个文件,这将成为瓶颈.如果操作系统确实预读,则Fortran代码不必等待I/O. (4认同)
  • @Barmar你仍然会有函数调用开销和逻辑来检查数据是否每次都在缓存中. (3认同)

cas*_*sey 55

numpy更快,因为你在python中编写了更高效的代码(并且大部分numpy后端是用优化的Fortran和C编写的)和Fortran中非常低效的代码.

看看你的python代码.您立即加载整个数组,然后调用可以在阵列上运行的函数.

看看你的fortran代码.您一次读取一个值并使用它执行一些分支逻辑.

您的大部分差异是您在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
Run Code Online (Sandbox Code Playgroud)