# Optimisation of invariant \`if\` conditions in a loop

**URL:** <https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191>\
**Category:** Uncategorized\
**Created:** [February 9, 2025, 11:58am UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191 "2025-02-09T11:58:38Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![pcosta](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/pcosta/32/5504_2.png) [@pcosta](https://fortran-lang.discourse.group/u/pcosta)\
**Post date:** [February 9, 2025, 11:58am UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/1 "2025-02-09T11:58:38Z")

</div>

Hello,

I am considering revising some of the CPP macros in a code. However, in some cases, I have loops with this kind of logic:

```Fortran
   do k = 1, nz
     do j = 1, ny
       do i = 1, nx
         diff1 = field(i, j, k) + 1.
         diff2 = field(i, j, k) - 1.
 #if defined(_OPTION_1)
 #if defined(_OPTION_1_PLUS)
         res = diff1 + diff2
 #else
         res = diff1 * diff2
 #endif
 #else
         res = diff1 - diff2
 #endif
         field(i, j, k) = res
       end do
     end do
   end do

```

I would think that these CPP macros can be replaced with plain `if` conditions. The compiler could simply write an optimized loop for each of the options since they can be determined beforehand.

In other words, take these two examples (I added OpenACC directives for optional testing on a GPU, e.g., with `nvfortran`).

### Example 1:

Can be compiled and run with:  
`gfortran -cpp -D_OPTION_1 -D_OPTION_1_PLUS test_cpp.f90 -o test_cpp; ./test_cpp`

```Fortran
program p
  implicit none
  integer, parameter :: nx = 5, ny = 5, nz = 5
  integer :: i, j, k
  real :: field(nx, ny, nz)
  real :: diff1, diff2, res
  integer :: iunit
  !
  ! initialize and move data to GPU
  !
  do k = 1, nz
    do j = 1, ny
      do i = 1, nx
        field(i, j, k) = i + j*10. + k*100.
      end do
    end do
  end do
  !$acc enter data copyin(field)
  !
  ! run kernel
  !
  !$acc parallel loop collapse(3) default(present) private(diff1,diff2,res)
  do k = 1, nz
    do j = 1, ny
      do i = 1, nx
        diff1 = field(i, j, k) + 1.
        diff2 = field(i, j, k) - 1.
#if defined(_OPTION_1)
#if defined(_OPTION_1_PLUS)
        res = diff1 + diff2
#else
        res = diff1 * diff2
#endif
#else
        res = diff1 - diff2
#endif
        field(i, j, k) = res
      end do
    end do
  end do
  !
  ! move data to GPU and save
  !
  !$acc update self(field)
  open(newunit=iunit, file="output.bin", form="unformatted", access="stream", status="replace")
  write(iunit) field
  close(iunit)
  print*, 'field(3,3,3) = ', field(3,3,3)
end program p

```

### Example 2

Can be compiled and run with:  
`gfortran test_nocpp.f90 -o test_nocpp; ./test_nocpp 1 1`

```auto
program p
  implicit none
  integer, parameter :: nx = 5, ny = 5, nz = 5
  integer :: i, j, k
  real :: field(nx, ny, nz)
  real :: diff1, diff2, res
  integer :: iunit
  integer :: option1, option1_plus
  character(len=32) :: arg
  !
  ! get options from the command line
  !
  if (command_argument_count() >= 1) then
    call get_command_argument(1, arg)
    read(arg, *) option1
  else
    option1 = 0
  endif
  if (command_argument_count() >= 2) then
    call get_command_argument(2, arg)
    read(arg, *) option1_plus
  else
    option1_plus = 0
  endif
  !
  ! initialize and move data to GPU
  !
  do k = 1, nz
    do j = 1, ny
      do i = 1, nx
        field(i, j, k) = i + j*10. + k*100.
      end do
    end do
  end do
  !$acc enter data copyin(field, option1, option1_plus)
  !
  ! run kernel
  !
  !$acc parallel loop collapse(3) default(present) private(diff1,diff2,res)
  do k = 1, nz
    do j = 1, ny
      do i = 1, nx
        diff1 = field(i, j, k) + 1.
        diff2 = field(i, j, k) - 1.
        if (option1 == 1) then
          if (option1_plus == 1) then
            res = diff1 + diff2
          else
            res = diff1 * diff2
          endif
        else
          res = diff1 - diff2
        endif
        field(i, j, k) = res
      end do
    end do
  end do
  !
  ! move data to host and save
  !
  !$acc update self(field)
  open(newunit=iunit, file="output.bin", form="unformatted", access="stream", status="replace")
  write(iunit) field
  close(iunit)
  print*, 'field(3,3,3) = ', field(3,3,3)
end program p

```

### Questions:

- Would any of the current compilers fail to optimize the two programs to make them equivalent in performance?
- How would you check this - would one need to read and understand the Intermediate Representation/Assembly?

Thanks!

EDIT: Related: [Loop-invariant code motion - Wikipedia](https://en.m.wikipedia.org/wiki/Loop-invariant_code_motion)

---

<div class="post-metadata">

**Author:** ![ivanpribec](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/ivanpribec/32/3290_2.png) [@ivanpribec](https://fortran-lang.discourse.group/u/ivanpribec)\
**Post date:** [February 9, 2025, 2:11pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/2 "2025-02-09T14:11:13Z")

</div>

Testing your Fortran-only example (without the preprocessor):

```txt
~/fortran/loop_unswitch$ gfortran -O3 -fopt-info p.f90
p.f90:42:12: optimized: loop with 5 iterations completely unrolled (header execution count 178992760)
p.f90:30:12: optimized: loop with 5 iterations completely unrolled (header execution count 178992760)
p.f90:29:10: optimized: loop with 5 iterations completely unrolled (header execution count 35807143)
p.f90:41:10: optimized: unswitching outer loop 4 on 'if' with condition: option1.8_161 == 1
p.f90:41:10: optimized: unswitching outer loop 4 on 'if' with condition: option1_plus.9_163 == 1
...

```

Seems to be among the `-O3` optimizations; can be enabled explicitly at \>= `-01`,

```txt
$ gfortran -O2 -funswitch-loops -fopt-info p.f90 
p.f90:42:12: optimized: unswitching outer loop 4 on 'if' with condition: option1.8_28 == 1
p.f90:42:12: optimized: unswitching outer loop 4 on 'if' with condition: option1_plus.9_29 == 1
...

```

Edit: apologies for the confusing renaming, my file `p.f90` was the `test_nocpp.f90` source file.

---

<div class="post-metadata">

**Author:** ![pcosta](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/pcosta/32/5504_2.png) [@pcosta](https://fortran-lang.discourse.group/u/pcosta)\
**Post date:** [February 9, 2025, 3:50pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/3 "2025-02-09T15:50:40Z")

</div>

> [@ivanpribec](#):
>
> `-fopt-info`

Thanks Ivan!

I see _ifx_ and _nvfortran_ also performing it, at `-O2` level (_ifx_’s default):

```shell
$ nvfortran -O2 -acc -Minfo=accel,opt test_nocpp.f90
p:
     29, Outer loop unrolled 5 times (completely unrolled)
     35, Generating enter data copyin(option1,option1_plus,field(:,:,:))
     39, Generating NVIDIA GPU code
         40, !$acc loop gang, vector(128) collapse(3) ! blockidx%x threadidx%x
         41, ! blockidx%x threadidx%x collapsed
         42, ! blockidx%x threadidx%x collapsed
     39, Generating default present(field(:,:,:))
     42, Generating implicit firstprivate(option1_plus,option1)
         Invariant if transformation
         Loop unrolled 5 times (completely unrolled)
     61, Generating update self(field(:,:,:))

```

```shell
$ ifx -O2 -qopt-report=1 test_nocpp.f90 && cat test_nocpp.optrpt

Begin optimization report for: MAIN__

LOOP BEGIN at test_nocpp.f90 (28, 3)
    remark #25436: Loop completely unrolled by 5

    LOOP BEGIN at test_nocpp.f90 (29, 5)
        remark #25436: Loop completely unrolled by 5

        LOOP BEGIN at test_nocpp.f90 (30, 7)
            remark #25436: Loop completely unrolled by 5
        LOOP END
    LOOP END
LOOP END

LOOP BEGIN at test_nocpp.f90 (40, 3)
<Predicate Optimized v1>
    remark #25423: Invariant If condition at line 45 hoisted out of this loop
    remark #25436: Loop completely unrolled by 5

    LOOP BEGIN at test_nocpp.f90 (41, 5)
        remark #25436: Loop completely unrolled by 5

        LOOP BEGIN at test_nocpp.f90 (42, 7)
            remark #25436: Loop completely unrolled by 5
        LOOP END
    LOOP END
LOOP END

LOOP BEGIN at test_nocpp.f90 (40, 3)
<Predicate Optimized v2>
    remark #25436: Loop completely unrolled by 5

    LOOP BEGIN at test_nocpp.f90 (41, 5)
        remark #25436: Loop completely unrolled by 5

        LOOP BEGIN at test_nocpp.f90 (42, 7)
            remark #25436: Loop completely unrolled by 5
        LOOP END
    LOOP END
LOOP END
=================================================================

---- Begin Inlining Report ----
COMPILE FUNC: MAIN__

---- End Inlining Report ------

```

---

<div class="post-metadata">

**Author:** ![ivanpribec](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/ivanpribec/32/3290_2.png) [@ivanpribec](https://fortran-lang.discourse.group/u/ivanpribec)\
**Post date:** [February 11, 2025, 9:08pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/4 "2025-02-11T21:08:12Z")

</div>

That’s good to know about [loop unswitching](https://en.wikipedia.org/wiki/Loop_unswitching) . I think in this case it is clear that it is worth it.

As a matter of personal taste I think I’d prefer separate loop nests at the expense of loop duplication.

---

<div class="post-metadata">

**Author:** ![aledinola](https://avatars.discourse-cdn.com/v4/letter/a/3ec8ea/32.png) [@aledinola](https://fortran-lang.discourse.group/u/aledinola)\
**Post date:** [February 12, 2025, 11:11pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/5 "2025-02-12T23:11:23Z")

</div>

The problem of these speed-up techniques like loop unswitching is that they make the code less maintainable.

As far as I understand, you have to move the conditional outside of the loops and then duplicate the loops. If you need to change the code in the future, you have to remember to do the change twice.

So it’s best if the compiler does the optimization

---

<div class="post-metadata">

**Author:** ![pcosta](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/pcosta/32/5504_2.png) [@pcosta](https://fortran-lang.discourse.group/u/pcosta)\
**Post date:** [February 12, 2025, 11:20pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/6 "2025-02-12T23:20:28Z")

</div>

> [@aledinola](#):
>
> As far as I understand, you have to move the conditional outside of the loops and then duplicate the loops. If you need to change the code in the future, you have to remember to do the change twice.

Indeed. In my example you’d need to repeat the loop three times.

---

<div class="post-metadata">

**Author:** ![Beliavsky](https://avatars.discourse-cdn.com/v4/letter/b/ba8739/32.png) [@Beliavsky](https://fortran-lang.discourse.group/u/Beliavsky)\
**Post date:** [February 12, 2025, 11:41pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/7 "2025-02-12T23:41:05Z")

</div>

> [@aledinola](#):
>
> The problem of these speed-up techniques like loop unswitching is that they make the code less maintainable.

I think more and more often programmers will not be modifying code by hand but telling an LLM to change it in some way, so this may not be a big problem.

---

<div class="post-metadata">

**Author:** ![ivanpribec](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/ivanpribec/32/3290_2.png) [@ivanpribec](https://fortran-lang.discourse.group/u/ivanpribec)\
**Post date:** [February 13, 2025, 8:57am UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/8 "2025-02-13T08:57:46Z")

</div>

There are other ways of cutting back on code duplication, but it depends on the context how applicable they are. For instance array expressions:

```fortran
! F2023 conditional syntax
op = option1 == 1 ? (option1_plus == 1 ? '+' : '*') : '-'

select case(op)
case('+')
  field = (field + 1) + (field - 1)
case('-')
  field = (field + 1) - (field - 1)
case('*')
  field = (field + 1) * (field - 1)
end select

```

I imagine that @pcosta’s true code is more complicated and that GPU parallelization is desirable, so this solution is probably not applicable.

Another alternative which is a bit more involved would be using a preprocessor like [Fypp](https://fypp.readthedocs.io/en/stable/fypp.html#def-directive) (I haven’t tested if the Fypp syntax below is actually correct):

```auto
abstract interface
   subroutine field_op(field, nx, ny, nz)
      integer, intent(in) :: nx, ny, nz
      real, intent(inout) :: field(nx, ny, nz)
   end subroutine
end interface

procedure(field_op), pointer :: op

! ...

op => option1 == 1 ? (option1_plus == 1 ? PADD : PMULT) : PSUB

call op(field,nx,ny,nz)

! ...

contains

#:def pkernel(NAME, OPERATION)
subroutine NAME(field, nx, ny, nz)
  integer, intent(in) :: nx, ny, nz
  real, intent(inout) :: field(nx, ny, nz)
  integer :: i, j, k
  real :: diff1, diff2, res
  !$acc parallel loop collapse(3) default(present) private(diff1,diff2,res)
  do k = 1, nz
    do j = 1, ny
      do i = 1, nx
        diff1 = field(i, j, k) + 1.
        diff2 = field(i, j, k) - 1.
        res = OPERATION
        field(i, j, k) = res
      end do
    end do
  end do
end subroutine NAME
#:enddef

#! Instantiate the template for different operations
@:pkernel(PADD, diff1 + diff2)
@:pkernel(PMULT, diff1 * diff2)
@:pkernel(PSUB, diff1 - diff2)

```

In any-case I agree with the original [KISS](https://en.wikipedia.org/wiki/KISS_principle) approach. Just wanted to throw out some ideas.

Nvfortran and gfortran probably don’t support the F2023 conditional syntax yet.

---

<div class="post-metadata">

**Author:** ![jwmwalrus](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/jwmwalrus/32/4483_2.png) [@jwmwalrus](https://fortran-lang.discourse.group/u/jwmwalrus)\
**Post date:** [February 13, 2025, 9:54am UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/9 "2025-02-13T09:54:20Z")

</div>

> [@ivanpribec](#):
>
> ```auto
> ! F2023 conditional syntax
> op = option1 == 1 ? (option1_plus == 1 ? '+' : '*') : '-'
> 
> ```

I think enclosing parentheses are required for Fortran’s conditional-expressions —just like in format strings.

As for avoiding code duplication, while keeping it simple/clear, contained procedures should do the trick.

(The optimizer should take care of any perceived overhead.)

---

<div class="post-metadata">

**Author:** ![pcosta](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/pcosta/32/5504_2.png) [@pcosta](https://fortran-lang.discourse.group/u/pcosta)\
**Post date:** [February 13, 2025, 10:00am UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/10 "2025-02-13T10:00:13Z")

</div>

Thanks Ivan,

At the end, I kept it simple and asked an LLM to produce the unswitched loop versions (funny that @Beliavsky predicted this). It goes a bit against my wish to avoid repeating code, but since this is very stable code, I’d prefer that over a solution with metaprogramming at this point.

I found a strange behavior in `ifx -fast` for this code: [Re: Performance of `ifx -fast` when invariant if conditions in loops. - Intel Community](http://community.intel.com/t5/Intel-Fortran-Compiler/Performance-of-ifx-fast-when-invariant-if-conditions-in-loops/m-p/1664979#M175033)

---

<div class="post-metadata">

**Author:** ![RonShepard](https://avatars.discourse-cdn.com/v4/letter/r/a3d4f5/32.png) [@RonShepard](https://fortran-lang.discourse.group/u/RonShepard)\
**Post date:** [February 13, 2025, 11:52pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/11 "2025-02-13T23:52:13Z")

</div>

I did not test these, but here are two more possible ways to rewrite the code. The first uses a single do concurrent block.

```auto
   do concurrent( k=1:nz, j=1:ny, i=1:nx )
      diff1 = field(i, j, k) + 1.
      diff2 = field(i, j, k) - 1.
      if (option1 == 1) then
         if (option1_plus == 1) then
            res = diff1 + diff2
         else
            res = diff1 * diff2
         endif
      else
         res = diff1 - diff2
      endif
      field(i, j, k) = res
   enddo

```

This version uses three separate do concurrent blocks.

```auto
   if (option1 == 1) then
      if (option1_plus == 1) then
         do concurrent( k=1:nz, j=1:ny, i=1:nx )
            diff1 = field(i, j, k) + 1.
            diff2 = field(i, j, k) - 1.
            field(i, j, k) = diff1 + diff2
         enddo
      else
         do concurrent( k=1:nz, j=1:ny, i=1:nx )
            diff1 = field(i, j, k) + 1.
            diff2 = field(i, j, k) - 1.
            field(i, j, k) = diff1 * diff2
         enddo
      endif
   else
      do concurrent( k=1:nz, j=1:ny, i=1:nx )
         diff1 = field(i, j, k) + 1.
         diff2 = field(i, j, k) - 1.
         field(i, j, k) = diff1 - diff2
      enddo
   endif

```

This second version is only a little longer than the first version. It is possible that the compiler will be more aggressive with optimizations with do concurrent than with a regular loop. Of course, that last one could have just the loop body `field(i,j,k)=2.0`, but I’m ignoring that. I wonder if a clever compiler would catch that?

All of these could also be written with array syntax.

```auto
field = 2.0*field
...
field = field * field - 1.0
...
field = 2.0

```

However, the danger with this is always that the compiler might allocate memory, perform the rhs of the operations, and then assign the result, which would be relatively more expensive than the do concurrent or the loop based codes, which will be done with no memory allocations.

---

<div class="post-metadata">

**Author:** ![ivanpribec](https://yyz2.discourse-cdn.com/free1/user_avatar/fortran-lang.discourse.group/ivanpribec/32/3290_2.png) [@ivanpribec](https://fortran-lang.discourse.group/u/ivanpribec)\
**Post date:** [February 14, 2025, 1:03pm UTC](https://fortran-lang.discourse.group/t/optimisation-of-invariant-if-conditions-in-a-loop/9191/12 "2025-02-14T13:03:44Z")

</div>

Do concurrent has the benefit it can be done on GPU with the Nvidia Fortran compiler. In gfortran it also serves as a vectorization hint similar to the [IVDEP directive](https://gcc.gnu.org/onlinedocs/gfortran/IVDEP-directive.html#IVDEP-directive-1):

> For `DO CONCURRENT` constructs this annotation [(IVDEP)] is implicit to all loop control variables.
