Undefined variables

program test
  real(8) :: x1, x2(5)
  real(8), allocatable :: x3(:)
  allocate(x3(5))
  print *, x1
  print *, x2
  print *, x3 
end program

Using GFortran 12.1.1, I get random values for x1 and x2 at each run, but x3 remains filled with zeros. How come?

The variables x1, x2, x3 are being printed out despite their status of ‘undefined’. The behavior of the program is ‘undefined’. Different compilers may print different results.

It is wrong to ask the question “how come?” in such a situation, as far as the Fortran language rules are concerned.

The values being printed out are not predictably random (for x1, x2) or zero (for x3). The values that you see simply happened to be there because some other program put those values into those memory locations.

You can look at the source code of the compiler and libgfortran for the answer, but I think that it is a waste of time to do so.

2 Likes

The value of a variable is undefined until you initialize it (its bytes may contain non zero values at the start of the program).

An allocatable variable is declared on the heap. Reading quickly the Fortran standard, it does not seem guaranteed that it will always be filled with zeros. Note that in C there is malloc() which does not put zeros values and calloc() which puts zeros in the allocated zone (and is therefore slower).

Automatically initializing to zero variables that you would later initialize with your own values would be a waste of CPU time.

2 Likes

That’s why I hate C++ std::vector.

It might be a good idea to re-run the program, perhaps after some time using the computer. I guess the values printed will be different then. gfortran is supposed to issue a warning for this example with the flag -Wmaybe-uninitialized, although I have seen it issuing false positives or not issuing any warning at all, depending on the optimization flag.

Using, and not using, compiler switches to initialise variables and comparing the results is a sensible precautionary check on new code, there are also compiler and run time checks for the use of variables before they have been assigned values. Thing get worrying when those two approaches yield different issues in the code!

Oh, you have many reasons to hate std::vector, std::array, and anything related to matrices in C++. Compated to Fortran it’s ridiculously primitive. :slightly_smiling_face:

Compiling on WSL2 with gfortran -g -ffpe-trap=invalid,overflow -finit-real=snan -fbacktrace undef.f90, as I suggested in a tweet, and running gives

(base) /mnt/c/fortran/test$ ./a.out

Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.

Backtrace for this error:
#0  0x7f7164504700 in ???
#1  0x7f71645038a5 in ???
#2  0x7f716433720f in ???
#3  0x7f71647442cd in ???
#4  0x7f7164746a2b in ???
#5  0x7f7164746dc5 in ???
#6  0x7f7164747dc6 in ???
#7  0x559ba4cd5325 in test
	at /mnt/c/fortran/test/undef.f90:5
#8  0x559ba4cd54b9 in main
	at /mnt/c/fortran/test/undef.f90:8
Floating point exception

I didn’t expect that. What is the “arithmetic operation” here? Implicitly converting snan into strings to print them? Thought that would be valid for any value.

As others have pointed out, anything can happen because the behaviour is undefined with respect to the Fortran standard.

With that said, I think we can still answer your question: my understanding is that since x3 is dynamically allocated here, it requires a request to the OS for more memory and the OS may zero that memory, or fill with random data, in order to protect information written there by a previous process. You cannot rely on this behaviour because it is implementation specific and repeated allocation/deallocation may simply use memory already allocated to your process.

x1 and x2 by contrast are on the stack and hence they likely contain any information left on the stack during the initialisation of the program.

2 Likes

Modifying your program like this:

program test
  real(8), allocatable :: x3(:)
  integer :: i

  do i = 1, 50
    allocate(x3(5))
    print *, x3
    deallocate(x3)
  end do
end program

I obtain that:

$ gfortran essai.f90 && ./a.out
   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000     
   1.1340447011303576E-313   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000     
   1.1340447011303576E-313   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000     
   1.1340447011303576E-313   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000
[...]

Strangely, in gdb I can see the address is always the same:

2: &x3 = (PTR TO -> ( real(kind=8), allocatable (5) )) 0x55555555c910

So why the x3(1) is zero during the first iteration of the loop and non-zero after? :thinking:

Each time I run the program the non-zero value is different.

With ifort/ifx, only zeros are printed.

Not to forget the Fortran language standard does not get into implementation details and considerations such as variables on the stack vs heap are not relevant to the standard.

The key aspect is undefined.and per the standard, it makes it nonconforming to reference an object that is undefined.

The instructions with PRINT on lines 5, 6, and 7 in the original post thus do not conform and the program behavior can be viewed as processor-dependent or better yet, not to be “read much into”.

By doing nothing with the values except printing them you
unfortunately slip through a loop-hole in several compilers
that ends up basically acting like the variables are never being used,
so I added a few lines where I added zero to the values to your example
to reflect that more common case.

If you had done any other operation other than printing on the variables common
compiler switches that should be used during development
would have warned this was not standard-conforming code.

For example, if you add zero to the values and compiler with

gfortran -Wall xx.f90 -o xx
./xx

You would have been warned about X1.

If you preset core you can see you had NaN values in
the static arrays.

If you had set floating point traps it would not have let
you run using the undefined values.

You should always have on as many debug flags as you can when
developing. Some people spent a good deal of time writing those
features because they are so valuable.

At least with gfortran, use -Wall at a minimum whenever
developing new code unless it causes a massive slowdown (which
is rare except with very large high-performance codes that
have long wall-clocks). So just making a simple script that
uses three sets of compiler switches, each of which can help
detect different issues:

#!/bin/bash
#@(#) debug.sh - compiler and run with debug flags
BASE_OPTIONS='
 -fimplicit-none 
 -Wuninitialized 
 -Wextra 
 -Wall 
 -pedantic-errors 
 -fcheck=all 
 -Wimplicit-interface 
 -Wimplicit-procedure 
 -ffree-form 
 -fbacktrace 
 -fmax-errors=3 
'
MORE='
 -finit-real=snan 
 -Wcharacter-truncation 
 -Warray-temporaries 
'
EXTRA='
-ffpe-trap=invalid,zero,overflow
'
set -x
rm -f ./a.out
gfortran -g -O0 $BASE_OPTIONS "$@" && ./a.out
rm -f ./a.out
gfortran -g -O0 $BASE_OPTIONS $MORE "$@" && ./a.out
rm -f ./a.out
gfortran -g -O0 $BASE_OPTIONS $MORE $EXTRA "$@" && ./a.out
exit

And using something like your example

program test
  real(8) :: x1, x2(5)
  real(8), allocatable :: x3(:)
  allocate(x3(5))
  x2=x2+0
  x3=x3+0
  x1=x1+0
  print *, x1
  print *, x2
  print *, x3 
end program
+ gfortran -g -O0 -fimplicit-none -Wuninitialized -Wextra -Wall -pedantic-errors -fcheck=all -Wimplicit-interface -Wimplicit-procedure -ffree-form -fbacktrace -fmax-errors=3 xx.f90
xx.f90:5:9:

    5 |   x1=x1+0
      |         ^
Warning: ‘x1’ may be used uninitialized in this function [-Wmaybe-uninitialized]
+ ./a.out
  -1.4614335721641550E+051
   0.0000000000000000       -1.4614335721641550E+051   0.0000000000000000       -1.4614331294416530E+051   0.0000000000000000     
   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000     

Will not catch everything, but you do get a warning x1 is not set

+ gfortran -g -O0 -fimplicit-none -Wuninitialized -Wextra -Wall -pedantic-errors -fcheck=all -Wimplicit-interface -Wimplicit-procedure -ffree-form -fbacktrace -fmax-errors=3 -finit-real=snan -Wcharacter-truncation -Warray-temporaries xx.f90
+ ./a.out
                       NaN
                       NaN                       NaN                       NaN                       NaN                       NaN
   0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000        0.0000000000000000     

Seeing those NaN values is a red flag and it makes it a lot less likely the
program will inadvertently run with undefined values.

+ gfortran -g -O0 -fimplicit-none -Wuninitialized -Wextra -Wall -pedantic-errors -fcheck=all -Wimplicit-interface -Wimplicit-procedure -ffree-form -fbacktrace -fmax-errors=3 -finit-real=snan -Wcharacter-truncation -Warray-temporaries -ffpe-trap=invalid,zero,overflow xx.f90
+ ./a.out

Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.

Backtrace for this error:
#0  0x7fa93b23cd21 in ???
#1  0x7fa93b23bef5 in ???
#2  0x7fa93b07020f in ???
#3  0x56334a787319 in test
        at /tmp/xx.f90:5
#4  0x56334a7876b7 in main
        at /tmp/xx.f90:11
fwish: line 27: 708172 Floating point exception(core dumped) ./a.out
+ exit

Checking for floating point exceptions stops you early on so you can figure
out what went wrong.

In some classes I used to teach compiler options were introduced early, with an emphasis on
a good set of debug switches. One of the most important features a good compiler has is the ability to
catch errors. New users should always make sure they carefully read the options their compiler has.
Unfortunately many switches are esoteric and complicated but often mixed in with the more important switches, which seems to put off a lot of people. This results in people using compilers for years without realizing there are really useful switches available.

There has been some discussion lately about new courses being created. I would highly recommend that somewhere in those courses someone discusses switches for debugging, switches for performance/production release, switches for profiling, and switches needed for using a debugger.

I have seen codes released into production running 20% slower than they should; codes with easily detetected bugs being put out in the wild; and so on that a good knowledge of readily available compiler options would have prevented.

5 Likes