NumPy为什么比我的Fortran程序快那么多?

84
我得到了一个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)

这在我使用的机器上每个文件大约需要 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)

现在,我当然希望这会更快,但我真的很惊讶。在相同的条件下,它只需要不到一秒钟的时间。平均值与我的Fortran例程发现的值有所偏差(我还使用128位浮点数运行了它,所以我更加信任它),但只有第7个有效数字左右。
numpy怎么能这么快?我的意思是,你必须查看数组的每个条目才能找到这些值,对吗?我的Fortran例程需要花费如此长的时间,我是否做了一些非常愚蠢的事情?
编辑:
回答评论中的问题:
  • 是的,我也使用32位和64位浮点数运行了Fortran例程,但对性能没有影响。
  • 我使用了iso_fortran_env,它提供了128位浮点数。
  • 使用32位浮点数时,我的平均值相差很大,所以精度确实是一个问题。
  • 我在不同的文件上以不同的顺序运行了两个例程,所以缓存应该在比较中是公平的,我猜?
  • 我实际上尝试了open 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)
...

解决了问题,但会增加计算时间(不是很多,但还是可以明显感觉到)。 有没有更好的方法来解决这个问题?我找不到一种将单精度直接读取为双精度的方法。 numpy 是如何避免这个问题的? 非常感谢您迄今为止所提供的所有帮助。

10
你有尝试过使用不需要128位浮点数的Fortran程序吗?我不知道是否有任何硬件实际支持这些,所以它们必须在软件中完成。 - user2357112
4
如果您尝试使用数组(特别是只读取一次而不是十亿次)来运行Fortran版本,会怎样呢? - francescalus
9
你是否考虑在Fortran中也使用数组运算符?这样,你可以尝试minval()maxval()sum()函数。此外,你在Fortran中混合了I/O操作和计算,但在Python中没有,这不是一个公平的比较;-) - Alexander Vogt
4
当对涉及大文件的某个东西进行基准测试时,确保所有运行都缓存了相同的内容。 - Tom Zych
1
你可以告诉numpy在计算平均值时使用双精度累加器:mean=numpy.mean(mmap, dtype=np.float64) - credondo
显示剩余7条评论
2个回答

114

您的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

这个想法是将整个文件一次性读入到一个数组tmp中。然后,我可以直接在该数组上使用函数MAXVALMINVALSUM


对于精度问题:只需使用双精度值并在运行时进行转换即可。

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-范数SLANGE在矩阵列上。运行时甚至比使用单精度数组函数的方法更快 - 而且不显示精度问题。


4
为什么将输入和计算混合在一起会大大减慢速度?因为两者都需要读取整个文件,这会成为瓶颈。如果操作系统进行预读取,则Fortran代码不应该对I/O等待太久。 - Barmar
3
每次仍然需要函数调用开销和检查数据是否在缓存中的逻辑。 - Overv

59
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

用这种方式计算的平均值是否获得与 numpy.mean 调用相同的精度?我对此有些怀疑。 - Bakuriu
1
@Bakuriu 不,它不会。请参阅Alexander Vogt的答案以及我对问题的编辑。 - user35915

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接