Improving Vectorization of OpenMP* Collapse Loop Nests
The collapse clause on an OpenMP* worksharing loop is handled by the compiler for CPU by merging a loop nest into a single iteration space so that more iterations are available to distribute across threads. This usually increases the amount of parallelism and improves load balancing, but it can work against vectorization in some cases: after the loops are merged, the compiler code generation may introduce divide and modulo operations inside the loop that uses the collapsed loop index to recover the loop-indices of the original loops. This may result in less efficient vectorization if the collapsed loop becomes the innermost vectorized loop.
The -fopenmp-vector-aware-collapse (Linux) or /Qopenmp-vector-aware-collapse (Windows) option lets the compiler collapse one loop level less than the requested collapse level, leaving the innermost loop untouched by collapsing so that it can be efficiently vectorized. Threading is still applied to the remaining collapsed loops and may reduce the total amount of parallelism. Program results are unchanged; only the distribution of iterations across threads and the shape of the loop nest in the generated code change.
The option is off by default because its profitability depends on whether the loop nest with reduced collapse level can still be effectively parallelized across the available threads on the CPU. The recommended approach is to evaluate the option and confirm that it leads to a performance gain.
A Simple Example: 3-Level Collapsed Loop Nest
The testcase below evaluates a degree-4 polynomial over a three-dimensional array, with collapse(3) on a three-deep nest. The kernel is repeated so the timing is stable, and the result array is summed at the end so that no part of the computation can be optimized away.
Fortran source, collapse_poly.f90:
program collapse_poly
use omp_lib
implicit none
integer, parameter :: ni = 1024, nj = 64, nk = 16
integer, parameter :: nrep = 9600
real, allocatable :: a(:,:,:), b(:,:,:)
real, parameter :: c0 = 1.0, c1 = 0.5, c2 = 0.25, c3 = 0.125, c4 = 0.0625
real :: x
real(kind=8) :: t0, t1, s
integer :: i, j, k, r
allocate(a(ni,nj,nk), b(ni,nj,nk))
do k = 1, nk
do j = 1, nj
do i = 1, ni
b(i,j,k) = real(mod(i + j + k, 17)) * 0.125
end do
end do
end do
! Warm up so the timed loop is not measuring first-touch page faults.
do r = 1, 5
!$omp parallel do collapse(3) private(x)
do k = 1, nk
do j = 1, nj
do i = 1, ni
x = b(i,j,k)
a(i,j,k) = (((c4 * x + c3) * x + c2) * x + c1) * x + c0
end do
end do
end do
!$omp end parallel do
end do
t0 = omp_get_wtime()
do r = 1, nrep
!$omp parallel do collapse(3) private(x)
do k = 1, nk
do j = 1, nj
do i = 1, ni
x = b(i,j,k)
a(i,j,k) = (((c4 * x + c3) * x + c2) * x + c1) * x + c0
end do
end do
end do
!$omp end parallel do
end do
t1 = omp_get_wtime()
s = sum(real(a, kind=8))
print '(a,i0)', 'threads = ', omp_get_max_threads()
print '(a,f9.2,a)', 'time = ', 1.0d3 * (t1 - t0), ' ms'
print '(a,es14.6)', 'checksum= ', s
deallocate(a, b)
end program collapse_poly
In this nest, when the option is enabled, the compiler will leave the innermost loop out of the collapsed iteration space. The outer two loops (k, j) still supply 16 x 64 = 1024 iterations to distribute across threads while the 1024-iteration innermost i-loop is vectorized.
Compiling and Running the Example
The testcase is built twice, once with the option disabled and once with the option enabled.
Linux:
ifx -O2 -qopenmp -xCORE-AVX512 -fno-openmp-vector-aware-collapse collapse_poly.f90 -o f90_off
ifx -O2 -qopenmp -xCORE-AVX512 -fopenmp-vector-aware-collapse collapse_poly.f90 -o f90_on
Windows:
ifx /O2 /Qopenmp /QxCORE-AVX512 /Qopenmp-vector-aware-collapse- collapse_poly.f90 -o f90_off
ifx /O2 /Qopenmp /QxCORE-AVX512 /Qopenmp-vector-aware-collapse collapse_poly.f90 -o f90_on
Each binary is run with the same thread count and thread placement, so that the performance difference is mainly due to the difference in the compiler code generation:
export OMP_NUM_THREADS=16
export KMP_AFFINITY=granularity=fine,compact,1,0
numactl --cpunodebind=0 --membind=0 ./f90_off
numactl --cpunodebind=0 --membind=0 ./f90_on
The reported checksum values can be compared to confirm that the results are unchanged.
Measured Results
Total time for 9600 iterations of the kernel was measured. The time reported for each binary is the fastest of seven alternating runs:
| Language | With option disabled | With option enabled | Speedup |
|---|---|---|---|
| Fortran (ifx) | 758.18 ms | 63.96 ms | 11.9x |
Hardware and Software Configuration
| Processor | 2 x Intel® Xeon® Platinum 8480L |
| Microarchitecture | Sapphire Rapids |
| Cores and threads | 56 cores per socket, 2 threads per core, 224 logical processors |
| Max turbo | 3.80 GHz |
| Caches | 48 KB L1d and 2 MB L2 per core; 105 MB L3 per socket |
| Vector ISA | Intel® AVX-512 (F, DQ, IFMA, CD, BW, VL, BF16, VBMI, VBMI2, VNNI, BITALG, VPOPCNTDQ, FP16) |
| Memory | 503 GiB, 4 NUMA nodes |
| Operating system | Ubuntu 24.04 LTS, kernel 6.8.0-59-generic |
| Compiler | Intel® Fortran Compiler 2026.2.0 |
| Compiler options | -O2 -qopenmp -xCORE-AVX512 |
| Threads | OMP_NUM_THREADS=16 KMP_AFFINITY=granularity=fine,compact,1,0 |
| Working set | two float arrays of 16 x 64 x 1024 elements, 4 MB each |
The 16 threads and the data were bound to a single socket, so the measurement is not affected by cross-socket memory traffic. Performance varies by use, configuration, and other factors.
Confirming That the Optimization Was Applied
Add the optimization report at level 2 or higher to the same compilation:
ifx -O2 -qopenmp -xCORE-AVX512 -fopenmp-vector-aware-collapse \
-qopt-report=2 -qopt-report-file=stdout -c collapse_poly.f90
When the option is enabled, the report shows the collapse count specified by the user, the reason it was adjusted down and the properties of the vectorized innermost loop that is now not included as part of the collapsing. For the timed loop of the Fortran example, at line 39:
OMP PARALLEL DO BEGIN at collapse_poly.f90 (39, 11)
remark #30014: OpenMP: 2 loops collapsed
LOOP BEGIN at collapse_poly.f90 (41, 7)
remark #30042: OpenMP: Collapse clause argument reduced from 3 to 2 to preserve innermost loop for efficient vectorization
remark #30000: OpenMP: Outlined parallel loop
remark #15541: loop was not vectorized: outer loop is not an auto-vectorization candidate.
LOOP BEGIN at collapse_poly.f90 (42, 9)
remark #15300: LOOP WAS VECTORIZED
remark #15305: vectorization support: vector length 8
remark #25438: Loop unrolled without remainder by 2 by LLVM community unroller
LOOP END
LOOP END
OMP PARALLEL DO END
The default Fortran compilation is the clearest illustration of why LOOP WAS VECTORIZED alone is not enough to judge a collapsed nest. It reports the loop as vectorized at vector length 16, twice the vector length of the version with the option enabled, and it is the slower of the two binaries measured above:
OMP PARALLEL DO BEGIN at collapse_poly.f90 (39, 11)
remark #30014: OpenMP: 3 loops collapsed
LOOP BEGIN at collapse_poly.f90 (41, 7)
remark #30000: OpenMP: Outlined parallel loop
remark #15300: LOOP WAS VECTORIZED
remark #15305: vectorization support: vector length 16
LOOP END
...
OMP PARALLEL DO END
Guidance on When to Use It
The best way to apply OpenMP* collapse clause is to apply it only on outer loops so that the innermost loops can be efficiently vectorized. If this is not possible then the user may want to evaluate the use of this option to improve application performance.
Leaving the innermost loop out of the collapsed iteration space reduces the number of iterations available for threading. If the remaining loops do not supply enough iterations to keep all threads busy, enabling this option can hurt performance rather than help it. Nests whose outer loops have small trip counts and whose innermost loop is long are the cases to check most carefully.
Notes
Applies to host code generation only; device code for OpenMP* offload is unaffected.
Requires OpenMP* to be enabled, with -qopenmp or /Qopenmp.
Does not apply to every collapse nest. Remark #30042 in the optimization report tells you which nests it affected.
See Also
fopenmp-vector-aware-collapse, Qopenmp-vector-aware-collapse compiler option
fopenmp-targets, Qopenmp-targets compiler option
fiopenmp, Qiopenmp compiler option
fopenmp compiler option