|
do k = kb,ke |
|
kp=k+1 |
|
km=k-1 |
|
do i = ib,ie |
|
ip=i+1 |
|
im=i-1 |
|
mlen = csz(i,k) * delta(i,k) |
|
do j = jb,je |
|
jp=j+1 |
|
jm=j-1 |
|
|
|
! iw = wall(i,j,k,1) ! indices of closest wall |
|
! jw = wall(i,j,k,2)-myid*jmax ! indices of closest wall in local j-index |
|
! kw = wall(i,j,k,3) |
|
! c1 = wall(i,j,k,4) ! shear stress component |
|
! c2 = wall(i,j,k,5) ! shear stress component |
|
! if ((jw >= jb-1) .and. (jw <= je+1)) then ! check if jw is within the halo of this proc |
|
! !write(*,'(A,E9.2,A,E9.2,A,E9.2,A,E9.2)') 'component1:', c1, 'component2:', c2, 'shear c1:', shear(iw,jw,kw,c1), 'shear c2:', shear(iw,jw,kw,c2) |
|
! distplus = mindist(i,j,k)*sqrt(abs(shear(iw,jw,kw,c1))+abs(shear(iw,jw,kw,c2)))*numoli |
|
! damp(i,j,k) = sqrt(1. - exp((-distplus*0.04)**3.)) ! Wall-damping according to Piomelli |
|
! ! write(*,'(A,2(1pE9.2))') 'damp, distplus', damp(i,j,k), distplus |
|
! else |
|
damp(i,j,k) = 1. |
|
! end if |
|
|
|
|
|
strain2 = ((u0(ip,j,k)-u0(i,j,k)) *dxi )**2 & |
|
+ ((v0(i,jp,k)-v0(i,j,k)) *dyi )**2 & |
|
+ ((w0(i,j,kp)-w0(i,j,k)) *dzfi(k) )**2 |
|
|
|
strain2 = strain2 + 0.125 * ( & |
|
+ ((w0(i ,j,kp)-w0(im,j ,kp))*dxi + (u0(i ,j,kp)-u0(i ,j,k ))*dzhi(kp))**2 & |
|
+ ((w0(i ,j,k )-w0(im,j ,k ))*dxi + (u0(i ,j,k )-u0(i ,j,km))*dzhi(k ))**2 & |
|
+ ((w0(ip,j,k )-w0(i ,j ,k ))*dxi + (u0(ip,j,k )-u0(ip,j,km))*dzhi(k ))**2 & |
|
+ ((w0(ip,j,kp)-w0(i ,j ,kp))*dxi + (u0(ip,j,kp)-u0(ip,j,k ))*dzhi(kp))**2 ) |
|
|
|
strain2 = strain2 + 0.125 * ( & |
|
+ ((u0(i ,jp,k)-u0(i ,j ,k))*dyi + (v0(i ,jp,k)-v0(im,jp,k))*dxi)**2 & |
|
+ ((u0(i ,j ,k)-u0(i ,jm,k))*dyi + (v0(i ,j ,k)-v0(im,j ,k))*dxi)**2 & |
|
+ ((u0(ip,j ,k)-u0(ip,jm,k))*dyi + (v0(ip,j ,k)-v0(i ,j ,k))*dxi)**2 & |
|
+ ((u0(ip,jp,k)-u0(ip,j ,k))*dyi + (v0(ip,jp,k)-v0(i ,jp,k))*dxi)**2 ) |
|
|
|
strain2 = strain2 + 0.125 * ( & |
|
+ ((v0(i,j ,kp)-v0(i,j ,k ))*dzhi(kp) + (w0(i,j ,kp)-w0(i,jm,kp))*dyi)**2 & |
|
+ ((v0(i,j ,k )-v0(i,j ,km))*dzhi(k ) + (w0(i,j ,k )-w0(i,jm,k ))*dyi)**2 & |
|
+ ((v0(i,jp,k )-v0(i,jp,km))*dzhi(k ) + (w0(i,jp,k )-w0(i,j ,k ))*dyi)**2 & |
|
+ ((v0(i,jp,kp)-v0(i,jp,k ))*dzhi(kp) + (w0(i,jp,kp)-w0(i,j ,kp))*dyi)**2 ) |
|
|
|
ekm(i,j,k) = (mlen*damp(i,j,k)) ** 2. * sqrt(2. * strain2) |
|
ekh(i,j,k) = ekm(i,j,k) * prandtli |
|
end do |
|
end do |
|
end do |
Performance audit: inefficient compute patterns in the main time loop
A code audit of the per-timestep hot path (advection → subgrid → wall functions/IBM → Poisson → integration → halos/BCs → thermodynamics) found a number of systemic performance issues. The recurring theme is memory-bandwidth waste: the model makes an estimated 15–25+ full-grid passes per RK3 substep on bookkeeping (zeroing, copying, snapshot/diffing) in addition to the physics, and the build configuration actively prevents SIMD vectorization.
All line references are permalinks to 21647cc. Findings marked ✅ were verified by hand in the source; the rest come from a systematic file-by-file audit.
Tier 0 — build configuration (free performance, zero code risk)
✅ Release builds trap floating-point exceptions.
-ffpe-trap=invalid,zero,overflow(GNU),-fpe0(Intel) and-K trap=...(Cray) sit in the global flags, so they apply to-O3builds:u-dales/CMakeLists.txt
Lines 42 to 45 in 21647cc
FPE trapping forbids the compiler from speculating/masking FP operations, which blocks vectorization across the whole code. These flags should move to the Debug configuration only.
No architecture targeting. Release is bare
-O3; GNU then targets generic SSE2. Add-march=native(or a cluster-appropriate-x/-xHostflag for Intel) to enable AVX2/AVX-512, and consider-fno-math-errnoso gfortran can vectorizesqrt/log.No OpenMP. The code is pure MPI; a hybrid mode would help at high core counts where the Poisson transposes become latency-bound. (Long-term item.)
Tier 1 — hot-loop defects (every RK3 substep, full grid)
Wrong loop nesting (k or j innermost over
(i,j,k)arrays) ✅The innermost loop must iterate
i(column-major); these nests stride a full i×j plane per innermost iteration, so essentially every access is a cache miss:Vertical advection in
advecc_2nd,advecu_2nd,advecv_2nd(j,i,korder). Noteadvecw_2ndis already correct and fused with the horizontal pass — it is the template for fixing the other three (which also do horizontal and vertical advection in two separate grid passes):u-dales/src/advec_2nd.f90
Lines 72 to 87 in 21647cc
Pressure-gradient velocity correction in
tderive(i,j,korder):u-dales/src/modpois.f90
Lines 1049 to 1059 in 21647cc
The Smagorinsky closure runs
k,i,jwithjinnermost across a ~36-element strain-rate stencil, and uses** 2.with a real exponent (may compile topow()). The Vreman loop in the same file is correctly ordered:u-dales/src/modsubgrid.f90
Lines 217 to 269 in 21647cc
periodicEBcorr(k innermost + 2 allreduces per call),fixuinf1(jinnermost),shiftedPBCs(k innermost, plus a per-grid-pointsin()of an i-only quantity) inmodforces.f90.Poisson solver (
modpois.f90) ✅Pencil work arrays allocated/deallocated every call (3× per step); should be persistent module arrays like
p/rhsalready are:u-dales/src/modpois.f90
Lines 442 to 444 in 21647cc
FFTs execute line-by-line 1D transforms with a copy into a scratch vector and an unpack per pencil line; the y-direction gather
py(i,:,k)is strided on top. FFTW's advanced (plan_many) interface removes the per-line call overhead and copies; the separatepx = px*facnormalization passes can be folded into the unpack:u-dales/src/modpois.f90
Lines 479 to 501 in 21647cc
Dead diagnostics paid for unconditionally every substep:
Fxy = pzandFxyz = pzfull-field copies are consumed only by commented-out fielddump code;rhs = p(...)(L434) is similar;fillpsrecomputes the identical divergence a second time intodpupdx/dpvpdy/dpwpdz:u-dales/src/modpois.f90
Line 550 in 21647cc
solmpjdeclares a full-3D automatic arraydper call (also a stack hazard).IBM (
modibm.f90) ✅ibmwallfunsnapshotsup/vp/wp/thlpinto a freshlyallocated full-size array and diffs whole fields afterwards (~16 full-grid sweeps + a heap allocation per substep) — purely to recover stress/flux deltas that the wall functions apply only at sparse facet cells. Accumulating directly intotau_x/thl_fluxinside the facet-section loop (the value is already in hand there) eliminates all of it:u-dales/src/modibm.f90
Lines 1191 to 1231 in 21647cc
bottomdoes the same 8 full-grid sweeps even whenlbottom = .false.(delta is nonzero only on thekbplane):u-dales/src/modibm.f90
Lines 2026 to 2029 in 21647cc
Wall functions recompute
log/sqrtof time-constant geometry (log(dist/z0)etc., 3–4 transcendentals per facet section per grid per substep — millions of calls per step) that could be precomputed per section ininitibmwallfun.Time integration (
tstep.f90) ✅tstep_integrateends with 7 unconditional whole-array zero-fills (qtp=0.in dry runs,e12p=0.without the one-equation model, …) plus 7 whole-array copies (um=u0…) at substep 3 — ~14 full-field bookkeeping passes that could be fused into the integration nest or replaced by pointer swaps:u-dales/src/tstep.f90
Lines 317 to 333 in 21647cc
The scalar update iterates
n(4th, slowest index) innermost inside the(k,j,i)nest; hoistdo noutside.thl0c(...) = thl0(...)is a separate full-field pass that could be fused into thethl0update loop.Kappa scheme (
advec_kappa.f90)advecc_kappazeroes two full-size automatic arrays three times each per call, updatesvarpover the whole halo region in three separate whole-array passes, and calls anexternallimiter functionrlimonce per face — which cannot be inlined, so the inner loops cannot vectorize. A single flux array with an inlined, branch-free (merge/sign) limiter is ~3–4× less memory traffic.advecc_upwsimilarly allocates its work array per call and makes 6 grid passes.Tier 2 — communication patterns (hurts at scale)
~18 + 2·nsv sequential halo exchanges per substep, one variable at a time in
halos(plusekm/ekh/pup/pvp/pwp/p/pres0elsewhere). The*mfields (um,vm,wm,thlm,qtm,svm) are exchanged every substep although they change only once per full RK3 step — redundant 2 of 3 times:u-dales/src/modboundary.f90
Lines 79 to 93 in 21647cc
Scattered scalar collectives that could be batched into single packed reductions: 2 allreduces for CFL/diffusion in
tstep_update, up to 4 per substep inmasscorr(one branch reduces a whole 3D field to get one scalar), 2 inperiodicEBcorr, 2 slab-average collectives per substep withBCtopm_pressure, 7 facet-sized allreduces per step withlwritefac(ALLREDUCE where REDUCE to rank 0 suffices).Legacy
excjs/excisallocate 4 buffers per call and post blocking ordered receives after the sends; persistent buffers + IRECV-first + WAITALL.The energy-balance facet solve runs entirely on rank 0 while all other ranks wait, followed by 5 separate broadcasts (
modEB.f90); facets are independent and could be distributed. The dense longwave exchange recomputesfacem(m)*boltz*facT(m,1)**4inside the O(nfcts²) loop instead of precomputing an O(nfcts) emission vector.Tier 3 — smaller / conditional
scalsourcesweeps the entire grid per source per scalar to deposit a 3σ Gaussian ball, withsqrt/exp/erfinside the mask — per-source index bounds computed once (asvegetation.f90already does; it is the model citizen here) make this O(source volume).✅ Duplicated top-BC calls:
fluxtopscal/valuetopscalare each called twice, but both already updatesv0andsvminternally — the second call is 100% duplicate work (pattern copied from thefluxtop(qtm)/fluxtop(qt0)pairs, where it is needed):u-dales/src/modboundary.f90
Lines 238 to 248 in 21647cc
Vreman/Smagorinsky post-passes (
ekm = ekm + numol,ekh = ekm*prandtli,damp = max(damp,dampmin)) are 3–5 extra whole-array sweeps (including halos) that fold into the closure loop; thedamparray itself is dead weight (Piomelli damping is commented out, so it is written as 1.0 and multiplied everywhere).sbshr/sbbuo/sbdissare stored to full 3D arrays then summed intoe12pin a fourth pass.Dry runs still pay moist costs:
qtpzeroing/updates,qt0av/ql0avslab averages (each a full-grid sweep + MPI collective) and the fullthv0formula run even whenlmoist = .false..nudgeoperates on(:,:,k)sections including ghost cells;fromztopallocates its 1D work arrays per call;checksimmakes 3 separate sweeps + 4 collectives where 1 sweep + 2 collectives suffice (though it is properly time-gated).Possible correctness bugs found in passing (may deserve separate issues)
wallfunheat:elseif (is_equal(norm, -yhat)) then flux = bctfxm— almost certainly should bebctfym(u-dales/src/modibm.f90
Line 1543 in 21647cc
modboundary.f90xm_periodic/ym_periodic: theloneeqnblocks use loop variablemafter thedo m = 1, ihloop ends, soe120/e12mcopy an out-of-range-by-one plane.yqi_profile:do i = jb - 1, ie + 1— lower bound should beib - 1(u-dales/src/modboundary.f90
Line 1071 in 21647cc
thermoNewton–Raphson branch (lqlnr): iteration is uncapped andniteris used uninitialized.EB/intqH:fachf/facefare zeroed on every call including substeps 1–2, silently discarding substep-1/2 flux accumulations — worth checking whether that is intended.What is already good
Precomputed reciprocal metrics mean almost no divisions in inner loops; FFT plans and tridiagonal coefficients are built once at init; the batch Thomas solve is textbook-vectorized; the runtime IBM path uses sparse point lists rather than masked sweeps;
vegetation.f90is exemplary (sparse lists, init-time precomputation, feature gating); and most loop nests are correctly ordered — the defects above are exceptions, not the rule.Suggested attack order
-march=native) — one-line edits, then rebenchmark; possibly a large fraction of the gap on its own.tstep_integratezero/copy passes.plan_many.*mexchanges except on substep 1 — the biggest win at high rank counts.Before the larger refactors, profile one representative urban case (gprof / VTune /
-pg+ a mid-size run) to confirm how time splits between Poisson, advection, subgrid and IBM.