Pierre de Buyl's homepage

Fortran bitmasks

Bitmasks are useful to store boolean settings. In Fortran, their use remains somewhat confidential in the sense that it is not easy to find examples of use. I have started to use them and show in this blog article a typical scenario for bitmasks.

How to define bitmasks

There are several ways to define bitmasks in Fortran. It is possible to simply use the corresponding integer value that are exponents of 2 or to use a "binary constant" such as b'01' (for one bit on and one off, starting from the right). Binary constants are parts of the so-called "BOZ" constant, for binary (b), octal (o), and hexadecimal (z) constants. Their use is limited by the standards to the initialization of data and as arguments to the real, dble, int, and cmplx intrinsics.

The solution I found the most satisfactory is to use the bit intrinsics. To define a bitmask whose first bit is set, use the function ibset(i1, i2) that returns an integer equal to i1 with the i2-th bit set to 1 (whether that bit was set to 0 or 1 for i1).


integer, parameter :: MASK_A = ibset(0, 0)
integer, parameter :: MASK_B = ibset(0, 1)

The masks can then be combined with logical "or" operations

mask = ior(MASK_A, MASK_B)

for the equivalent C code mask = MASK_A | MASK_B

and tested with logical "and" operations and equality testing

condition = (iand(mask, MASK_A) == MASK_A)

for the equivalent C code (mask & MASK_A) == MASK_A.

If you work with the position of the bits in the bitmasks, you can use the shorter "btest" logical function:

condition = btest(mask, 0)

Given the convenience of boolean operators (iand and ior) and of bit set/test functions (ibset and btest), I actually double defined the constants as bits and masks in the following way.

integer, parameter :: BIT_A = 0
integer, parameter :: MASK_A = ibset(0, BIT_A)
integer, parameter :: BIT_B = 1
integer, parameter :: MASK_B = ibset(0, BIT_B)

The intrinsics used to manipulate bitwise integer in Fortran appeared in Fortran 95.

Verifying the consistency of the boolean operations

An integer variable of storage size Nb bits can store Nb independent bitmasks. Fortran's type system leaves some freedom to compilers for the storage size of variables. The intrinsic selected_int_kind allows programmers to require a certain range defined in powers of 10, for instance from -10^r to 10^r. The issue here is to select the proper integer kind for a given number of bits.

Up to rounding errors, you can use log10(2^Nb) to evaluate the number of bits to use. Another Fortran-specific issue is the lack of unsigned integers and you might doubt whether the Nb-th bit is available for setting bitmasks. To make sure that the bitmasks work properly, I wrote a Python program that generates the definitions and tests that all bitmasks are pairwise independent.

The program performs the following checks:

  1. After setting an integer b to one of the mask, it verifies that b /= 0 and b == iand(b, MASK).
  2. By pair b = ior(FLAG_I, FLAG_J), the program verifies that FLAG_K == iand(b, FLAG_K) only for FLAG_K = FLAG_I or FLAG_K = FLAG_J.

In the case that the integer storage size is too short, some of the masks will be set to zero or some of the masks will be equal and the test will fail.

In my tests with gfortran 7.2, whether I use ibset or BOZ constants to define the masks, I cannot use the upper bit that is actually the sign bit. The compiler complains with the message

   integer(kind=ik), parameter :: FLAG_07 = ibset(0, 7)
Error: Arithmetic overflow converting INTEGER(4) to INTEGER(1) at (1). This check can be disabled with the option ‘-fno-range-check’

ifort 18.0.1 did not thow such an error. In practice, you can add the option -fno-range-check if you want to use the full storage space of a bitmask at the expense of the corresponding safety check by the compiler. When compiling the code with this flag, all the tests ran successfully but it remains a constraint when working in larger projects or for code reuse.

Using ik = selected_real_kind(3) provides with gfortran or ifort 2 bytes (16 bits) of storage. Excluding the sign bit, it means that one can already store 15 independent bitmaks.

Summary and example

Before using bitmasks on a platform, you can use the program generate_bitmask_tests.py to check the number of supported bits.

For the smallest supported size, here is an example of use:

$ ./generate_bitmask_tests.py 7 --kind-selector 2 > bitmask_test.f90
$ gfortran -o bitmask_test{,.f90}
$ ./bitmask_test
 selected_int_kind(2) =           1
 storage_size(b) =            8 bits
 FLAG_00 =     1
 FLAG_01 =     2
 FLAG_02 =     4
 FLAG_03 =     8
 FLAG_04 =    16
 FLAG_05 =    32
 FLAG_06 =    64
 T F F F F F F
 F T F F F F F
 F F T F F F F
 F F F T F F F
 F F F F T F F
 F F F F F T F
 F F F F F F T

The program is available in my "programming log" here. Below, I show a self-contained example of using bitmasks, available at the same location as example_bitmasks.f90.

program example_bitmasks
  implicit none

  integer, parameter :: ik = selected_int_kind(2)
  integer, parameter :: BIT_00 = 0
  integer(kind=ik), parameter :: MASK_00 = ibset(0, BIT_00)
  integer, parameter :: BIT_01 = 1
  integer(kind=ik), parameter :: MASK_01 = ibset(0, BIT_01)
  integer, parameter :: BIT_02 = 2
  integer(kind=ik), parameter :: MASK_02 = ibset(0, BIT_02)

  integer(kind=ik) :: mask

  mask = ior(MASK_00, MASK_02)

  write(*,*) 'Truth table of mask with the three bitmasks'
  write(*,*) 'iand(mask, MASK_00) == MASK_00', iand(mask, MASK_00) == MASK_00
  write(*,*) 'btest(mask, BIT_00)', btest(mask, BIT_00)
  write(*,*) 'iand(mask, MASK_00) == MASK_00', iand(mask, MASK_01) == MASK_01
  write(*,*) 'btest(mask, BIT_01)', btest(mask, BIT_01)
  write(*,*) 'iand(mask, MASK_00) == MASK_00', iand(mask, MASK_02) == MASK_02
  write(*,*) 'btest(mask, BIT_02)', btest(mask, BIT_02)

end program example_bitmasks

Comments !

Generated with Pelican. Theme based on MIT-licensed Skeleton.