numpy如何比我的Fortran例程快得多?


82

我从仿真中得到了一个代表温度分布的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位浮点数的意思还是有很多,所以精度确实是个问题。
  • 我在不同的文件上以不同的顺序运行了这两个例程,所以在比较中,缓存应该是公平的。
  • 我实际上尝试过打开MP,但是要同时从不同位置读取文件。阅读您的评论和答案现在听起来真的很愚蠢,这也使例程花费了更长的时间。我可以尝试一下数组操作,但也许甚至没有必要。
  • 这些文件实际上是1 / 2G大小,这是一个错字,谢谢。
  • 我现在将尝试数组实现。

编辑2:

我实现了@Alexander Vogt和@casey在他们的答案中建议的内容,它的速度numpy与@Luaan指出的可能一样快,但是现在我遇到了精度问题。使用32位浮点数组时,由计算的平均值将减少sum20%。在做

...
real,allocatable :: tmp (:,:,:)
double precision,allocatable :: tmp2(:,:,:)
...
tmp2=tmp
mean=sum(tmp2)/size(tmp)
...

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

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


10
您是否尝试了不带128位浮点数的Fortran例程?我不知道实际支持这些硬件的任何硬件,因此必须在软件中完成。
user2357112支持Monica15年

4
如果您尝试使用数组(尤其是使用读取而不是十亿读取)的Fortran版本,该怎么办?
francescalus

9
您是否考虑过在Fortran中使用数组运算符?然后,你可以尝试minval()maxval()sum()?此外,您将IO与Fortran中的操作混合在一起,而不是在Python中混合-这是不公平的比较;-)
Alexander Vogt 2015年

4
在对涉及大文件的内容进行基准测试时,请确保所有运行都将其缓存为相同的内容。
Tom Zych

1
还要注意,在Fortran中,精度是相当大的事情,而且要付出一定的代价。即使用Fortran代码解决了所有这些明显的问题,也很可能需要额外的精度,并且会导致明显的速度损失。
a安2015年

Answers:


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

想法是一次性将整个文件读入一个数组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

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


4
为什么将输入与计算混合会大大降低它的速度?他们俩都必须读取整个文件,这将成为瓶颈。而且,如果操作系统确实进行了预读,则Fortran代码不必为I / O等待太多时间。
Barmar

3
@Barmar您仍将具有函数调用开销和逻辑,用于每次都检查数据是否在缓存中。
2015年

55

numpy的速度更快,因为您使用python编写了效率更高的代码(并且许多numpy后端都是用优化的Fortran和C编写的)和效率极低的代码。

查看您的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电话吗?我对此有些怀疑。
2015年

1
@Bakuriu不,不是。请参阅Alexander Vogt的答案和我对该问题的编辑。
user35915
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.