Простые и распределенные / указательные массивы, совет Фортрана? - PullRequest
0 голосов
/ 23 февраля 2019

Я написал следующий надуманный пример для умножения матриц, просто чтобы посмотреть, как объявление разных типов массивов может повлиять на производительность.К моему удивлению, я обнаружил, что производительность простых массивов с известными размерами при объявлении ниже, чем у размещаемых массивов / массивов указателей.Я думал, что allocatable нужен только для больших массивов, которые не помещаются в стек.Вот код и тайминги с использованием компиляторов gfortran и Intel Fortran.Платформа Windows 10 используется с флагами компилятора -Ofast и -fast соответственно.

program matrix_multiply
   implicit none
   integer, parameter :: n = 1500
   real(8) :: a(n,n), b(n,n), c(n,n), aT(n,n)                 ! plain arrays 
   integer :: i, j, k, ts, te, count_rate, count_max
   real(8) :: tmp

   ! real(8), allocatable :: A(:,:), B(:,:), C(:,:), aT(:,:)  ! allocatable arrays
   ! allocate ( a(n,n), b(n,n), c(n,n), aT(n,n) )

   do i = 1,n
      do j = 1,n
         a(i,j) = 1.d0/n/n * (i-j) * (i+j)
         b(i,j) = 1.d0/n/n * (i-j) * (i+j)
      end do 
   end do 

   ! transpose for cache-friendliness   
   do i = 1,n
      do j = 1,n
         aT(j,i) = a(i,j)
      end do 
   end do 

   call system_clock(ts, count_rate, count_max)
   do i = 1,n
      do j = 1,n
         tmp = 0 
         do k = 1,n
            tmp = tmp + aT(k,i) * b(k,j)
         end do
         c(i,j) = tmp
      end do
   end do
   call system_clock(te)
   print '(4G0)', "Elapsed time: ", real(te-ts)/count_rate,', c_(n/2+1) = ', c(n/2+1,n/2+1)    
end program matrix_multiply

Сроки таковы:

! Intel Fortran
! -------------
Elapsed time: 1.546000, c_(n/2+1) = -143.8334 ! Plain Arrays
Elapsed time: 1.417000, c_(n/2+1) = -143.8334 ! Allocatable Arrays  

! gfortran:
! -------------
Elapsed time: 1.827999, c_(n/2+1) = -143.8334 ! Plain Arrays 
Elapsed time: 1.702999, c_(n/2+1) = -143.8334 ! Allocatable Arrays

Мой вопрос: почему это происходит?Выделяемые массивы дают компилятору больше гарантий для лучшей оптимизации?Какой самый лучший совет в целом при работе с массивами фиксированного размера в Фортране?

С риском удлинения вопроса, вот еще один пример, где компилятор Intel Fortran демонстрирует то же поведение:

program testArrays
  implicit none
  integer, parameter :: m = 1223, n = 2015 
  real(8), parameter :: pi = acos(-1.d0)
  real(8) :: a(m,n)
  real(8), allocatable :: b(:,:)
  real(8), pointer :: c(:,:)
  integer :: i, sz = min(m, n), t0, t1, count_rate, count_max

  allocate( b(m,n), c(m,n) )
  call random_seed()
  call random_number(a)
  call random_number(b)
  call random_number(c)

  call system_clock(t0, count_rate, count_max)
    do i=1,1000
      call doit(a,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time plain: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( a(1:3,1:3) )

  call system_clock(t0)
    do i=1,1000
      call doit(b,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time alloc: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( b(1:3,1:3) )

  call system_clock(t0)
    do i=1,1000 
      call doitp(c,sz)
    end do 
  call system_clock(t1)
  print '(4g0)', 'Time p.ptr: ', real(t1-t0)/count_rate, ',  sum 3x3 = ', sum( c(1:3,1:3) )

  contains 
  subroutine doit(a,sz)
    real(8) :: a(:,:)
    integer :: sz 
    a(1:sz,1:sz) = sin(2*pi*a(1:sz,1:sz))/(a(1:sz,1:sz)+1)
  end

  subroutine doitp(a,sz)
    real(8), pointer :: a(:,:)
    integer :: sz
    a(1:sz,1:sz) = sin(2*pi*a(1:sz,1:sz))/(a(1:sz,1:sz)+1)
  end    
end program testArrays 

ifort время:

Time plain: 2.857000,  sum 3x3 = -.9913536
Time alloc: 2.750000,  sum 3x3 = .4471794
Time p.ptr: 2.786000,  sum 3x3 = 2.036269  

gfortran время, однако, намного длиннее, но следую моим ожиданиям:

Time plain: 51.5600014,  sum 3x3 = 6.2749456118192093
Time alloc: 54.0300007,  sum 3x3 = 6.4144775892064283
Time p.ptr: 54.1900034,  sum 3x3 = -2.1546109819149963

Ответы [ 2 ]

0 голосов
/ 24 февраля 2019

Чтобы понять, считает ли компилятор разницу, посмотрите на сгенерированную сборку для процедур.Основываясь на кратком обзоре, сборка для временной секции двух случаев для первого примера выглядит более или менее эквивалентной с точки зрения работы, которую должен выполнять процессор.Это, как и ожидалось, так как массивы, представленные в синхронизированном разделе, более или менее эквивалентны - они большие, непрерывные, не перекрываются и со значениями элементов, известными только во время выполнения.

(Помимо компилятора, тогдаДолжны быть различия из-за способа представления данных в различных кэшах во время выполнения, но это должно быть одинаковым для обоих случаев.)

Основное различие между явной формой и выделяемыми массивами заключается во времени, которое требуется длявыделить и освободить хранилище для последнего.В вашем первом примере не более четырех распределений (так что это вряд ли обременительно по сравнению с последующими вычислениями), и вы не рассчитываете время этой части программы.Вставьте пару выделения / неявного освобождения в цикл, а затем посмотрите, что вы делаете.

Массивы с указателем или целевым атрибутом могут подвергаться псевдонимам, поэтому компилятору, возможно, придется проделать дополнительную работу, чтобы учесть возможностьхранилища для перекрытия массивов.Однако природа выражения во втором примере (на который ссылается только один массив) такова, что компилятор, вероятно, знает, что в этом конкретном случае нет необходимости в дополнительной работе, и операции снова становятся эквивалентными.

В ответ на «Я думал, что для выделения памяти нужно было только для больших массивов, которые не помещаются в стек» - для выделения требуется необходимо (т. Е. У вас нет реального выбора), когда выне может определить размер или другие характеристики вещи, выделяемой в части спецификации процедуры, ответственной за полноту существования вещи.Даже для вещей, не известных до времени выполнения, если вы все еще можете определить характеристики в части спецификации соответствующей процедуры, тогда автоматические переменные являются опцией.(Однако в вашем примере нет автоматических переменных - в нераспределяемых, не указательных случаях все характеристики массивов известны во время компиляции.) На уровне реализации процессора Фортрана, который варьируется между компиляторами и параметрами компиляции,автоматическим переменным может потребоваться больше места в стеке, чем доступно, и это может вызвать проблемы, которые могут облегчить размещение (или вы можете просто изменить параметры компилятора).

0 голосов
/ 24 февраля 2019

Это не ответ на вопрос, почему вы получаете то, что вы наблюдаете, а скорее отчет о несогласии с вашими наблюдениями.Ваш код,

program matrix_multiply
   implicit none
   integer, parameter :: n = 1500
  !real(8) :: a(n,n), b(n,n), c(n,n), aT(n,n)                 ! plain arrays 
   integer :: i, j, k, ts, te, count_rate, count_max
   real(8) :: tmp

   real(8), allocatable :: A(:,:), B(:,:), C(:,:), aT(:,:)  ! allocatable arrays
   allocate ( a(n,n), b(n,n), c(n,n), aT(n,n) )

   do i = 1,n
      do j = 1,n
         a(i,j) = 1.d0/n/n * (i-j) * (i+j)
         b(i,j) = 1.d0/n/n * (i-j) * (i+j)
      end do 
   end do 

   ! transpose for cache-friendliness   
   do i = 1,n
      do j = 1,n
         aT(j,i) = a(i,j)
      end do 
   end do 

   call system_clock(ts, count_rate, count_max)
   do i = 1,n
      do j = 1,n
         tmp = 0 
         do k = 1,n
            tmp = tmp + aT(k,i) * b(k,j)
         end do
         c(i,j) = tmp
      end do
   end do
   call system_clock(te)
   print '(4G0)', "Elapsed time: ", real(te-ts)/count_rate,', c_(n/2+1) = ', c(n/2+1,n/2+1)    
end program matrix_multiply

, скомпилированный с помощью компилятора Intel Fortran 18.0.2 для Windows и включенные флаги оптимизации,

ifort /standard-semantics /F0x1000000000 /O3 /Qip /Qipo /Qunroll /Qunroll-aggressive /inline:all /Ob2 main.f90 -o run.exe

, фактически, противоположен тому, что вы наблюдаете:

Elapsed time: 1.580000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.560000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.555000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.588000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.551000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.566000, c_(n/2+1) = -143.8334   ! plain arrays
Elapsed time: 1.555000, c_(n/2+1) = -143.8334   ! plain arrays

Elapsed time: 1.634000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.634000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.602000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.623000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.597000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.607000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.617000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.606000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.626000, c_(n/2+1) = -143.8334   ! allocatable arrays
Elapsed time: 1.614000, c_(n/2+1) = -143.8334   ! allocatable arrays

Как видите, выделяемые массивы на самом деле немного медленнее, в среднем, что я и ожидал увидеть, что также противоречит вашим наблюдениям.Единственный источник различий, который я вижу, - это используемые флаги оптимизации, хотя я не уверен, как это может изменить ситуацию.Возможно, вы захотите запустить свои тесты в нескольких разных режимах без оптимизации и с разными уровнями оптимизации и посмотреть, получаете ли вы постоянные различия в производительности во всех режимах или нет.Чтобы получить больше информации о значении используемых флагов оптимизации, см. Справочная страница Intel .

Кроме того, не используйте real(8) для объявлений переменных.Это нестандартный синтаксис, непереносимый и, следовательно, потенциально проблематичный.Более последовательный способ, в соответствии со стандартом Фортрана, заключается в использовании встроенного модуля iso_fortran_env, например:

!...
use, intrinsic :: iso_fortran_env, only: real64, int32
integer(int32), parameter :: n=100
real(real64) :: a(n)
!...

Этот встроенный модуль имеет следующие виды:

   int8 ! 8-bit integer
  int16 ! 16-bit integer
  int32 ! 32-bit integer
  int64 ! 64-bit integer
 real32 ! 32-bit real
 real64 ! 64-bit real
real128 ! 128-bit real 

Итак, дляНапример, если вы хотите объявить сложную переменную с компонентами 64-битного типа, вы можете написать:

program complex
    use, intrinsic :: iso_fortran_env, only: RK => real64, output_unit
    ! the intrinsic attribute above is not essential, but recommended, so this would be also valid:
    ! use iso_fortran_env, only: RK => real64, output_unit
    complex(RK) :: z = (1._RK, 2._RK)
    write(output_unit,"(*(g0,:,' '))") "Hello World! This is a complex variable:", z
end program complex

, что дает:

$gfortran -std=f2008 *.f95 -o main
$main
Hello World! This is a complex variable: 1.0000000000000000 2.0000000000000000

Обратите внимание, что для этого требуется совместимость с Fortran 2008компилятор.В iso_fortran_env также есть другие функции и объекты, например output_unit, который является номером устройства для предварительно подключенного стандартного устройства вывода (то же самое, что используется print или write с спецификатором устройства *), а также некоторые другие, такие как compiler_version(), compiler_options() и другие.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...