Numpy 怎么可能比我的 Fortran 例程快这么多?

我得到了一个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个有效数字左右。

麻木怎么会这么快?我的意思是你必须查看数组的每个条目才能找到这些值,对吗?我是不是在 Fortran 例程中做了什么很蠢的事情,以至于花了这么长时间?

编辑:

回答评论中的问题:

  • 是的,我还用32位和64位浮点运行了 Fortran 例程,但它对性能没有影响。
  • 我使用了提供128位浮点数的 iso_fortran_env
  • 使用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)
...

解决了这个问题,但是增加了计算时间(不是很多,但是很明显)。 有没有更好的办法来解决这个问题?我找不到一种方法直接从文件中读取单个文件到双个文件。 那么 numpy如何避免这种情况呢?

谢谢你到目前为止的帮助。

11159 次浏览

您的 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。运行时甚至比使用单精度数组函数的方法还要快,而且没有显示精度问题。

Numpy 更快,因为你用 python 编写的代码效率更高(而 numpy 的后端大部分是用优化过的 Fortran 和 c 编写的) ,而在 Fortran 编写的代码效率非常低。

看看你的 Python 代码。您可以立即加载整个数组,然后调用可以对数组进行操作的函数。

查看 fortran 代码,每次读取一个值并使用它执行一些分支逻辑。

你的大部分差异都是因为你在 Fortran 写的不完整的书面许可。

编写 Fortran 的方式与编写 python 的方式几乎相同,您会发现这种方式运行速度要快得多。

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