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

Posted

技术标签:

【中文标题】numpy 怎么能比我的 Fortran 例程快得多?【英文标题】:How can numpy be so much faster than my Fortran routine? 【发布时间】:2016-02-16 20:47:20 【问题描述】:

我得到一个 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 例程中做了一些非常愚蠢的事情,以至于需要更长的时间?

编辑:

回答cmets中的问题:

是的,我还使用 32 位和 64 位浮点数运行了 Fortran 例程,但它对性能没有影响。 我使用了iso_fortran_env,它提供了 128 位浮点数。 使用 32 位浮点数虽然我的意思有点偏离,所以精度确实是个问题。 我以不同的顺序在不同的文件上运行了这两个例程,所以我猜在比较中缓存应该是公平的? 我实际上尝试过打开MP,但要同时从不同位置的文件中读取。阅读了您的 cmets 并回答了这听起来现在真的很愚蠢,而且这也使例行程序花费了更长的时间。我可能会尝试一下数组操作,但也许这甚至没有必要。 文件大小实际上是 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 又是如何避免这种情况的呢?

感谢到目前为止的所有帮助。

【问题讨论】:

您是否尝试过没有 128 位浮点数的 Fortran 例程?我不知道有任何硬件实际上支持这些,所以它们必须在软件中完成。 如果您尝试使用数组的 Fortran 版本(特别是使用一次读取而不是十亿次读取)会怎样? 您是否考虑过在 Fortran 中也使用数组运算符?然后,您可以尝试minval()maxval()sum()?此外,您将 IO 与 Fortran 中的操作混合在一起,而不是在 Python 中 - 这不是一个公平的比较;-) 在对涉及大文件的内容进行基准测试时,请确保所有运行的缓存都相同。 另请注意,精度在 Fortran 中是一个相当大的问题,而且它是有代价的。即使你用你的 Fortran 代码解决了所有这些明显的问题,也很可能需要额外的精度,并且会导致显着的速度损失。 【参考方案1】:

您的 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-norm SLANGE。运行时间甚至比使用单精度数组函数的方法更快 - 并且没有显示精度问题。

【讨论】:

为什么混合输入和计算会减慢速度?他们都必须阅读整个文件,这将是瓶颈。如果操作系统确实预读,则 Fortran 代码不必为 I/O 等待太多时间。 @Barmar 您仍然需要每次都检查数据是否在缓存中的函数调用开销和逻辑。【参考方案2】:

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 不,它没有。请参阅 Alexander Vogt 的回答和我对该问题的编辑。

以上是关于numpy 怎么能比我的 Fortran 例程快得多?的主要内容,如果未能解决你的问题,请参考以下文章

pandas 比 numpy 慢得多?

C++ 从 dll 调用 FORTRAN 子例程

调用 Fortran 例程的 C MPI 程序崩溃

由 R 调用时,Fortran 子例程不计算

如何在由 MPI 并行化的 fortran 中调用子例程?

使用 GCC 对模块内的 fortran 子例程进行外部命名