OpenMP Defaults Are Not Portable: A Note on OMP_DYNAMIC

3 minute read

Published:

For reproducible scaling experiments, it is good practice to explicitly set OMP_DYNAMIC=false, especially when you want to guarantee the number of threads spawned for each parallel region. In this short article, I explain why this is important and how one can sift through the GCC codebase (140k+ files and more than 5 million LOC) plus the OpenMP standard to find this information.

Controlling the exact thread count is important for reproducibility. If the runtime is allowed to dynamically adjust the number of threads, the same program may exhibit different performance characteristics across compilers, systems, or execution environments. Explicitly disabling dynamic thread adjustment helps ensure that scaling measurements and performance comparisons are consistent and interpretable.

Interestingly, this is not strictly necessary when using GCC, because GCC’s OpenMP implementation (gcc/libgomp) already defaults to OMP_DYNAMIC=false (well, sort of… more on that later). However, relying on the default behavior is not portable. Per section 6.3: OMP_DYNAMIC of the OpenMP standard:

The behavior of the program is implementation defined if the value of OMP_DYNAMIC is neither true nor false.

Moreover, per section 2.5: Internal Control Variables and section 2.5.2: Internal Control Variable Initialization, the variable dyn_var determines whether dynamic adjustment of the number of threads is enabled, and its value is implementation defined. This means compiler developers are free to choose the default behavior of their OpenMP implementation. In principle, this means that a compiler implementer may choose to enable dynamic adjustment of the number of threads by default.

Note that the internal control variable dyn_var simply reads from OMP_DYNAMIC if its defined. However, explicitly disabling it ensures more predictable and reproducible behavior across systems and compilers.

How do we know, though, that GCC defaults to OMP_DYNAMIC=false? Well, GCC does not actually “set” OMP_DYNAMIC, so that’s why I wrote “sort of” earlier. Really, GCC defines dyn_var in a struct of configuration variables that can be updated by environment variables. If the environment variable doesn’t exist, then the default value of the configuration variable is used. If you want to verify GCC’s behavior yourself, you can trace it directly through the gcc/libgomp source code:

1
2
3
4
5
6
7
8
9
$ git clone --depth 1 https://github.com/gcc-mirror/gcc.git
$ cd gcc
$ cd libgomp 
$ rg dyn_var
parallel.c 
73: if (icv->dyn_var)

env.c
78: .dyn_var = false

From ripgrep, we can see where dyn_var gets used in parallel.c#L73. We can also see from env.c#L78, the default value of dyn_var, and thus know its value for certain without having to guess or treat GCC as a black box.

1
2
3
4
5
6
7
8
9
10
11
12
13
// @file env.c
const struct gomp_default_icv gomp_default_icv_values = {
  .nthreads_var = 1,
  .thread_limit_var = UINT_MAX,
  .run_sched_var = GFS_DYNAMIC,
  .run_sched_chunk_size = 1,
  .default_device_var = INT_MIN,
  .max_active_levels_var = 1,
  .bind_var = omp_proc_bind_false,
  .nteams_var = 0,
  .teams_thread_limit_var = 0,
  .dyn_var = false            // <----------------- DEFAULT VALUE!!!! 
};

Overall, OMP_DYNAMIC is a small but important example of a broader issue in HPC: default behavior is not guaranteed to be portable, even when it is consistent within a specific compiler like GCC. For reproducible performance experiments, especially scaling studies, explicitly setting OpenMP configuration variables removes ambiguity and ensures that results are comparable across compilers, systems, and environments. As shown in GCC’s libgomp implementation, even seemingly “fixed” defaults are ultimately implementation details and relying on them implicitly can introduce hidden variability into performance measurements. When in doubt: always set OpenMP environment variables explicitly for reproducible performance work.