Passing array arguments efficiently in GEOS-Chem
On this page we provide strategies to help you write GEOS-Chem code that passes arrays between subroutines in the most efficient manner.
Overview
In many areas of GEOS-Chem, we pass arrays as arguments from one routine to another. But if this is not done properly, it can cause GEOS-Chem to use an excessive amount of memory and take longer to run, especially at very fine resolutions. The following sections explain this issue in more depth:
Technical Description
This very technical description of how Fortran passes arrays to subroutines is taken from the Intel Fortran Compiler Version 11 Manual, p. 1628-1630:
In Fortran, there are two general types of array arguments:
- Explicit-shape arrays (introduced with Fortran 77); for example, A(3,4) and B(0:*)
- These arrays have a fixed rank and extent that is known at compile time.
- Other dummy argument (receiving) arrays that are not deferred-shape (such as assumed-size arrays) can be grouped with explicit-shape array arguments.
- Deferred-shape arrays (introduced with Fortran 95/90); for example, C(:.:)
- Types of deferred-shape arrays include array pointers and allocatable arrays.
- Assumed-shape array arguments generally follow the rules about passing deferred-shape array arguments.
When passing arrays as arguments, either the starting (base) address of the array or the address of an array descriptor is passed:
- When using explicit-shape (or assumed-size) arrays to receive an array, the starting address of the array is passed.
- When using deferred-shape (or assumed-shape) arrays to receive an array, the address of the array descriptor is passed (the compiler creates the array descriptor).
Passing an assumed-shape array or array pointer to an explicit-shape array can slow run-time performance. This is because the compiler needs to create an array temporary for the entire array. The array temporary is created because the passed array may not be contiguous and the receiving (explicit-shape) array requires a contiguous array. When an array temporary is created, the size of the passed array determines whether the impact on slowing run-time performance is slight or severe.
The following table summarizes what happens with the various combinations of array types. The amount of run-time performance inefficiency depends on the size of the array.
Dummy Argument Array Types (i.e. declared in the routine being called) Actual Argument Array Type
(i.e. declared in the calling routine)EXPLICIT-SHAPE ARRAYS DEFERRED-SHAPE and ASSUMED-SHAPE ARRAYS EXPLICIT-SHAPE ARRAYS Result when using this combination: VERY EFFICIENT.
- Does not use an array temporary.
- Does not pass an array descriptor.
- Interface block optional.
Result when using this combination: EFFICIENT.
- Only allowed for assumed-shape arrays (not deferred-shape arrays).
- Does not use an array temporary.
- Passes an array descriptor.
- Requires an interface block.
DEFERRED-SHAPE and ASSUMED-SHAPE ARRAYS Result when using this combination:
- When passing an allocatable array, VERY EFFICIENT.
- Does not use an array temporary.
- Does not pass an array descriptor.
- Interface block optional.
- When not passing an allocatable array: NOT EFFICIENT.
- Instead, use allocatable arrays whenever possible.
- Uses an array temporary.
- Does not pass an array descriptor.
- Interface block optional.
Result when using this combination: EFFICIENT.
- Requires an assumed-shape or array pointer as dummy argument.
- Does not use an array temporary.
- Passes an array descriptor.
- Requires an interface block.
NOTE: Code that is contained within a Fortran MODULE construct will have interface blocks computed automatically. Otherwise you have use a Fortran INTERFACE statement to explicitly list the argument name and the sizes and types of each argument.
--Bob Y. 15:37, 6 June 2013 (EDT)
Description in plain English
The discussion in the preceding section is very technical. We attempt to explain it in more understandable terms below.
Allocatable arrays
Many arrays in GEOS-Chem are declared with the ALLOCATABLE attribute, such as:
REAL*8, ALLOCATABLE :: XMID(:,:,:)
In this example, we tell the compiler to expect a 3-dimensional array named XMID. But we also tell the compiler not to give XMID any memory at compile time. Instead, we do this after GEOS-Chem has started executing by means of an ALLOCATE statement:
ALLOCATE( XMID( IIPAR, JJPAR, LLPAR ), STAT=RC ) IF ( RC /= 0 ) CALL ALLOC_ERR( 'XMID' ) XMID = 0d0
The above statements tell the GEOS-Chem to make the XMID array XMID be of size (IIPAR, JJPAR, LLPAR), where these values have been declared elsewhere. We also check to make sure that there is enough memory before we give memory to XMID. You will see these statements in many modules of GEOS-Chem.
Derived-type objects
In GEOS-Chem v9-02k and higher versions, we have replaced many common allocatable arrays with fields from derived type objects. We did this to better enable GEOS-Chem to run within an external GCM, such as NASA's GEOS-5 GCM.
In particular:
- The State_Chm%Tracers field now replaces the STT array of advected tracers.
- The State_Chm%Species field now replaces the CSPEC_FULL array of chemical species.
- The State_Met object replaces all GEOS-Chem met field arrays formerly contained in GeosCore/dao_mod.F.
State_Chm is a derived type object that holds information needed for GEOS-Chem's chemistry solver. A derived-type object is a "bucket-o-variables", where you can store scalar and array variables of different types (INTEGER, REAL*4, REAL*8, CHARACTER, etc.) in a single container. Similarly, State_Met gathers various meteorological parameters in a single construct.
One important thing to note is that the Fortran-90 standard requires array fields within a derived-type object to be declared with the POINTER attribute instead of with the ALLOCATABLE attribute, as you might otherwise expect. For this reason, fields in State_Chm and State_Met are declared as POINTER arrays. In other words, the declaration of these fields looks like this:
TYPE ChmState ... etc ... REAL*8, POINTER :: Tracers(:,:,:,:) REAL*8, POINTER :: Species(:,:,:,:) ... etc ... END TYPE ChmState TYPE MetState ... etc ... REAL*8, POINTER :: UWND(:,:,:) REAL*8, POINTER :: VWND(:,:,:) ... etc ... END TYPE METSTATE
Using POINTER instead of ALLOCATABLE here may seem like a very insignificant distinction. In outward apperance, POINTER fields of derived type objects (such as State_Chm%Tracers) behave much in the same way as ALLOCATABLE arrays. But as you will see in the next couple of subsections, there are important consequences for the overall efficiency of GEOS-Chem.
Passing ALLOCATABLE arrays as arguments to other routines
GEOS-Chem contains a significant fraction of third-party routines, such as TPCORE, FAST-J, etc. These routines were often not originally intended to work with GEOS-Chem. As such, many of these routines accept inputs (such as the array of tracer concentrations) via the argument list. Prior to GEOS-Chem v9-02k, this would have looked like:
SUBROUTINE MY_GC_SUB !------------------------------------------------------- ! Example GEOS-Chem routine that calls a 3rd-party code !-------------------------------------------------------- USE CMN_SIZE_MOD, ONLY : IIPAR, JJPAR, LLPAR ! Dimensions USE TRACER_MOD, ONLY : STT ! Tracer array USE TRACER_MOD, ONLY : N_TRACERS ! # of tracers USE THIRD_PARTY_MOD, ONLY : THIRD_PARTY_SUB ! 3rd-party routine ... etc ... ! Call another subroutine CALL THIRD_PARTY_SUB( IIPAR, JJPAR, LLPAR, N_TRACERS, STT ) ... etc ... END SUBROUTINE MY_GC_SUB . . . MODULE THIRD_PARTY_MOD CONTAINS SUBROUTINE THIRD_PARTY_SUB( IX, JX, LX, NX, Q ) !------------------------------------------------------------ ! Example 3rd-party routine that gets called from GEOS-Chem !------------------------------------------------------------ ! Arguments INTEGER, INTENT(IN) :: IX, JX, LX, NX REAL*8, INTENT(IN) :: Q(IX,JX,LX,NX) ... etc ... END SUBROUTINE THIRD_PARTY_SUB END THIRD_PARTY_MOD
From the table in the preceding section, we see that we have the following situation:
- Array in calling routine: Deferred-shape, allocatable array (STT)
- Array in called routine: Explicit-shape array (Q)
- Result: VERY EFFICIENT.
Execution is very efficient because GEOS-Chem does not have to make a duplicate copy of STT when calling the subroutine. This saves both memory and CPU cycles.
Passing POINTER arrays as arguments to other routines
Now look what happens in GEOS-Chem v9-02k and later versions, where STT is now replaced with the pointer field State_Chm%Tracers:
SUBROUTINE MY_GC_SUB( Input_Opt, State_Chm ) !--------------------------- ! Example calling routine !--------------------------- USE CMN_SIZE_MOD, ONLY : IIPAR, JJPAR, LLPAR ! Dimensions USE GIGC_Input_Opt_Mod, ONLY : OptInput ! Derived type for Input_Opt USE GIGC_State_Chm_Mod, ONLY : ChmState ! Derived type for State_Chm USE THIRD_PARTY_MOD, ONLY : THIRD_PARTY_SUB ! 3rd-party routine ! Arguments TYPE(OptInput), INTENT(IN) :: Input_Opt ! Input Options object TYPE(ChmState, INTENT(INOUT) :: State_Chm ! Chemistry State object ! Local variables INTEGER :: N_TRACERS ... etc ... N_TRACERS = Input_Opt%N_TRACERS ! Call another subroutine CALL THIRD_PARTY_SUB( IIPAR, JJPAR, LLPAR, N_TRACERS, State_Chm%Tracers ) ... etc ... END SUBROUTINE MY_GC_SUB . . . MODULE THIRD_PARTY_MOD CONTAINS SUBROUTINE THIRD_PARTY_SUB( IX, JX, LX, NX, Q ) !----------------------------------- ! Example 3rd-party routine ! that gets called from GEOS-Chem !----------------------------------- ! Arguments INTEGER, INTENT(IN) :: IX, JX, LX, NX REAL*8, INTENT(IN) :: Q(IX,JX,LX,NX) ... etc ... END SUBROUTINE THIRD_PARTY_SUB END THIRD_PARTY_MOD
Referring to the above table, we see that we have this situation:
- Array in calling routine: Deferred-shape array, but NOT ALLOCATABLE (State_Chm%Tracers is a POINTER)
- Array in called routine: Explicit-shape array (Q)
- Result: NOT EFFICIENT.
In this case, GEOS-Chem must copy the values of State_Chm%Tracers into an internal temporary array (also known as an array temporary) and then pass that to the subroutine. The larger the size of State_Chm%Tracers, the larger the array temporary will have to be, and the more memory and CPU cycles will be wasted. If array temporaries are being created for multiple arguments within the same subroutine call, this can cause GEOS-Chem to either run out of available memory or to take exceedingly long to execute.
Caveat: Now that GEOS-Chem is making more use of derived-type objects, we will need to be extra careful to prevent problems like these from occurring.
--Bob Y. 15:46, 6 June 2013 (EDT)
Correcting inefficient array argument passing in GEOS-Chem
Diagnosing with Intel Fortran Compiler
If you are using the Intel Fortran Compiler, you can check if array temporaries are being created with the compiler switch -check arg_temp_created. For your debugging runs, you can add this onto the FFLAGS section in the Makefile_header.mk file. Change this section of code:
# Pick compiler options for debug run or regular run ifdef DEBUG FFLAGS := -cpp -w -O0 -auto -noalign -convert big_endian -g -DDEBUG else FFLAGS := -cpp -w $(OPT) -auto -noalign -convert big_endian -vec-report0 endif
to this:
# Pick compiler options for debug run or regular run ifdef DEBUG FFLAGS := -cpp -w -O0 -auto -noalign -convert big_endian -g -DDEBUG -check arg_temp_created else FFLAGS := -cpp -w $(OPT) -auto -noalign -convert big_endian -vec-report0 endif
Then recompile your code as follows:
make realclean make -j4 DEBUG=yes
This will build run-time checking for array temporaries into the GEOS-Chem executable. If there are any array temporaries, you will see output such as:
forrtl: warning (402): fort: (1): In call to THIRD_PARTY_MOD^THIRD_PARTY_SUB, an array temporary was created for argument #5
--Bob Y. 15:57, 6 June 2013 (EDT)
Preventing array temporaries
In many cases, you can prevent an array temporary from being created by converting a dummy argument from an explicit-shape array to a deferred-shape array. From our above example, we can do this by changing the argument type from explicit-shape to deferred shape, as shown below:
! Call the subroutine from GEOS-Chem, as shown in prior example CALL THIRD_PARTY_SUB( IIPAR, JJPAR, LLPAR, N_TRACERS, State_Chm%TRACERS ) . . . MODULE THIRD_PARTY_MOD CONTAINS SUBROUTINE THIRD_PARTY_SUB( IX, JX, LX, NX, Q ) !----------------------------------- ! Example 3rd-party routine ! that gets called from GEOS-Chem !----------------------------------- ! Arguments INTEGER, INTENT(IN) :: IX, JX, LX, NX !------------------------------------------------- !%%%% CHANGING THIS AVOIDS AN ARRAY TEMPORARY!!! !REAL*8, INTENT(IN) :: Q(IX,JX,LX,NX) !------------------------------------------------- REAL*8, INTENT(IN) :: Q(:,:,:,:) ... etc ... END SUBROUTINE THIRD_PARTY_SUB END MODULE THIRD_PARTY_MOD
Then we will have the following situation:
- Array in calling routine: Deferred-shape array, but NOT ALLOCATABLE (State_Chm%Tracers is a POINTER)
- Array in called routine: Deferred-shape array (Q)
- Result: EFFICIENT.
If you do not have permission to modify the subroutine THIRD_PARTY_SUB (i.e. if it comes from an external source that is not maintained by you), then this is the best you can expect. But if you are not afraid of hacking into someone else's code, you may get more bang for your buck if you rewrite the subroutine to accept the entire derived type object. For example:
! Now pass entire derived-type object from GEOS-Chem to 3rd-party routine CALL THIRD_PARTY_SUB( State_Chm ) . . . MODULE THIRD_PARTY_MOD CONTAINS SUBROUTINE THIRD_PARTY_SUB( State_Chm ) !----------------------------------- ! Example 3rd-party routine ! that gets called from GEOS-Chem !----------------------------------- USE GIGC_State_Chm_Mod, ONLY : ChmState ! Arguments TYPE(ChmState), INTENT(INOUT) :: State_Chm !%%% NOTE: Define Q as a local pointer array instead !%%% of having it be an input argument REAL*8, POINTER :: Q(:,:,:,:) ... etc ... !%%% NOTE: Point Q to State_Chm%Tracers !%%% This allows us to keep all other references !%%% to Q in the code below untouched Q => State_Chm%Tracers ... etc ... !%%% NOTE: Free pointer memory before exiting subroutine NULLIFY( Q ) END SUBROUTINE THIRD_PARTY_SUB
In the above example, we have totally eliminated passing a pointer array as an argument to a subroutine. We now pass the entire derived-type object State_Chm, so that within the subroutine we can access any of its fields. Actually, GEOS-Chem will pass a reference to State_Chm to the subroutine, so that it does not have to make a duplicate copy of it. This is very efficient.
Furthermore, note that within THIRD_PARTY_SUB, we can point to State_Chm%Tracers with locally-defined POINTER arrays. This is much more efficient than trying to pass State_Chm%Tracers via the argument list.
--Bob Y. 16:41, 6 June 2013 (EDT)
Array temporaries corrected in GEOS-Chem v9-02
We have rewritten sections of GEOS-Chem code in the following routines in order to prevent array temporaries from being created. We have introduced these modifications in v9-02k.
bromocarb_mod.F
In routine INIT_BROMOCARB, modify these subroutine calls:
call NcRd( ARRAY2x25, fileID, 'CHBr3_emission', & (/ 1, 1 /), (/ 144, 91 /) ) . . . call NcRd( ARRAY2x25, fileID, 'CH2Br2_emission', & (/ 1, 1 /), (/ 144, 91 /) )
so as not to accept the st2d, ct2d arguments.
INTEGER :: st2d(2) INTEGER :: ct2d(2) . . . ! Start and count arrays for netCDF reads st2d = (/ 1, 1 /) ct2d = (/ 144, 91 /) . . . call NcRd( ARRAY2x25, fileID, 'CHBr3_emission', st2d, ct2d ) . . . call NcRd( ARRAY2x25, fileID, 'CH2Br2_emission', st2d, ct2d )
--Bob Y. 17:22, 6 June 2013 (EDT)
tpcore_fvdas_mod.F90
In routine TPCORE_FVDAS, rewrite the following argument definitions:
!---------------------------------------------------------------------------- ! Prior to 6/4/13: ! Now use assumed-shape declaration for U (bmy, 6/4/13) ! REAL*8, INTENT(IN) :: u(IM,JFIRST:JLAST,KM) !---------------------------------------------------------------------------- REAL*8, INTENT(IN) :: u(:,:,:) . . . . . . !---------------------------------------------------------------------------- ! Prior to 6/4/13: ! Now use assumed-shape declaration for XMASS, YMASS (bmy, 6/4/13) ! REAL*8, INTENT(IN) :: XMASS(IM,JM,KM) ! REAL*8, INTENT(IN) :: YMASS(IM,JM,KM) !---------------------------------------------------------------------------- REAL*8, INTENT(IN) :: XMASS(:,:,:) REAL*8, INTENT(IN) :: YMASS(:,:,:) . . . . . . !---------------------------------------------------------------------------- ! Prior to 6/4/13: ! Now use assumed-shape declaration for V (bmy, 6/4/13) ! REAL*8, INTENT(INOUT) :: v(IM, JFIRST-MG:JLAST+MG, KM) !---------------------------------------------------------------------------- REAL*8, INTENT(INOUT) :: v(:,:,:) . . . . . . !---------------------------------------------------------------------------- ! Prior to 6/4/13: ! Now use assumed-shape declaration for Q (bmy, 6/4/13) ! REAL*8, INTENT(INOUT) :: q(IM, JFIRST-NG:JLAST+NG, KM, NQ) !---------------------------------------------------------------------------- REAL*8, INTENT(INOUT) :: q(:,:,:,:)
Then add local pointer array:
! Add pointer to avoid array temporary in call to FZPPM (bmy, 6/5/13) REAL*8, POINTER :: ptr_Q(:,:,:)
Then make ptr_Q PRIVATE to the OMP loop:
!-------------------------------------------------------- ! For time optimization : we parallelize over tracers and ! we loop over the levels outside horizontal transport ! subroutines. (ccc, 4/1/09) !-------------------------------------------------------- !$OMP PARALLEL DO & !$OMP DEFAULT( SHARED )& !$OMP PRIVATE( IQ, IK, adx, ady, qqu, qqv, dq1, ptr_Q ) do iq = 1, nq
Then modify call to FZPPM
! Set up temporary pointer to Q to avoid array temporary in FZPPM ! (bmy, 6/5/13) ptr_Q => q(:,:,:,iq) ! ========== call Fzppm & ! ========== !----------------------------------------------------------------------------- ! Prior to 6/5/13: ! Now pass ptr_Q to avoid creating an array temporary in FZPPM. ! This array temporary was found with -check arg_temp_created (bmy, 6/5/13) ! (klmt, delp1, wz, dq1, q(:,:,:,iq), fz(:,:,:,iq), & !----------------------------------------------------------------------------- (klmt, delp1, wz, dq1, ptr_Q, fz(:,:,:,iq), & j1p, 1, jm, 1, im, 1, jm, & im, km, 1, im, 1, jm, 1, km) ! Free pointer memory (bmy, 6/5/13) NULLIFY( ptr_Q )
--Bob Y. 17:22, 6 June 2013 (EDT)
tpcore_geos5_window_mod.F90 and tpcore_geos57_window_mod.F90
These modifications were committed under Prevent array temporaries in other areas of GEOS-Chem
Identical modfications are made in both:
- Routine TPCORE_GEOS5_WINDOW (in tpcore_geos5_window_mod.F90
- Routine TPCORE_GEOS57_WINDOW (in tpcore_geos57_window_mod.F90):
Modify arguments:
!------------------------------------------------------------------------------ ! Prior to 6/4/13: ! Now use assumed shape declarations for U, V (bmy, 6/4/13) ! real, intent(in):: u(im,jfirst:jlast,km) ! u-wind (m/s) at mid-time-level (t=t+dt/2) ! real, intent(inout):: v(im,jfirst-mg:jlast+mg,km) ! v-wind (m/s) at mid-time-level (t=t+dt/2) !------------------------------------------------------------------------------ real, intent(in):: u(:,:,:) ! u-wind (m/s) at mid-time-level (t=t+dt/2) real, intent(inout):: v(:,:,:) ! v-wind (m/s) at mid-time-level (t=t+dt/2) . . . . . . !------------------------------------------------------------------------------ ! Prior to 6/4/13: ! Now use assumed-shape declaration for Q (bmy, 6/4/13) ! real, intent(inout):: q(im,jfirst-ng:jlast+ng,km,nq) ! Tracer "mixing ratios" !------------------------------------------------------------------------------ real, intent(inout):: q(:,:,:,:) . . . . . . !------------------------------------------------------------------------------ ! Prior to 6/4/13: ! Now use assumed-shape declaration for Q (bmy, 6/4/13) ! real, intent(inout):: q(im,jfirst-ng:jlast+ng,km,nq) ! Tracer "mixing ratios" !------------------------------------------------------------------------------ real, intent(inout):: q(:,:,:,:)
Then modify calls to routine XPAVG as follows:
if ( jfirst == 1 ) then !----------------------------------------------------------------------------- ! Prior to 6/4/13: ! Use psg(:,jm,1) and psg(:,jm,2) to pass a longitude slice (bmy, 6/4/13) ! call xpavg(psg(1,1,1), im) ! call xpavg(psg(1,1,2), im) !----------------------------------------------------------------------------- call xpavg(psg(:,1,1), im) call xpavg(psg(:,1,2), im) endif if ( jlast == jm ) then !----------------------------------------------------------------------------- ! Prior to 6/4/13: ! Use psg(:,jm,1) and psg(:,jm,2) to pass a longitude slice (bmy, 6/4/13) ! call xpavg(psg(1,jm,1), im) ! call xpavg(psg(1,jm,2), im) !----------------------------------------------------------------------------- call xpavg(psg(:,jm,1), im) call xpavg(psg(:,jm,2), im) endif
and again:
! Average q at both poles do iq=1,nq !$omp parallel do & !$omp shared(im) & !$omp private(k) do k=1,km if ( jfirst == 1 ) then !----------------------------------------------------------------------------- ! Prior to 6/4/13: ! Use q(:,1,k,iq) to pass a longitude slice (bmy, 6/4/13) ! call xpavg(q(1,1,k,iq), im) !----------------------------------------------------------------------------- call xpavg(q(:,1,k,iq), im) endif if ( jlast == jm ) then !----------------------------------------------------------------------------- ! Prior to 6/4/13: ! Use q(:,jm,k,iq) to pass a longitude slice (bmy, 6/4/13) ! call xpavg(q(1,jm,k,iq), im) !----------------------------------------------------------------------------- call xpavg(q(:,jm,k,iq), im) endif enddo enddo
--Bob Y. 17:22, 6 June 2013 (EDT)