2015-08-23 4 views
5

Im folgenden Code übergeben wir zwei Arrays an eine Subroutine und führen einige zusätzliche Operationen in DO-Schleifen durch. Hier betrachten wir drei Fälle, in denen verschiedene Operationen durchgeführt werden: Fall 1 = keine Operation, Fälle 2 und 3 = Zuweisung von Zeigervariablen.Leistungsabfall von DO-Schleifen durch mögliche Änderung gebundener Variablen

!------------------------------------------------------------------------ 
module mymod 
    implicit none 
    integer, pointer :: n_mod 
    integer :: nloop 
contains 

!......................................................... 
subroutine test_2dim (a, b, n) 
    integer :: n 
    real :: a(n,n), b(n,n) 

    integer, pointer :: n_ptr 
    integer i1, i2, iloop 

    n_ptr => n_mod 

    do iloop = 1, nloop 

     do i2 = 1, n 
     do i1 = 1, n 
      b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop 

      !(nothing here) !! Case 1 : gfort => 3.6 sec, ifort => 3.2 sec 
!   n_ptr = n   !! Case 2 : gfort => 15.9 sec, ifort => 6.2 sec 
!   n_ptr = n_mod  !! Case 3 : gfort => 3.6 sec, ifort => 3.5 sec 
     enddo 
     enddo 

    enddo 
endsubroutine 

endmodule 

!------------------------------------------------------------------------ 
program main 
    use mymod 
    implicit none 
    integer, target :: n 
    real, allocatable :: a(:,:), b(:,:) 

    nloop = 10000 ; n = 1000 
    allocate(a(n, n), b(n, n)) 
    a = 0.0 ; b = 0.0 

    n_mod => n 

    call test_2dim (a, b, n) 

    print *, a(n,n), b(n,n) !! for check 
end 

Hier bemerken wir, daß dieser Zeiger zu den oberen Grenzen der DO-Schleife über die Modulgröße (n_mod) verbunden ist. Daher sollte das Ändern der Zeigervariablen innerhalb der Schleife das Verhalten der Schleifen beeinflussen. Aber bitte beachten Sie, dass wir die Grenzen in der Praxis nicht ändern (nur eine Kopie der Variablen). gfortran 4.8 und ifort 14.0 mit -O3 ergaben das oben angegebene Timing. Bemerkenswerterweise ist Fall 2 im Vergleich zu Fall 1 sehr langsam, obwohl die Nettoberechnung nicht viel anders aussieht. Ich vermutete, dass dies daran liegen könnte, dass der Compiler nicht feststellen kann, ob die obere Grenze der zweiten Schleife (für i1) durch Zeigerzuweisung geändert wird, und vermeidet so eine aggressive Optimierung. Um dies zu überprüfen, habe ich getestet, die folgende Routine anstelle von test_2dim():

subroutine test_1dim (a, b, n) 
    integer :: n 
    real :: a(n * n), b(n * n) 

    integer, pointer :: n_ptr 
    integer iloop, i 

    n_ptr => n_mod 

    do iloop = 1, nloop 

     do i = 1, n * n 
      b(i) = a(i) + b(i) + iloop 

      ! (nothing here) !! Case 1 : gfort => 3.6 sec, ifort => 2.3 sec 
      ! n_ptr = n   !! Case 2 : gfort => 15.9 sec, ifort => 6.0 sec 
      ! n_ptr = n_mod  !! Case 3 : gfort => 3.6 sec, ifort => 6.1 sec 
     enddo 

    enddo 
endsubroutine 

Hier ist der einzige Unterschied zwischen test_1dim() und test_2dim() ist, dass die Arrays A und B zugegriffen werden von 1- oder 2- dim-Indizes (im Wesentlichen kein Unterschied in der Höhe der Berechnung). Überraschenderweise ergab Fall 2 auch ein langsames Ergebnis, obwohl es nur eine einzelne DO-Schleife gibt. Da Fortran-DO-Schleifen die obere Grenze der Schleife nach dem Eintrag [Ref] bestimmen, erwartete ich, dass test_1dim() nicht von der Zeigerzuweisung beeinflusst würde, obwohl dies nicht der Fall war. Gibt es eine vernünftige Erklärung für dieses Verhalten? (Ich hoffe, dass ich nicht einige großen Fehler mache, die zu dieser Zeitdifferenz führt.)


Meine Motivation für diese Frage: Ich habe mit abgeleiteten Typen weitgehend mehrdimensionale Schleifen zu spezifizieren, zum Beispiel

module Grid_mod 
type Grid_t 
    integer :: N1, N2, N3 
endtype 
.... 

subroutine some_calc (vector, grid) 
type(Grid_t) :: grid 
.... 
do i3 = 1, grid % N3 
do i2 = 1, grid % N2 
do i1 = 1, grid % N1 
    (... various operations...) 
enddo 
enddo 
enddo 

Bis jetzt habe ich nicht viel Aufmerksamkeit darauf verwendet, ob Grid_t Objekte TARGET oder POINTER Attribut erhalten (vorausgesetzt, dass es im Wesentlichen keine Auswirkung auf die Leistung hat). Ich denke jedoch, dass dies zu einem Leistungsabfall führen kann, wenn ein Compiler nicht bestimmen kann, ob die oberen Grenzen innerhalb der Schleifen konstant sind (obwohl ich niemals die Grenzen in tatsächlichen Codes ändern werde). Daher wäre ich für jeden Rat dankbar, ob ich vorsichtiger sein sollte, TARGET- oder POINTER-Attribute für gebundene Variablen zu verwenden (einschließlich Komponenten des abgeleiteten Typs, wie sie durch das obige Rasterobjekt angegeben sind).


aktualisiert

Anregung durch @francescalus Nach habe ich versucht, "Vorsatz (in), den Wert" auf das Scheinargument "n" zu befestigen. Das Ergebnis ist wie folgt:

test_1dim(): 
    Case 1: gfort => 3.6 s, ifort => 2.3 s 
    Case 2: gfort => 3.6 s, ifort => 3.1 s 
    Case 3: gfort => 3.6 s, ifort => 3.4 s 

test_2dim(): 
    Case 1: gfort => 3.7 s, ifort => 3.1 s 
    Case 2: gfort => 3.7 s, ifort => 3.1 s 
    Case 3: gfort => 3.7 s, ifort => 6.4 s 

Während ifort etwas unregelmäßig Ergebnis ergibt (6,4 s) für Fall 3 in test_2dim(), alle anderen Ergebnisse zeigen im Wesentlichen die beste Leistung. Dies legt nahe, dass die Behandlung von Grenzen durch den Compiler die Leistung beeinflusst (nicht aufgrund der Kosten der Zeigerzuweisung).Da es wichtig ist, dem Compiler mitzuteilen, dass die Grenzen konstant sind, habe ich auch versucht, das Pseudo-Argument n (hier nicht mit "int (in), value") in eine lokale Variable n_ zu kopieren und als Schleifengrenzen zu verwenden:

integer :: n !! dummy argument 
    integer :: n_ !! a local variable 
    ... 
    n_ = n 

    do i2 = 1, n_ 
    do i1 = 1, n_ 
     b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop 
     ... 

das Ergebnis für test_2dim() ist wie folgt:

test_2dim(): 
    Case 1: gfort => 3.6 s, ifort => 3.1 s 
    Case 2: gfort => 15.9 s, ifort => 6.2 s 
    Case 3: gfort => 3.7 s, ifort => 6.4 s 

hier leider (und meine Erwartung im Gegensatz), Fall 2 besserte sich nicht ... Obwohl das Kopieren auf eine lokale n_ sollte Damit garantiert wird, dass n_ in den DO-Schleifen konstant ist, scheint der Compiler nicht glücklich zu sein, da die Array-Form immer noch durch n bestimmt ist, nicht durch n_, wodurch eine aggressive Optimierung vermieden wird ()- nur meine Vermutung).


Update2

Nach dem Vorschlag von @innoSPG ich auch n geändert innen DO-Schleifen für Case 2, N_ und es stellt sich dann heraus, dass der Code so schnell wie Fall 1 läuft! Genauer gesagt, ist der Code

Aber wie die Antwort vorschlägt, kann diese Effizienz sein, weil die Zuordnung Anweisung vollständig vom Compiler eliminiert wird. Also denke ich, ich muss mehr praktische Codes (nicht zu einfach) prüfen, um die Auswirkungen von Zeigern oder Zeigerkomponenten auf die Schleifenoptimierung zu testen ...

(... Es tut mir leid für eine sehr lange Frage ...)

+0

Ich denke auch, dass die Zeitdifferenz sein kann, weil die Zuordnung der Zeigervariable ziemlich teuer ist, aber nicht sehr sicher ... – roygvib

+0

Versuchen Hinzufügen 'Absicht (in), value' auf die Erklärung von' n' in der Unterroutine (es sei denn, Sie wollen wirklich die Möglichkeit, Dinge zu ändern). – francescalus

+0

@francescalus Ich habe die Frage aktualisiert, also bitte überprüfen Sie das Ergebnis. Und ich würde mich freuen, wenn Sie eine kurze Antwort (Erklärung) darüber (d. H., Warum das VALUE-Attribut die Leistung verbessert), also werde ich die Antwort akzeptieren. – roygvib

Antwort

2

Wenn Sie die Optimierung durchführen, benötigt der Compiler mehr Zeit, um mehr über Ihr Programm zu erfahren, so dass neben der Nutzung der Architektur auch unnötige Berechnungen vermieden werden können. Dies hängt stark vom Compiler und von der Architektur ab.

So ist meine Vermutung, dass der Compiler weiß im Voraus, dass n_ptr und n_mod genau die gleiche Sache ist und nicht einmal Zeit für die Zuweisung für den Fall 3. Das Szenario für Fall 2 ähnlich ist, zu verbringen, wenn n Absicht ist, (in) , der Compiler kann vorhersagen, dass es nicht notwendig ist, die Zuweisung in der Schleife auszuführen, es muss nur einmal ausgeführt werden, da n_ptr an keiner anderen Berechnung in der Subroutine beteiligt ist. Ich vermute, ich hätte diesen Punkt verpasst. Außerdem können Sie eine Intel-basierte Architektur verwenden, was ifort Vorteile bietet. Für den Fall 2, wenn n nicht beabsichtigt ist (in), merkt der Compiler, dass es ein Ziel ist und durch viele andere Mittel geändert werden kann, und es gibt absolut keine Hinweise auf Zeiger, die darauf zeigen. Dies führt zu der Dereferenzierung des Zeigers während der Zuweisung, um die Rechenzeit zu machen. Im Grunde benötigt das Laden/Speichern des Wertes, auf den gezeigt wird, von einer Zeigervariablen zweimal mehr Zeit zum Laden/Speichern eines Wertes von einer Nicht-Zeigervariablen. Ich weiß nicht, wie Zeiger in Fortran tatsächlich implementiert werden, daher kann ich keine ernsthaften Hinweise auf die Zeitfaktoren geben. Es hängt stark von der Implementierung von Zeigern und Zielen ab.

Ich habe das noch nicht versucht, aber ich schlage vor, dass Sie für Ihren Test mit der lokalen Variablen n_ auch die rechte Seite der Zuweisung von Fall 2 in die lokale Variable n_ ändern. Ich bin der festen Überzeugung, dass Sie die gleiche Zeit wie in Fall 1 erhalten werden, weil der Compiler vorhersagen kann, dass es nicht notwendig ist, die Zuweisung in der Schleife auszuführen.

n_ = n 
do iloop = 1, nloop 

    do i2 = 1, n_ 
    do i1 = 1, n_ 
     b(i1,i2) = a(i1,i2) + b(i1,i2) + iloop 

     !(nothing here) !! Case (1) 
     !n_ptr = n_   !! Case (2) 
     !n_ptr = n_mod  !! Case (3) 
    enddo 
    enddo 

enddo 
+0

Ein Skalarzeiger in Gfortran (und allen anderen Compilern, die ich gesehen habe) ist nur eine Adresse. –

+0

@innoSPC Vielen Dank. Ja, der Compiler hat möglicherweise die Zeile für die Zuweisung von Zeigervariablen eliminiert ... (weil sie bedeutungslos ist). Ich habe eine ähnliche Sache erlebt, bevor gfortran unnötige Linien eliminiert, während ifort sie konservativ beibehält. Ich melde mich wieder bei Ihnen, nachdem ich weitere Tests gemacht habe (inkl. Ihres vorgeschlagenen). – roygvib

+0

@VladimirF, danke für den Punkt auf Gfortran Implementierung von Zeigern. Haben Sie etwas darüber in Erfahrung gebracht, um zu sehen, wie viel Aufwand eine Skalarvariable mit einem Zeigerattribut im Vergleich zu einer mit keinem Zeigerattribut hat? – innoSPG

Verwandte Themen