Подтвердить что ты не робот

Как 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-й значащей цифре или около того.

Как numpy может быть настолько быстрым? Я имею в виду, что вы должны смотреть на каждую запись массива, чтобы найти эти значения, не так ли? Я делаю что-то очень глупо в моей программе Fortran, чтобы она занимала намного больше времени?

EDIT:

Чтобы ответить на вопросы в комментариях:

  • Да, я также запускал подпрограмму Fortran с 32-битными и 64-битными поплавками, но не влиял на производительность.
  • Я использовал 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)
...

Решает проблему, но увеличивает вычислительное время (не очень, но заметно). Есть ли лучший способ обойти эту проблему? Я не мог найти способ прочитать синглы из файла непосредственно в парном разряде. И как numpy избежать этого?

Спасибо за всю помощь до сих пор.

4b9b3361

Ответ 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 за один раз. Затем я могу использовать функции MAXVAL, MINVAL и SUM непосредственно в массиве.


Для проблемы точности: просто используйте значения двойной точности и выполняйте преобразование "на лету" как

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 для матричных столбцов. Время выполнения еще быстрее, чем подход, использующий функции одиночной точности, и не показывает проблему точности.

Ответ 2

Целое число быстрее, потому что вы пишете гораздо более эффективный код в python (и большая часть базы данных numpy написана в оптимизированном Fortran и C) и ужасно неэффективный код в Fortran.

Посмотрите на свой код на Python. Вы загружаете весь массив сразу, а затем вызываете функции, которые могут работать с массивом.

Посмотрите на свой код fortran. Вы читаете одно значение за раз и делаете с ним некоторую ветвящуюся логику.

Большая часть вашего несоответствия - фрагментированный IO, написанный вами в Fortran.

Вы можете написать 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