1. Motivation
ISO/IEC 19570:2018 (1) introduced data-parallel types to the C++ Extensions
for Parallelism TS. [P1928R6] is proposing to make that a part of C++ IS.
Intel supports the concept of a standard interface to SIMD instruction
capabilities and we have made extra suggestions for other APIs and facilities for
in document [P2638R0]. One of the extra features we suggested was the
ability to work with interleaved complex values. It is now becoming common
for processor instruction sets to include native support for these operations
(e.g., Intel AVX512-FP16, ARM Neon) and we think that allowing
to
directly access these features is advantageous.
This document gives more details about the motivation for supporting complex-values, the different storage and implementation alternatives, and some suggestions for a suitable API.
2. Background
Complex-valued mathematics is widely used in scientific computing and many types
of engineering, with applications including physical simulations, wireless
signal processing, media processing, and much more besides. C++ already supports
complex-valued operations using the
template available in the
header file and C supports complex values by using the
keyword. C++ supports floating point elements, including
(C++23),
,
or
,
though in practice compilers permit integer complex values too. Such is the
importance of interleaved complex values that some mainstream processor vendors
now provide native hardware support for complex-value SIMD instructions (e.g.,
Intel AVX512-FP16, ARM Neon v8.3).
Any complex value is represented as a pair of values, one for the real component and one for the imaginary component. In C and C++ a complex value is a pair of two values of the appropriate data type, allocated to a single unit containing 2 elements which are stored contiguously. This is illustrated in the figure below. This storage layout is used in other languages too, allowing for easy data interchange between software written in different languages, or with comparable user-defined types. This format may also be used for hardware accelerators or interfaces too:
When many complex values are stored together, such as in a C-style array, a C++
or a C++
, then the real and imaginary elements are
interleaved in memory:
Many applications, libraries, programming languages and interfaces between diverse software and hardware components will store data in this interleaved format, and it is treated as the default layout for blocks of complex-valued data. To improve the compute efficiency of this data format both Intel and ARM have introduced native hardware support to allow efficient SIMD operations to be performed in this data format without having to resort to reformatting the data into a more SIMD-amenable form. Where the underlying target hardware does not have native support for interleaved complex values it is straight-forward to synthesize a sequence of operations which give the same effect.
Given the popularity of interleaved complex-values as a data exchange and
compute format we propose that
should be able to directly represent
this simd format in a form exemplified as:
std :: simd < std :: complex < float > , 5 > std :: simd < std :: complex < double > , 9 >
In all these cases the fundamental element type will be a suitable complex value, and the individual components of each complex element will be worked on as a single unit, according to the rules of complex arithmetic where appropriate. This includes:
-
All numeric operators (e.g.,
,+
,/
,-
) and their derivatives (e.g., assignment operators) will operate as per their standard complex type definition. For example, the multiply operator will invoke a complex multiply for each complex element.* -
Any numeric operation will apply the operation on a per-element basis, generating a simd object of the same size as the input, but modifying the element type to be the appropriate return type. For example, when applied to a
:simd < complex < T >> -
,exp
,sin
,log
,tan
and so on will generate acos simd < complex < T >> -
orabs
will generate anorm
(i.e., real-valued SIMD equivalent).simd < T >
Note that the mathematical behavior will also be applied appropriately (e.g., if a real or imaginary component is a NaN, then the complete operation for a complex operation will also be NaN).
-
-
The
associated with asimd_mask
will have each individual mask bit refer to a complete complex element, not to sub-components of the complex elements. When thesimd < complex < T >>
is backed by a compact representation (e.g., AVX-512), then each individual bit will refer to a complete complex element. When thesimd_mask
is backed by an element mask (i.e., multiple bits filling a container of the same size as the simd element) then the size of each bit element will be twice the size of the underlying complex value type (e.g., asimd_mask
would be equivalent to something likesimd_mask < complex < _Float16 >>
, sincesimd < uint32_t >
.sizeof ( uint32_t ) == 2 * sizeof ( _Float16 ) -
Operations or functions which are invalid for complex values will be removed. This includes relational operators (e.g.,
,<
,<=
,>
) and those derived from them such, as>=
ormin
.max -
Any operation which moves or reorders elements will move entire complex values. For example, any sort of permute, resize, split, concat, insert, extract or indexing operation works on complete complex elements as though they are atomic units.
-
Reduction operations apply at the level of complex values (this follows from the previous bullet), provided the mathematical operation is valid (e.g., reduce-max would not be available).
One of the aims of
is to make it possible to write a generic
code, which can use either a scalar type or a data-parallel type
interchangeably. To this end,
provides overloads of many of the
standard functions which operate in data-parallel mode. The same should apply to
a simd of complex values; it should be possible to write an algorithm which uses
either
or
with only a type replacement,
not with major API changes. To this end
should include some member
functions, such as real or imag, to mirror those found in std::complex.
Note that an alternative way to create parallel complex values is to allow
to use simd elements, such as
.
This storage layout is called separated storage and discussion of this format is
outside the scope of this paper.
3. Overview of updates required to support std :: complex
simd elements
There are three ways in which
needs to change to accommodate
elements:
-
Modify the definition of vectorizable types and the operations on them to allow for complex elements. These modifications are small in nature and don’t change the fundamental
API.std :: simd -
Add a small number of methods to
to mimic those found instd :: simd
. These modifications allow astd :: complex
value to be easily inserted into source code by text or template replacement in place of astd ::: simd < complex < T >>
value without requiring a rewrite.std :: complex -
Add a set of overloads and free functions to allow
to reflect the different behaviors of complex (e.g.,simd < complex < T >>
,sin
,cos
,exp
).log
Each of these points will now be discussed in more detail.
3.1. std :: simd
base modifications
In its current definition,
allows the element type to be any
cv-unqualified arithmetic type other than bool. The first modification is to
extend the set of allowable element types to include any type which is valid for
.
Any simd operation or function which relies only on an element being an atomic
container which can be arbitrarily moved or duplicated as a single unit will
continue to operate as expected. For example, all the following will work on
without modification:
-
Constructors or
/copy_to
functions for loading and storing values to and from contiguous iteratorscopy_from -
Broadcast constructor
-
Index operations
-
Reordering operations, such as
,simd_split
,simd_cat
,permute
,resize
andinsert
.extract -
Masking selection operations (using a mask to include or exclude values from an operation or copy).
’s of complex values do not need to have any special behaviour since the mask
is simply a collection of boolean values, and the underlying element type makes no difference.
Constructors, including generator functions, which work on atomic units (e.g., broadcasting constructor) will work without modification. Note that the existing broadcast constructor will already permit a real-valued object to be used in a broadcast, since an individual value may be converted to a complex value with a zero imaginary:
auto x = simd < std :: complex < float >> ( 3.14f ); // [3.14 + 0i, 3.14 + 0i, ...]
Numeric operations (binary, unary, and compound-assignment) are mostly covered by [P1928R6] remark:
A simd object initialized with the results of applying the indicated operator to
and
as a binary element-wise operation
In the case of complex values this has the practical effect of ensuring that
operators will perform their complex operations without changing the definition
of
(e.g.,
will perform complex-valued
multiplication). Furthermore, [P1928R6] notes that constraints will be applied
as necessary on operators and functions:
Constraints: Application of the indicated operator to objects of type
is well-formed.
This will ensure that operators which are not permitted for complex numbers
(e.g.,
,
,
) will be removed from
the overload set, as will functions which rely on these operators (e.g.,
,
,
).
3.2. Additional complex methods for std :: simd
It is desirable for
to be source- or template-replaceable with
respect to its underlying element type. For example, given a simple code
fragment which works with
:
auto my_fn ( auto cmplx_value ) { std :: complex < float > rotator ( 0 , 1 ); return ( cmplx_value * rotator ). real (); }
In this example the function will work with a
input. Ideally
it should also work with an input of type
. For this to happen,
firstly the
must work with a scalar value, and secondly it should be
possible for the
method to be invoked on a
value.
provides a number of member functions:
-
Constructors
-
(assignment)operator = -
,operator +=
,operator -=
,operator *= operator /= -
real -
imag
The assignment operator and the compound-assignment operators are already
provided by
so no further action is required. The remaining member
functions that are required are considered in the following sub-sections.
3.2.1. Constructors
Conversion and copy methods already exist in
and can be used for
complex SIMD values. An additional constructor is needed to match the
constructor which can build a complex value from separate
real and imaginary components: constructed from real and imaginary simd values.
requires is_complex_number < T > constexpr basic_simd ( const basic_simd < T :: value_type , UAbi >& r = {}, const basic_simd < T :: value_type , UAbi >& i = {})
This constructor is constrained so that it can only be used for SIMD values with
complex elements (some suitable concept will be added to enforce this). Like its
counterpart it can be used to build a complex value from real
components only, with insertion of zero imaginaries. This constructor can be used
as in the following example:
simd < float , 8 > reals = ...; simd < float , 8 > imaginaries = ...; // Create a real-valued complex number (i.e., zero imaginaries) simd < std :: complex < float > , 8 > realAsComplex ( reals ); // Create a complex value from real and imaginary simd inputs simd < std :: complex < float > , 8 > cmplx ( reals , imaginaries );
3.2.2. Real/imag accessors
It should be possible to separately read or write the real and imaginary
components. For example, given a simd of
values, the real
components of the simd will be extracted as a value of type
instead.
Similarly, given a
value those values can be inserted into the simd
as the real components of each respective value.
The read accessors for
come in two variants: a free function,
and a member function. These would be defined in
as follows:
// Free functions for accessing real/imaginary components template < typename T , typename ABI > constexpr auto real ( const basic_simd < complex < T > , ABI >& ); template < typename T , typename ABI > constexpr auto imag ( const basic_simd < complex < T > , ABI >& ); // Member functions of std::simd for accessing real/imaginary components. constexpr simd < T , size () > std :: simd < std :: complex < T >>:: real () const ; constexpr simd < T , size () > std :: simd < std :: complex < T >>:: imag () const ;
The write accessors for
are provided as member functions
which allow the real and imaginary components of an existing complex value to be
overwritten.
template < typename T , typename ABI > constexpr void real ( const basic_simd < T , ABI >& reals ); template < typename T , typename ABI > constexpr void imag ( const basic_simd < T , ABI >& reals );
The advantage of making
and
members of
is an API consistency
between
and
for both getters and setters
A disadvantage though is the
and
seem too specific to be provided for
.
Please see § 3.3.1 Alternative design for real and imag for more design considerations.
3.3. Additional free functions
The free functions for
include numeric operators, such as
,
,
and
. These are already proposed in [P1928R6] and need not be considered
further.
The remaining free functions which should be specialized for
include:
| returns the real part |
---|---|
| returns the imaginary part |
| returns the magnitude of a complex number |
| returns the phase angle |
| returns the squared magnitude |
| returns the complex conjugate |
| returns the projection onto the Riemann sphere |
| constructs a complex number from magnitude and phase angle |
| complex base e exponential |
| complex natural logarithm with the branch cuts along the negative real axis |
| complex common logarithm with the branch cuts along the negative real axis |
| complex power, one or both arguments may be a complex number |
| complex square root in the range of the right half-plane |
| computes sine of a complex number |
| computes cosine of a complex number |
| computes tangent of a complex numbe |
| computes arc sine of a complex number |
| computes arc cosine of a complex number |
| computes arc tangent of a complex number |
| computes hyperbolic sine of a complex number |
| computes hyperbolic cosine of a complex number |
| computes hyperbolic tangent of a complex number |
| computes area hyperbolic sine of a complex number |
| computes area hyperbolic cosine of a complex number |
| computes area hyperbolic tangent of a complex number |
All of these functions should operate element-wise in the same way as their
scalar counterparts. Note that although most of these functions take complex
inputs and generate complex outputs, some of these functions generate
real-valued outputs (e.g.,
returns the magnitude, which is real-valued), or
accept an input which is real-valued (e.g.,
can accept a real-valued value
and return a complex value).
3.3.1. Alternative design for real
and imag
Instead of making
and
member functions we could provide them
only as free functions. Free function getters are already included in the
proposal, but the setters would be removed as member functions and replaced by
the following:
// Setters template < typename T , typename AbiC , typename AbiR > void real ( basic_simd < complex < T > , AbiC >& , basic_simd < T , AbiC > ); template < typename T , typename AbiC , typename AbiR > void imag ( basic_simd < complex < T > , AbiC >& , basic_simd < T , AbiR > );
The member function getters would also be removed.
The advantage of that approach is it keeps
specific functions outside of
class. The disadvantage though is inconsistency with existing
and
free functions for
, which allow reading only.
Please see § 3.2.2 Real/imag accessors for initial design.
We would like to hear the feedback of C++ standard committee what design looks more appropriate.
4. Implementation experience
Intel have written an example implementation of
which includes
the extensions to support interleaved complex values. We have been able to
generate efficient code both on targets with native support (e.g., AVX512_FP16),
and also on targets without native support which require synthesized code
sequences.
5. Wording
The wording relies on [P1928R6] being landed to the Working Draft. Below, substitute the � character with a number the editor finds appropriate for the table, paragraph, section or sub-section.
5.1. Modify [simd.general]
�: The set of vectorizable types comprises all cv-unqualified arithmetic types other than
std::complex<>
specializations.
.
5.2. Modify [simd.summary]
�: Default intialization performs no initialization of the elements
,
except when the value type is a std::complex<>
specialization in which case all
elements will be value-initialized
; value-initialization initializes each
element with
.
5.3. Modify [simd.ctor]
�constructors [simd.ctor]
simd
[...]
template < typename CAbi > constexpr explicit basic_simd ( const basic_simd < complex - element - type , CAbi > reals = {}, const basic_simd < complex - element - type , CAbi > imags = {}); Constraints:
The
is a complex value (i.e., some specialization of
value_type ).
std :: complex <> The
is the same as
complex - element - type (where T is the simd’s own
T :: value_type ).
value_type
is equal to
basic_simd < complex - element - type , CAbi >:: size .
basic_simd :: size Effects:
Construct a simd of elements of type
(where
value_type is a specialization of
value_type ), such that the ith element is equal to the object constructed as
std :: complex <> for all i in the range
value_type ( reals [ i ], imags [ i ]) .
[ 0. . size ) Remarks:
Where no parameter values are provided this function acts as default initializer which value-initializes all real and imaginary elements.
5.4. Add new section [simd.complex_accessors]
�complex accessors [simd.complex_accessors]
simd constexpr simd < complex - element - type , size > real () const ; constexpr simd < complex - element - type , size > imag () const ; Constraints:
is a complex value (i.e., any valid instantiation of
T template).
std :: complex <> Returns:
A simd with values of complex-element-type that is a value_type of
of the same width. The ith element will be equal to
std :: complex or
( * this )[ i ]. real () respectively for all i in the range
( * this )[ i ]. imag () .
[ 0. . size ) template < typename CAbi > constexpr void real ( const basic_simd < complex - element - type , CAbi >& v ) noexcept ; template < typename CAbi > constexpr void imag ( const basic_simd < complex - element - type , CAbi >& v ) noexcept ; Constraints:
is a complex value (i.e., any valid instantiation of
T template).
std :: complex <>
is the same as the simd
complex - element - type .
value - type
is equal to
basic_simd < complex - element - type , CAbi >:: size .
basic_simd :: size Effect:
Modify each element of the simd such that the real or imaginary component of the ith element is replaced by the value of
or
v [ i ]. real respectively.
v [ i ]. imag () template < typename T , typename Abi > constexpr simd < complex - element - type , size > real ( const basic_simd < typename T , typename Abi >& v ) noexcept ; template < typename T , typename Abi > constexpr simd < complex - element - type , size > imag ( const basic_simd < typename T , typename Abi >& v ) noexcept ; Constraints:
is a complex value (i.e., any valid instantiation of
T template).
std :: complex <> Returns:
A simd with values of complex-element-type that is a value_type of
of the same width, where the ith element is equal to
std :: complex or
v [ i ]. real () respectively for all i in the range
v [ i ]. imag () .
[ 0. . v . size )
5.5. Add new section [simd.complex_math]
�complex maths library [simd.complex_library]
simd For each set of overloaded functions within
there shall be additional overloads sufficient to ensure that:
< complex >
Any
type can be used where
basic_simd < std :: complex < T > , Abi > would be used (parameter or return value).
std :: complex <> Any real-valued
type can be used where T would be used (parameter or return value).
simd < T , Abi > It is unspecified whether a call to these overloads with arguments that are all convertible to
but are not of type
basic_simd < T , Abi > is well-formed.
basic_simd < T , Abi > Each function overload produced by the above rules applies the indicated
function element-wise. For the mathematical functions, the results per element only need to be approximately equal to the application of the function which is overloaded for the element type.
< complex > The result is unspecified if a domain, pole, or range error occurs when the input argument(s) are applied to the indicated
function. [ Note: Implementations are encouraged to follow the C specification (especially Annex F). — end note ]
< complex >
6. Polls
6.1. Kona 2022 SG1 polls
Poll: After significant experience with the TS, we recommend that the next version (the TS version with
improvements) of
target the IS (C++26)
SF | F | N | A | SA |
---|---|---|---|---|
10 | 8 | 0 | 0 | 0 |
Poll: Future papers and future revisions of existing papers that target
should go directly to LEWG. (We do not believe
there are SG1 issues with
today.)
SF | F | N | A | SA |
---|---|---|---|---|
9 | 8 | 0 | 0 | 0 |
7. Revision History
R3 => R4
-
Updated wording to reflect changes made to
in [P1928R6] (e.g., removal ofstd :: simd
, addition offixed_size_simd
).basic_simd
R2 => R3
-
Added wording
R1 => R2
-
Add more content for proposal overview
-
Rewrite to bikeshed
R0 => R1
-
Add polls from Kona SG1 2022