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).
Example:
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:
- After setting an integer
b
to one of the mask, it verifies thatb /= 0
andb == iand(b, MASK)
. - By pair
b = ior(FLAG_I, FLAG_J)
, the program verifies thatFLAG_K == iand(b, FLAG_K)
only forFLAG_K = FLAG_I
orFLAG_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)
1
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 !
Comments are temporarily disabled.