Як може Numpy бути набагато швидшим за мою рутину Fortran?


82

Я отримую масив 512 ^ 3, що представляє розподіл температури від моделювання (написаний у Фортрані). Масив зберігається у двійковому файлі розміром приблизно 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)

Зараз, я очікував, що це буде швидше, звичайно, але мене справді обдурило. Це займає менше секунди за однакових умов. Середнє значення відхиляється від знайденого моєю рутинною фортранською формою (яку я також використовував із 128-бітовими плаваючими знаками, тому я чомусь йому більше довіряю), але лише на 7-му значущому значенні чи близько того.

Як може Numpy бути таким швидким? Я маю на увазі, що вам потрібно переглянути кожен запис масиву, щоб знайти ці значення, чи не так? Я роблю щось дуже дурне у своїй рутині Фортрана, щоб це зайняло набагато більше часу?

РЕДАГУВАТИ:

Щоб відповісти на запитання в коментарях:

  • Так, я також запускав процедуру 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уникнути?

Дякуємо за всю допомогу.


10
Ви пробували процедуру Fortran без 128-розрядних плаваючих даних? Мені невідомо жодне обладнання, яке насправді їх підтримує, тому їх потрібно робити в програмному забезпеченні.
user2357112 підтримує Моніку

4
Що робити, якщо ви спробуєте версію Fortran із використанням масиву (і, зокрема, з використанням одного зчитування, а не мільярда)?
francescalus

9
Ви також розглядали можливість використання операторів масивів у Fortran? Тоді ви могли б спробувати minval(), maxval()і sum()? Крім того, ви змішуєте IO з операціями у Fortran, але не в Python - це нечесне порівняння ;-)
Олександр Фогт

4
При тестуванні чогось із великим файлом переконайтесь, що воно кешоване однаково для всіх запусків.
Tom Zych

1
Також зауважте, що точність - це досить велика справа у Fortran, і це коштує витрат. Навіть після того, як ви вирішите всі ці очевидні проблеми з кодом Fortran, цілком можливо, що потрібна додаткова точність і спричинить значну втрату швидкості.
Луан

Відповіді:


110

Ваша реалізація Fortran має два основні недоліки:

  • Ви змішуєте введення-виведення та обчислення (і читаєте з запису файлу за записом).
  • Ви не використовуєте векторні / матричні операції.

Ця реалізація виконує ту ж операцію, що і ваша, і на моїй машині швидше в 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-норма SLANGEна стовпцях матриці. Час роботи навіть швидший, ніж підхід, що використовує функції одиночного прецизійного масиву - і не відображає проблеми з точністю.


4
Чому змішування введення з розрахунком так сильно уповільнює? Вони обоє повинні прочитати весь файл, що буде вузьким місцем. І якщо ОС робить читання, код Fortran не повинен довго чекати введення / виводу.
Бармар,

3
@Barmar Ви все одно матимете накладні витрати на виклик функції та логіку для перевірки, чи дані щоразу знаходяться в кеші.
Оверв

55

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

Подивіться на свій код python. Ви завантажуєте весь масив відразу, а потім викликаєте функції, які можуть працювати з масивом.

Подивіться на свій код fortran. Ви читаєте одне значення за раз і робите з ним певну логічну розгалуження.

Більшість ваших розбіжностей - фрагментований IO, який ви написали у Фортрані.

Ви можете написати 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виклик? У мене є деякі сумніви щодо цього.
Бакуріу

1
@Bakuriu Ні, це не так. Дивіться відповідь Олександра Фогта та мої правки щодо питання.
user35915
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.