Search code examples
fortrancompiler-optimizationgfortrancompiler-warnings

gfortran execution bug when compiled with optimization if some variable is declared "intent(out)"


The following Fortran code should give a value close to 2/3.

! File "buggy.f90".

program buggy

  implicit none
  integer, parameter :: n = 1000
  integer :: i, k
  logical :: c1, c2
  real, parameter :: q2 = 0.5
  real :: p2

  k = 0
  do i = 1, n
     c1 = .false.
     c2 = .false.
     do
!        write(*,*) c1
        if (c1 .or. c2) exit
        call proc(c1)
        if (c1) then
           k = k + 1
        else
           call random_number(p2)
           if (p2 >= q2) c2 = .true.
        end if
     end do
  end do
  write(*,*) "Result should be close to 2/3: ", real(k)/n

contains

  subroutine proc(c1)

    implicit none
    logical, intent(out) :: c1
!    logical, intent(inout) :: c1
    real, parameter :: q1 = 0.5
    real :: p1

    call random_number(p1)
    if (p1 >= q1) c1 = .true.
    
  end subroutine proc
  
end program buggy

I obtain the correct value when I compile with ifort or with gfortran -O0 (executables buggy_ifort and buggy_gfortran-O0 in the Make file below).

# Make file.

ifort = ifort13
gfortran = gfortran-11

all: buggy_ifort buggy_gfortran-O0 buggy_gfortran-O1 buggy_gfortran-O1_correct

buggy_ifort: buggy.f90
    @ echo "\nCompiling \"buggy_ifort\"."
    @ $(ifort) -warn declarations -check uninit -warn argument_checking -warn uninitialized -warn usage -implicitnone -warn uncalled -warn unused \
        -std95 -warn all -o buggy_ifort buggy.f90
    @ echo "\nDone."

buggy_gfortran-O0: buggy.f90
    @ echo "\nCompiling \"buggy_gfortran-O0\"."
    @ $(gfortran) -Wall -Wextra -pedantic -std=f95 -O0 -o buggy_gfortran-O0 buggy.f90
    @ echo "\nDone."

buggy_gfortran-O1: buggy.f90
    @ echo "\nCompiling \"buggy_gfortran-O1\"."
    @ $(gfortran) -Wall -Wextra -pedantic -std=f95 -O1 -o buggy_gfortran-O1 buggy.f90
    @ echo "\nDone."

buggy_gfortran-O1_correct: buggy.f90
    @ echo "\nCompiling \"buggy_gfortran-O1_correct\"."
    @ $(gfortran) -Wall -Wextra -pedantic -std=f95 -O1  \
        -o buggy_gfortran-O1_correct buggy.f90  \
        -fno-tree-ccp                       \
        -fno-tree-ch                        \
        -fno-tree-dominator-opts        \
        -fno-tree-fre               
    @ echo "\nDone."

# The output of buggy_gfortran-O1_correct is wrong if any of the -fno-tree-* above options is removed.

# The compilation of buggy_gfortran-O1_correct produces the following message:
# "   buggy.f90:18:12:
# 
#        18 |         if (c1) then
#           |            ^
#     Warning: ‘c1’ may be used uninitialized in this function [-Wmaybe-uninitialized]   ".

# This message disappears if -fno-tree-fre is removed.
# It is replaced by
# "   buggy.f90:16:19:
# 
#        16 |         if (c1 .or. c2) exit
#           |                   ^
#     Warning: ‘c1’ may be used uninitialized in this function [-Wmaybe-uninitialized]   "
# if -fno-tree-ch is removed.

# If -fno-tree-ch is replaced by -fno-inline-functions-called-once, or if these two options
# are present, the output is correct but all the warning messages disappear.

clean:
    @ rm buggy_ifort buggy_gfortran-O0 buggy_gfortran-O1 buggy_gfortran-O1_correct

However, with gfortran -O1 (executable buggy_gfortran-O1), I always get the same wrong result: exactly 1. In all the cases, no warning is given.

Strangely, a correct value is produced with gfortran -O1 if the write(*,*) c1 statement is uncommented at the beginning of the inner loop, or if intent(out) is replaced by intent(inout) in the proc subroutine. Disabling some of the optimizations made active by -O1 also provides the right result (executable buggy_gfortran-O1_correct. See comments in the Make file); in this case, however, I get a warning that the c1 variable may be used uninitialized!

The difference seems indeed to come from the value assigned to c1. My understanding is that, with c1 declared as intent(out) in the proc subroutine, c1 should retain the value it had before the call to proc if it is not modified in the subroutine.

So, if it is the standard interpretation, why does the execution fail with gfortran and the -O1 (or above) general optimization, unless some specific optimizations are disabled? And if it is not the standard interpretation, why does neither ifort nor gfortran issue a warning message given the compilation directives in the Make file?

Note: This question is not exactly the same as Difference between intent(out) and intent(inout). The purpose is rather to know why the output is wrong when the code is compiled with optimization.

Regarding the effect of intent(out), Note 12.17 in Sect. 12.4.1.1 of the Fortran 95 standard indeed says that

INTENT(OUT) means that the value of the argument after invoking the procedure is entirely [my emphasis] the result of executing that procedure. If there is any possibility that an argument should retain its current value rather than being redefined, INTENT(INOUT) should be used rather INTENT(OUT), even if there is no explicit reference to the value of the dummy argument.

Still, even if the compiler is not required to issue a warning in that case, the apparition of warning messages seems quite erratic with gfortran, depending on the optimizations used.


Solution

  • You quote the reason for the problem in the question itself

    INTENT(OUT) means that the value of the argument after invoking the procedure is entirely [your emphasis] the result of executing that procedure. If there is any possibility that an argument should retain its current value rather than being redefined, INTENT(INOUT) should be used rather INTENT(OUT), even if there is no explicit reference to the value of the dummy argument.

    You cannot reference the previous value of the argument with intet(out). It is against the standard. If you do so, the program is no longer (standard) Fortran. The standard does not specify what should happen, the result will be undefined. Other languages call this the "undefined behaviour" (UB).

    The compiler is not required by the standard to diagnose such a bug from the programmer. There is no requirement for that in the standard. Unless the compiler itself promises that to be caught somewhere, it is not required to diagnose this.

    Various compilers offer various error or undefined behaviour check flags and tools, but they are not guaranteed to catch everything.