Weird behaviour when using optimization

Hello everyone,

I am relatively new to Fortran. I encountered an issue with the code below when I use the optimization flag `-O3` with gfortran 14.2 on Ubuntu.

The code:

module m

    implicit none

    contains

    subroutine foo(done)
        logical, intent(out) :: done

        if (10 < 5) then
            done = .TRUE.
        end if

    end subroutine

end module


program demo
    use m
    implicit none
    logical :: done

    done = .FALSE.

    call foo(done)

    print *, "done: ", done

end program demo
  • Compiling that piece of code without optimization, the result is false:
gfortran -o demo main.f90
./demo
done: F
  • Compiling with `-O3` the result is true:
gfortran -O3 -o demo main.f90
./demo
done: T

Why does this happen? Is there any tool that can detect such behaviors? can a tool like fortitude be able to warn the user about the potential problem?

Best regards,

Simo

1 Like

In the subroutine, since done is intent(out)and is not set later, it is uninitialized and can be either .true. or .false. upon exit. To get deterministic output declare it intent(in out).

2 Likes

How to detect that in large codes? is there any tool that can analyze the Fortran code and show a warning about that when optimization is used?

Some compilers can detect this at run-time,

> nagfor -C=undefined unassigned.f90
NAG Fortran Compiler Release 7.2(Shin-Urayasu) Build 7203
[NAG Fortran Compiler normal termination]
> ./a.out
Runtime Error: unassigned.f90, line 28: Reference to undefined variable DONE
Program terminated by fatal error
Aborted (core dumped)

Details explained here..

I thought that ifx supports this too with the -check uninit option. Maybe it only applies to integers and reals?

This is unrelated to optimization, that is a red herring. The final print statement is simply printing an undefined value.

The returned value can be any random bit pattern, not just those that correspond to that fortran compiler’s patterns for .true. and .false.. In this case, it is a bit of a mystery where those bits could come from.

1 Like

These types of errors can be avoided if you assign directly:

done = 10 < 5
3 Likes

ifort supported that flag directly, but ifx ditched it in favor of an LLVM-based sanitizer —which, imho, has way too many preconditions to be really useful.

Because of that the -check all flag in ifx does not imply uninit by default.

1 Like

The observed behavior is indirectly related to optimization, as compilers tend to initialize differently the variables depending on the optimization level. But the code is invalid anyway.

Unfortunately, I only have access to gfortran and intel compilers. I tried both ifx and ifort, but none issued a warning.

What other options, if any besides the paid NAG Fortran compiler, should one consider for large codebases, on the order of tens of thousands of lines of code?

That’s definitely related to optimization. I know that this is somehow an undefined behavior but when optimization is enabled the compiler for some reason does not execute the statement before the subroutine call that assigns `done`.

Example below (see the comment):

module m
    implicit none

    contains
        subroutine foo(k)
            integer , intent(out) :: k
        end subroutine foo
end module m
program demo

    use m
    integer :: k

    k = 55        !<<<<<<<<<<<<< with optimization this has no effect!!!
    call foo(k)
    print *, "k = ", k

end program demo

Results:

# without optimization, the output is:
k = 55
# with optimization, the output is:
k =  32636 # or random integer

By default they won’t, you have to specify some compilation options. With gfortran -Wall enables many warnings and -fcheck=all enables many runtime checks. With ifort/ifx this is -warn and -check

You said it: this is an undefined behavior. In that case a compiler is free to do whatever he wants (including starting WWIII, usual joke…). In your latter example the compiler is actually doing what you have asked him to do: optimizing. Seing that k is intent(out) in foo(k), he concludes that the k = 55 assignment doesn’t matter at all and thus it does not generate the corresponding code.

1 Like

I meant that I tried with all those checks in gfortran, ifx, ifort but none worked.

This is the output when compiling your latter example with gfortran:

    5 |         subroutine foo(k)
      |                        1
Warning: Dummy argument 'k' at (1) was declared INTENT(OUT) but was not set [-Wunused-dummy-argument]

Thank you for trying that. I know in that case it is much easier for a compiler or a linter to find unused variables. The original problem I was referring to is not restricted to the “simple” example I provided in the original post that can be caught by inspection.

I am running a very large code written in Fortran by someone else and sometimes I get some non-deterministic behavior which I suspect is related to these kind of issues. Inspecting the code manually is nearly impossible.

1 Like

I would try using gfortran with:

-finit-logical=<true|false>

Other options can be found here: Code Gen Options (The GNU Fortran Compiler)

Relying on implicit initialization to a default value is a mistake in Fortran. If addition of these flags make the code run deterministically, then you’re likely right.

1 Like

You could try fpt: WinFPT and ftp - Fortran Engineering - Summary.

Maybe @Jcollins can comment if this bug can be caught.

You could try running the executable under valgrind which will detect memory reads of memory locations that were never written to. That will not catch all that NAG Compiler catches and it might flag a code line that is unhelpful. I recommend the NAG Compiler for your needs.

1 Like

I like this integer example better than the logical example because for the integer every random bit pattern results in a valid integer value, but in the logical case only two of those patterns result in valid fortran logical values. And that is what you are seeing here (in both cases), random (meaning arbitrary in this context) bit patterns.

There could be two things that are occuring, maybe even both together. First, the compiler sees

subroutine foo(k)
   integer , intent(out) :: k
end subroutine foo

The intent(out) tells the compiler that it can ignore any incoming value of k. It can reset it to zero, it can keep its original value, it can reset it to whatever is in the contents of register 0 at that moment, it can examine an environment variable, and so on. It can do whatever it wants. The fortran way of describing that state is that the value is undefined. Maybe the compiler does something different with different options. Maybe there is an option to initialize values to zero, or to some other given bit pattern. Maybe the behavior depends on an optimization level option. Those things are all allowed because those options do not violate any requirements in the standard. But regardless, the value is undefined from the perspective of fortran.

The standard does define in other situations what happens with intent(out) declarations. To give one example, if the dummy argument is allocatable, then it is deallocated upon entry to the subroutine. If it is a complicated object, say a linked list with a million levels, that simple declaration then results in the recursive deallocations of all subobjects within that derived type. So intent(out) should not be taken as a benign, passive, attribute in general, it should be regarded as active, and meaningful, with purpose.

Ok, then the second thing that the compiler sees is

k = 55
call foo(k)

The compiler has the explicit interface for foo(), so it knows that the argument is intent(out). Let’s assume for the moment that the module is compiled separately from the main program, so it does not know what is within foo(), but only its interface. So just from that interface, it knows that foo() is allowed to ignore the incoming value of k and return something else. That something else can include an undefined value, that possibility is allowed. So if the value of k is immediately overwritten by foo(), why should it assign the value just before? It knows that it doesn’t need to do that, that statement can be ignored, and it can be ignored at any optimization level. It is the same as, for example

k = 55
k = 42

The compiler knows that the initial assignment has no consequences, so it is allowed to ignore it at any optimization level.

How can the programmer know if a statement like that is removed? I think only by looking at the compiled code (either intermediate code or the assembler code), or by using a debugger to step through the code. However that examination occurs, the compiler is allowed to remove dead code, and this is a type of dead code. You might think you could change the code to look at the value:

k = 55
write(*,*) k
k = 42

But that changes the program, and now the first assignment is no longer dead – the assignment must either occur as written or the program must behave “as if” it is executed that way. To give an example of the latter, this last code could be executed as

write(*,*) 55
k = 42

That is, the value of 55 could be sent directly to the i/o library without it ever being placed into the memory location associated with the variable k. And further, this behavior could occur with any level of optimization, the language standard does not care as long as it behaves “as if” the original code would behave.

So that is what I meant when I said that optimization was a red herring. It is something that is irrelevant, a distraction, something to lead you astray as you search for the problem. This is not an error introduced by the compiler optimization, rather the real issue is that the return value from the subroutine is undefined.

A common expression in fortran when the program behavior is undefined in some way is that “anything can happen, it could even start WW III”. Printing an undefined value in fortran is like that, it could even start WW III. These days, that expression doesn’t seem very funny.

3 Likes

I’ve tried the code a bit with flang-20 also, and the latter keeps the value of actual arguments as-is even with intent(out). I think it would be nicer for compilers to keep the value of the argument with intent(out) intact to avoid unnecessary troubles (even if the value is supposed to be defined in the routine), unless it is a derived-type object.

That behavior is inconsistent with the standard. For example, an intent(out), allocatable array is deallocated upon entry, removing its bounds in the process. If instead it is treated as intent(inout), then its original allocation would be kept along with its original bounds. Another example I gave above is a linked list with a million levels. With intent(out) all of those levels are recursively deallocated, while with intent(inout) the incoming linked list would be kept intact.

1 Like