Complex Operations¶
Fortran includes support for complex number types and a set of operators and intrinsics that work on these types. Some of those operations are complicated and require runtime function calls to implement.
This document outlines a design for generating these operations using the MLIR complex dialect while avoiding cross-platform ABI issues.
FIR Representation¶
MLIR contains a complex dialect, similar to the Math dialect also used for lowering some integer and floating point operations in Flang. Conversion between fir.complex types and MLIR complex types is supported.
As a result at the FIR level, complex operations can be represented as conversions from the fir.complex type to the equivalent MLIR complex type, use of the MLIR operation and a conversion back.
This is similar to the way the math intrinsics are lowered, as proposed here
Fortran
function pow_self(c)
complex, intent(in) :: c
complex :: pow_self
pow_self = c ** c
end function pow_self
FIR
func.func @_QPpow_self(%arg0: !fir.ref<!fir.complex<4>>) -> !fir.complex<4> {
%0 = fir.alloca !fir.complex<4>
%1 = fir.load %arg0 : !fir.ref<!fir.complex<4>>
%2 = fir.load %arg0 : !fir.ref<!fir.complex<4>>
%3 = fir.convert %1 : (!fir.complex<4>) -> complex<f32>
%4 = fir.convert %2 : (!fir.complex<4>) -> complex<f32>
%5 = complex.pow %3, %4 : complex<f32>
%6 = fir.convert %5 : (complex<f32>) -> !fir.complex<4>
fir.store %6 to %0 : !fir.ref<!fir.complex<4>>
%7 = fir.load %0 : !fir.ref<!fir.complex<4>>
return %7 : !fir.complex<4>
}
Some operations are currently missing in the MLIR complex dialect that we would want to use here, such as powi and the hyperbolic trigonometry functions. For the missing operations we call directly to libm where possible, for powi we provide an implementation in the flang runtime.
Lowering¶
The MLIR complex dialect supports lowering either by emitting calls to the complex functions in libm (ComplexToLibm), or through lowering to the standard dialect (ComplexToStandard). However, as MLIR has no target awareness, the lowering to libm functions suffers from ABI incompatibilities on some platforms. As such the custom lowering to the standard dialect is used. This may be something to revisit in future if performance could be improved by using the libm functions.
Similarly to the numerical lowering through the math dialect, certain MLIR optimisations could violate the precise floating point model, so when that is requested lowering manually emits calls to libm, rather than going through the MLIR complex dialect.
The ComplexToStandard dialect does still call into libm for some floating point math operations, however these don’t have the same ABI issues as the complex libm functions.
The flang driver option -fcomplex-arithmetic=
allows you to select whether
complex number division is lowered to function calls or to the complex.div
operation in the MLIR complex dialect. To avoid the ABI issues mentioned above,
the choice of function calls or the complex.div
operation is made during the
lowering phase. The behavior of this option is as follows:
basic
: Lowers tocomplex.div
and is converted to algebraic formulas. No special handling to avoid overflow. NaN and infinite values are not handled.improved
: Lowers tocomplex.div
and is converted to Smith’s algorithm. See SMITH, R. L. Algorithm 116: Complex division. Commun. ACM 5, 8 (1962). This value offers improved handling for overflow in intermediate calculations, but overflow may occur. NaN and infinite values are handled.full
: Lowers to a call to runtime library functions. Overflow and non-finite values are handled by the library implementation. This is the default value.
While the same option in clang allows specifying promoted
, this is not
implemented in Flang. Also, in the case of improved
, clang does not handle NaN
and infinite values, but Flang does. These behavioral differences arise because
the transformation of complex division calculations depends on the implementation
of ComplexToStandard, which may change in the future.