To: J3 J3/24-129 From: Pierre Hugonnet Subject: allow complex pointer to reals and vice versa Date: 2024-June-10 #Reference: 1. Introduction The proposal is to allow a complex pointer to be associated to a real array, and similarly to allow a real pointer to be associated to a complex scalar or array. 2. Motivation In short, the motivation is to save memory and cpu-memory bandwidth in some HPC contexts, typically when dealing with very large or huge data volumes. When working in the Fourier domain, it is common to use in-place real to complex Fourier transforms. Before the forward transform one generally stores data in a real array, and the transform outputs complex numbers in the same real array, with interleaved real and imaginary parts. Once in the Fourier domain the complex type is more appropriate to perform computations, but this requires either: - duplicating the data into a complex array after the transform, thus cancelling the advantage of the in-place transform. Note that in HPC, and particularly when dealing with large data volumes, duplicating the data is really something that we want to avoid whenever possible. - or using various non-standard tricks that rely on the fact that virtually all compilers store a complex number by the sequence real part / imaginary part. For instance, consider a code that performs a FFT based convolution at some point: program foo real, allocatable :: r1(:), r2(:), r3(:) integer, parameter :: n = 100000 ! ... allocate( r1(n), r2(n), r3(n) ) ! ... some computations on r1(:) and r2(:) as reals call rfft(r1) ; call rfft(r2) call fftconvol(r1,r2,r3,n/2) ! called without any interface, ! hence type mismatchs call irfft(r3) ! ... some computations on r3(:) as real end program ! dangling routine (not contained and not in a module) subroutine fftconvol(c1,c2,c3,n) integer, intent(in) :: n complex, intent(in) :: c1(n), c2(n) complex, intent(out) :: c3(n) c3(:) = c1(:) * c2(:) end subroutine With modern Fortran another trick has got quite popular, by using the C interoperability features. Nonetheless the trick is non standard: program foo use iso_c_binding real, allocatable :: r1(:), r2(:), r3(:) integer, parameter :: n = 100000 complex, pointer :: c1(:), c2(:), c3(:) ! ... allocate( r1(n), r2(n), r3(n) ) ! ... some computations on r1(:) and r2(:) as reals call rfft(r1) ; call rfft(r2) ! non-standard trick call c_f_pointer( c_loc(r1), c1, [n/2] ) call c_f_pointer( c_loc(r2), c2, [n/2] ) call c_f_pointer( c_loc(r3), c3, [n/2] ) c3(:) = c1(:) * c2(:) call irfft(r3) ! ... some computations on r3(:) as real end program The `c_f_pointer()` trick is even mentioned in the official documentation of FFTW, which is one of the most popular FFT library [1]. More generally, this is a recurrent topic on forums or on StackOverflow [2] 3. Proposed solution The proposal essentially aims at standardizing the usual practice above, by allowing a real array to be viewed as a complex array and vice-versa. The above code would become: program foo real, allocatable, target :: r1(:), r2(:), r3(:) integer, parameter :: n = 100000 complex, pointer :: c1(:), c2(:), c3(:) ! ... allocate( r1(n), r2(n), r3(n) ) ! ... some computations on r1(:) and r2(:) as reals call rfft(r1) ; call rfft(r2) c1 => r1 ; c2 => r2 ; c3 => r3 c3(:) = c1(:) * c2(:) call irfft(r3) ! ... some computations on r3(:) as real end program 3.1. Syntax No new syntax need to be created, the usual pointer association can be used, with additional rules and restrictions: `c => r` - `r` shall be a *contiguous* real array, which has either the target or the pointer attribute - `c` shall be a complex array pointer of the same kind as `r`, and of the same rank by default (but pointer rank remapping can be used) - `c` could also be a complex scalar pointer, in the case r is a rank-1 array of size 2 - the size of the first dimension of `r` shall be even - `c%re` shall refer to the same storage as `r(1::2)` (rank-1), or `r(1::2,:)` (rank-2), etc... - `c%im` shall refer to the same storage as `r(2::2)` (rank-1), or `r(2::2,:)` (rank-2), etc... `r => c` - the exact opposite - `c` shall be a *contiguous* complex array or a complex scalar, which has either the target or the pointer attribute - `r` shall be a real array pointer of the same kind as `c`, and of the same rank by default (but pointer rank remapping can be used) - if `c` is a scalar, then `r` shall be a rank-1 pointer of size 2 - same other rules as above 3.2 Alternative syntaxes If one wants to make the association between two different types more explicit and more type-safe, a new syntax may be introduced: Alternative syntax 1 (kind of pointer casting): ``` c => complex :: r r => real :: c ``` Alternative syntax 2: ``` c => complex_pointer(r) r => real_pointer(c) ``` Alternative syntax 3 (similar to the c_f_pointer() subroutine): ``` call complex_pointer(r,c[,shape][,lower]) call real_pointer(c,r[,shape][,lower]) ``` Alternative syntax 4 (more generic, in case other inter-type associations would be allowed in future versions of the standard): ``` call storage_associate(r,c[,shape][,lower]) call storage_associate(r,c[,shape][,lower]) ``` 3.3 Intermediate representation During the discussions it has been proposed tu use an intermediate representation between the real and complex view, which would be a real view with an augmented rank and first dimension of size 2. Instead of going directly from `r(n)` to `c(n/2)` one would define by rank remapping: `r2(1:2,1:n/2) => r(1:n)`, then `c => r2` And the other way: `r2 => c`, then `r(1:n) => r2(1:2,1:n/2)` This could also be coupled with a new pseudo-component notation `c%reim` that would be equivalent the `r2` respresentation (i.e. `c%reim` would be `real`, with a shape [2,n/2]). I personally find that such an intermediate representation just makes the proposal more complex than it needs to be, and that nothing really useful can be performed with it. Moreover the `c%reim` notation would require another change in the standard to allow non zero ranks on both sides of `%`. 3.4 Prototyping, etc... I think that this proposal doesn't need to have a preliminary prototype implementation, as it essentially consists in standardizing an already existing and common practice. A prototype implementation would do nothing else than mimicing the `c_f_pointer()` trick. 4. Issues / Objections / Limitations 4.1. Memory layout This proposal would constraint the memory layout of the complex type beyond what the current standard does. However, the required layout seems to be the de-facto standard, used by virtually all existing compilers (i.e. storing a complex scalar by the sequence real part/ imaginary part, in that order). Also, the proposal would not prevent alternative memory layouts for the arrays in future versions of the standard, such as the row major storage. For instance, in the case of a row major 2D array, `c%re` would refer to the same storage as `r(:,1::2)` instead of `r(1::2,:)` for the column major case. More generally the obtained view would be layout dependent, and the standard would have to describe the view for each layout. Similarly, the proposal would be compatible with the so-called split-complex layout, if introduced later (without assessing here if such a layout is possible at all). In this layout, a complex array is stored with all the real parts first, then all the imaginary parts. In this case, `c%re` would refer to the same storage as `r(1:n/2)`. 4.2. Argument association Allowing a real actual argument to be associated to a complex dummy argument -and vice-versa- has also been considered, but it would raise backward compatibility issues. So this part has been dropped from the proposal. 4.3. Alignment Considering for instance a default real type stored on 4 bytes, the default complex type is stored on 8 bytes. Compilers may prefer/want to align the complex arrays on 8 bytes boundaries, which cannot be guaranteed if a complex pointer is associated to an arbitrary real array (e.g. `c => r(2:)`). If this is a problem, the pointer association may be allowed only the other way (real pointer associated to a complex array), where alignement should not be a problem. 5. References [1] https://www.fftw.org/fftw3_doc/Reversing-array-dimensions.html [2] https://fortran-lang.discourse.group/t/ implicit-real-complex-conversion-in-fortran/7381 https://stackoverflow.com/questions/31590004/ is-the-storage-of-complex-in-fortran-guaranteed-to-be-two-reals https://stackoverflow.com/questions/36977737/ pass-a-real-array-if-a-complex-array-is-expected Discussions related to this proposal: https://fortran-lang.discourse.group/t/complex-type-storage-again/7020 https://github.com/j3-fortran/fortran_proposals/issues/323 https://github.com/j3-fortran/fortran_proposals/pull/325