Zużycie pamięci w fortran przy użyciu tablicy typu pochodnego ze wskaźnikiem

13

W tym przykładowym programie robię to samo (przynajmniej tak mi się wydaje) na dwa różne sposoby. Korzystam z tego na komputerze z systemem Linux i monitoruję użycie pamięci za pomocą top. Korzystając z gfortran, stwierdzam, że w pierwszy sposób (między „1” a „2”) używana pamięć wynosi 8,2 GB, podczas gdy w drugim sposobie (między „2” i „3”) zużycie pamięci wynosi 3,0 GB. W przypadku kompilatora Intel różnica jest jeszcze większa: 10 GB w porównaniu do 3 GB. Wydaje się to nadmierną karą za używanie wskaźników. Dlaczego to się dzieje?

program test
implicit none

  type nodesType
    integer:: nnodes
    integer,dimension(:),pointer:: nodes 
  end type nodesType

  type nodesType2
    integer:: nnodes
    integer,dimension(4):: nodes 
  end type nodesType2

  type(nodesType),dimension(:),allocatable:: FaceList
  type(nodesType2),dimension(:),allocatable:: FaceList2

  integer:: n,i

  n = 100000000

  print *, '1'
  read(*,*)
  allocate(FaceList(n))
  do i=1,n
    FaceList(i)%nnodes = 4
    allocate(FaceList(i)%nodes(4))
    FaceList(i)%nodes(1:4) = (/1,2,3,4/)
  end do
  print *, '2'
  read(*,*)

  do i=1,n
    deallocate(FaceList(i)%nodes)
  end do
  deallocate(FaceList)

  allocate(FaceList2(n))
  do i=1,n
    FaceList2(i)%nnodes = 4
    FaceList2(i)%nodes(1:4) = (/1,2,3,4/)
  end do
  print *, '3'
  read(*,*)

end program test

Tłem jest lokalne udoskonalenie sieci. Wybrałem połączoną listę, aby łatwo dodawać i usuwać twarze. Domyślnie liczba węzłów wynosi 4, ale może wzrosnąć w zależności od lokalnych ulepszeń.

Chris
źródło
1
„Pierwszego sposobu” należy unikać w jak największym stopniu, ponieważ jest on podatny na wycieki (tablice muszą być jawnie zwolnione, tak jak to robiłeś), poza różnicą w wydajności, którą widzisz. Jedynym powodem do użycia byłoby ścisłe przestrzeganie Fortran 95. Allocatable w typach pochodnych zostały dodane w TR 15581, ale wszystkie kompilatory Fortran (nawet te, które nie mają żadnych cech z 2003 roku) wspierały je, tj. F95 + TR15581 + TR15580 od zawsze .
stali
1
Powodem tego jest to, że niektóre twarze mogą mieć więcej niż 4 węzły.
Chris
To z pewnością ma sens. Zakładałem, że 4 było stałą.
stali

Odpowiedzi:

6

Właściwie nie wiem, jak działają kompilatory fortran, ale w oparciu o funkcje językowe mogę zgadywać.

Dynamiczne tablice w Fortranie są dostarczane z meta-danymi do pracy z funkcjami wewnętrznymi, takimi jak kształt, rozmiar, łączenie, ubound oraz przydzielane lub powiązane (przydzielane vs wskaźniki). W przypadku dużych tablic rozmiar metadanych jest znikomy, ale w przypadku małych tablic, tak jak w twoim przypadku, może się sumować. W twoim przypadku tablice dynamiczne rozmiaru 4 prawdopodobnie zawierają więcej metadanych niż dane rzeczywiste, co prowadzi do powstania balonu użycia pamięci.

Zdecydowanie odradzam pamięć dynamiczną na dole twoich struktur. Jeśli piszesz kod dotyczący systemów fizycznych w pewnej liczbie wymiarów, możesz ustawić go jako makro i ponownie skompilować. Jeśli masz do czynienia z wykresami, możesz statycznie przypisać górną granicę liczby krawędzi lub polubień. Jeśli masz do czynienia z systemem, który faktycznie potrzebuje drobnoziarnistej dynamicznej kontroli pamięci, prawdopodobnie najlepiej jest przejść do C.

Max Hutchinson
źródło
Tak, ale czy argument metadanych nie jest prawdziwy w obu przypadkach?
stali
@stali nie, zauważ, że drugi przypadek wymaga jednego wskaźnika, w przeciwieństwie do nwskaźników wymaganych przez pierwszą metodę.
Aron Ahmadia,
Dodałem informacje w tle. Twoja sugestia statycznego przydzielenia górnej granicy jest już dobrą poprawą. Górna granica wynosi 8, ale większość będzie miała 4, tylko niewielki procent będzie miał 5,6,7 lub 8. Więc pamięć jest nadal marnowana ...
Chris
@chris: Czy możesz zrobić dwie listy, jedną z 4, a drugą z 8 węzłami?
Pedro
Prawdopodobnie. To wydaje się być dobrym kompromisem.
Chris
5

Jak wskazał maxhutch , problemem jest prawdopodobnie sama liczba oddzielnych przydziałów pamięci. Oprócz metadanych prawdopodobnie są to jednak wszelkie dodatkowe dane i wyrównanie, których potrzebuje menedżer pamięci, tzn. Prawdopodobnie zaokrągla każdą alokację do pewnej wielokrotności 64 bajtów lub więcej.

Aby uniknąć przydzielania małej porcji dla każdego węzła, możesz spróbować przydzielić każdemu węzłowi część wstępnie przydzielonej tablicy:

integer :: finger
indeger, dimension(8*n) :: theNodes

finger = 1
do i=1,n
    FaceList(i)%nodes => theNodes(finger:finger+FaceList(i)%nnodes-1)
    finger = finger + FaceList(i)%nnodes
end do

Mój Fortran jest trochę zardzewiały, ale powyższe powinno działać, jeśli nie w zasadzie.

Nadal będziesz mieć koszty ogólne tego, co kompilator Fortran uważa za potrzebne do przechowywania typu POINTER, ale nie będziesz mieć kosztów ogólnych związanych z menedżerem pamięci.

Pedro
źródło
to pomaga, ale tylko trochę. Problem polega na tym, że nie jest to pojedynczy wskaźnik, ale dynamiczna tablica wskaźników: FaceList (i)% nodes (1: FaceList (i)% nnodes) => theNodes (palec: palec + FaceList (i)% nnodes-1). Implikuje również dokładne oszacowanie wielkości wstępnie przydzielonej tablicy.
Chris
@chris: Nie jestem pewien, czy całkowicie rozumiem ... Co rozumiesz przez „dynamiczny zestaw wskaźników”? Pole nodesType%nodesjest wskaźnikiem do tablicy dynamicznej.
Pedro
0

O. To ten sam problem, który cierpiałem. To pytanie jest bardzo stare, ale sugeruję nieco inny styl kodu. Moim problemem była tablica instrukcji do przydzielenia w pochodnym typie danych, jak kod podążający.

type :: node
  real*4,dimension(:),allocatable :: var4
  real*8,dimension(:),allocatable :: var8
end type node

type(node),dimension(:),allocatable :: nds

imax = 5000
allocate(nds(imax))

Na podstawie niektórych testów potwierdziłem, że jeśli użyję instrukcji alokowalnej lub instrukcji wskaźnikowej w typie pochodnym jako kodu podążającego o czterech przypadkach, wyciek pamięci stanie się bardzo duży. W moim przypadku czerwony plik o rozmiarze 520 MB. Ale użycie pamięci wyniosło 4 GB w trybie zwolnienia na kompilatorze Intel Fortran. To 8 razy większy!

!(case 1) real*4,dimension(:),allocatable :: var4
!(case 2) real*4,dimension(:),pointer :: var4
!(case 3) real*4,allocatable :: var4(:,:)

!(case 4) 
type :: node(k)
  integer,len :: k = 4
  real*4 :: var4(k)
end type node

Wyciek pamięci nie występuje, gdy używam instrukcji alokowalnej lub wskaźnika bez typu pochodnego. Moim zdaniem, jeśli zadeklaruję zmienną typu możliwego do przydzielenia lub zmienną typu wskaźnikowego w typie pochodnym, a dużą zmienną typu pochodnego zmienną typu niemożliwą do przydzielenia w typie pochodnym, nastąpi wyciek pamięci. Aby rozwiązać ten problem, zmieniłem kod, który nie zawiera typu pochodnego jako kodu podążającego.

real*4,dimension(:,:),allocatable :: var4 
! array index = (Num. of Nodes, Num. of Variables)

a może ten styl?

integer,dimension(:),allocatable :: NumNodes ! (:)=Num. of Cell or Face or etc.
integer,dimension(:),allocatable :: Node     ! (:)=(Sum(NumNodes))

Zmienna NumNodes oznacza liczbę węzłów na każdej powierzchni, a zmienna Węzeł to numery węzłów pasujące do zmiennej NumNodes. Może w tym stylu kodu nie występuje wyciek pamięci.

G.Ku
źródło