P2664R3
Proposal to extend std::simd with permutation API

Published Proposal,

This version:
http://wg21.link/P2664R3
Authors:
(Intel)
(Intel)
Audience:
LEWG
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++

Abstract

Proposal to extend std::simd with features enabling permutation of data within and across SIMD values.

1. Motivation

ISO/IEC 19570:2018 introduced data-parallel types to the C++ Extensions for Parallelism TS. [P1915R0] asked for feedback on std::simd in Parallelism TS 2. [P1928R3] aims to make that a part of C++ IS. Intel supports the concept of a standard interface to SIMD instruction capabilities and has made further suggestions for other APIs and facilities for std::simd in document [P2638R0].

One of the extra features we suggested was the ability to be able to permute elements across or within SIMD values. These operations are critical for supporting formatting of data in preparation for other operations (e.g., transpose, strided operations, interleaving, and many more), but are relatively poorly supported in the current std::simd proposal. Many modern SIMD processors include sophisticated support for permutation instructions so in this document we shall propose a set of API functions which make those underlying instructions accessible in a generic way.

In this document we shall start with some background into the current std::simd proposal and extensions, describe the state of the art on other simd libraries, and then describe a proposal for some new API functions to add to std::simd.

2. Revision History

R2 => R3

R1 => R2

R0 => R1

3. Background

In ISO/IEC 19570:2018 there were only a few functions which could be used to achieve any sort of element permutation across or within simd<> values. Those functions were relatively modest in scope and they built upon the ability to split and concatenate simd values at compile-time. For example, a simd value containing 5 elements could be split into two smaller pieces of sizes 2 and 3 respectively:

fixed_size_simd<float, 5> x;const auto [piece0, piece1] = split<2, 3>(x);

Those smaller pieces could be acted on using any of the std::simd features, and then later recombined using concat, possibly in a different order or with duplication:

const auto output = concat(piece1, piece1, piece0);

The split and concat functions can be used to achieve several permutation effects, but they are unwieldy for anything complicated, and since they are also compile-time they preclude any dynamic runtime permutation.

In [P2638R0] Intel suggested extra operations to insert and extract SIMD values to and from other SIMD containers:

const auto t = extract<2, 5>(v); // Read out 3 SIMD values, starting from position 2
insert<5>(v, t); // Put the 3 previously read values back into the container at position 5 

These are convenient, but they still don’t allow arbitrary or expressive permute operations.

Two suggestions were made in [P0918R1]: add an arbitrary compile-time shuffle function, and add a specific type of permutation called an interleave. The compile-time shuffle can be used as follows:

const auto p = shuffle<3, 4, 1, 1, 2, 3>(x);
// p::value_type will be the same as x::value_type
// p::size will be 6

The indexes can be arbitrary, and therefore allow any duplication and reordering of elements. The output std::simd object will have the same number of elements as there are supplied indexes, and the type of the SIMD output will be that of the input. It is noted also in the shuffle API that this function can be applied to simd_mask values too, and that shuffling across multiple SIMD values can be achieved by first concatenating the values into a single larger SIMD:

const auto p = shuffle<10, 9, 0, 1>(concat(x, y));

In addition to the shuffle function, [P0918R1] also proposes a function called interleave which accepts two SIMD values and interleaves respective elements into a new SIMD value which is twice the size. For example, given inputs [a0 a1 a2] and [b0 b1 b2], the output would be [a0 b0 a1 b1 a2 b2]. Interleaving is a common operation and is often directly supported by processors with explicit instructions (e.g., Intel’s punpcklwd), so as in [P0918R1] and also in other simd libraries there will be named functions which expose that instruction. There will be other common permutation operations which also have specialist hardware support and it is common for them to be exposed in named functions also. For example, [Highway] provides DupOdd, DupEven, ReverseBlocks, Shuffle0231, and so on, which map efficiently to underlying instructions. While this can ensure that good code is generated for specific function, it does mean that:

Neither of these is desirable, and in our suggestions below we provide an alternative.

4. Extending std::simd to support permutations

There are four classes of permutation which could be supported by std::simd;

Modern processor instruction sets typically support all 4 of these classes of instruction. In the following sections we shall examine in more detail each of these classes and the potential APIs needed to expose those features to the user. We shall also examine what it means to permute memory, and whether named functions should be provided for common permutation patterns.

std::simd could be modified to allow permutations either by adding to the base definition of std::simd itself (specifically, to include a new simd::operator[] or simd_mask::operator[]), or by introducing overloaded free functions which extend std::simd. We shall consider both options below.

Note that a simd_mask is a special case of a type of SIMD and can therefore be permuted in the same way as a conventional SIMD. In our discussions below we assume that the permutations would apply equally to a simd_mask unless we note otherwise.

4.1. Using compile-time computed indexes

The first proposal is to provide a permute function which accepts a compile-time function which is used to generate the indexes to use in the permutation. This is a very powerful concept: firstly, it allows the task of computing the indexes to be offloaded to the compiler using an index-generating function, and secondly it works on simd<> values of arbitrary size. It’s definition would be something like:

template<std::size_t SizeSelector = 0, typename T, typename Abi, typename PermuteGenerator>
constexpr resize_simd<output-size, simd>
permute(const simd<T, Abi>& v, PermuteGenerator fn)

It can be used as in the following example:

const auto dupEvenIndex = [](size_t idx) -> size_t { return (idx / 2) * 2; };
const auto de = permute(values, dupEvenIndex);

Note that the permutation function maps from the index of each output element to the index which should be used as the source for that position. So, for example, the dupEvenIndex function would map the output indexes [0, 1, 2...) to the source indexes [0 0 2 2 4 4). This example permutation is common enough that it has hardware support in Intel processors, and the compiler can map the above function directly to the corresponding instruction:

vmovsldup ymm0, ymm0

By default, the permute function will generate as many elements in the output simd as there were input values, but the output size can be explicitly specified if it cannot be inferred or it needs to be modified. For example, the following permute generates 4 values with a stride of 3 (i.e., indexes [0, 3, 6, 9]).

const auto stride3 = permute<4>(values, [](size_t idx) -> size_t { return idx * 3; });

4.1.1. Special generator indexes

The obvious index representation to return from each generator invocation is to return an integral value in the range [0...size). The compiler can enforce at compile-time that the index is in the valid range and dynamic indexes would not be permitted. In addition, there are three other types of index value which can be returned which give the compiler even more flexibility.

Here is an example where zeros are inserted into the even-indexed output positions of a SIMD value while the odd-indexed values are copied directly from the input.

permute(v, [](auto idx) { (idx % 2) ? simd_zero_element : idx; });

4.1.2. Generator function size argument

It can be useful to create libraries of common permutation generator functions to handle common use cases, but such functions need to be able to know the extent of the indexes that they are allowed to permute. The generator function is allowed to accept an optional std::size_t argument giving the size of the SIMD value being permuted. For example, the following generator extracts elements of a SIMD starting at the half-way point:

auto topHalf = [](std::integral auto idx, std::size_t simd_size) {
  return (simd_size / 2) + idx;
};

4.1.3. Design option - more special constants

There are other possible special constants that could be given special status in the compile-time permute, including the ability to insert NaN, +ve infinity, -ve infinite, max_value, min_value, and so on. Should these be adopted too?

4.1.4. Implementation Experience

In other simd or vector libraries which provide named specific permute functions, the reason often given is that it allows specific hardware instructions to be used to efficiently implement those permutes. For example, Intel has the vmovsldup instruction as noted above, so some libraries provide functions in their API with names which reflect this (e.g., such a function might be called DupLow). The disadvantages are that it hinders portability, and only allows access to the hardware features that the authors of the library have chosen to expose.

Using the generator-based approach makes it easier to access a wide range of underlying special purpose instructions, or to fall back to equivalent instructions sequences where they are not available, but it is desirable that such behavior should be efficient. To judge whether the generator-based permutation API was efficient and useful we implemented the extensions in the Intel std::simd library and looked at the performance of different compile-time patterns and their generated instruction sequences. We found that GCC and LLVM were able to determine the correct hardware instructions to use for a variety of common permutation patterns, and in some cases the compiler was even able to use the side-effects of other instructions to effect permutations in ways that human programmers had overlooked. In all cases, where the pattern was too complicated to map to native hardware features the compiler fell back to using general purpose permutation patterns, such as Intel’s permutex2var family of instructions.

The generator function works by creating a list of compile-time constants which are used to perform the permutation. Because the list is compile-time, that list can be generated using completely arbitrary (and potentially very inefficient) code, but the outcome will always be a list of constants. Therefore the compiler’s ability to generate a permutation is dependent upon how well it can convert a list of constants into a permutation, not how well the programmer is at writing clear permute functions.

As a small illustration of the compiler’s ability, the following table shows some compile-time function permute calls and the code that the LLVM 14.0.0 compiler has generated for each:

Permute call Purpose Output from clang 14.0.0
permute (x, [](auto idx) { return idx & ~1; }); Duplicate even elements vmovsldup zmm0, zmm0
permute (x, [](auto idx) { return idx ^ 1; }); Swap even/odd elements in each pair (complex-valued IQ swap) vpermilps ymm0, ymm0, 177
permute<8>(x, [](auto idx) { return idx + 8; }); Extract upper half of a 16-element vector. Note that the instruction sequence accepts a zmm input and returns a ymm output. vextractf64x4 ymm0, zmm0, 1

In each case the compiler has accepted the compile-time index constants and converted them into an instruction which efficiently implements the desired permutation. We can safely leave the compiler to determine the best instruction from an arbitrary compile-time function.

4.2. Using another SIMD as the dynamic index

The second proposal for the permute API is to allow the required indexes to be passed in using a second SIMD value containing the run-time indexes:

template<typename T, typename AbiT, std::integral U, typename AbiU>
constexpr resize_simd_t<simd_size_v<U, AbiU>, simd<T, AbiT>>
permute(const simd<T, AbiT>& v, const simd<U, AbiU>& indexes)

This can be used as in the following example:

fixed_size_simd<unsigned, 8> indexes = getIndexes();
fixed_size_simd<float, 5> values = getValues();
...
const auto result = permute(values, indexes);
// result::value_type is float
// result::size is 8

The permute function will return a new SIMD value which has the same element type as the input value being permuted, and the same number of elements as the indexing SIMD (i.e., float and 8 in the example above). The permute may duplicate or arbitrarily reorder the values. The index values must be of integral type, with no implicit conversions from other types permitted. The behavior is undefined if any index is outside the valid index range. Dynamic permutes across multiple input values are handled by concatenating the input values together. The indexes in the selector will only be treated as indexes, and will never have special properties as described above (e.g., no zero element selection)

In addition to the function called permute, a subscript operator will also be provided:

template <std::integral U, typename _AbiU>
constexpr resize_simd_t<simd_size_v<U, AbiU>, simd>
simd::operator[](const simd<U, AbiU> &indexes) const;

Here is an example of how this could be used:

fixed_size_simd<int, 8> indexes = getIndexes();
fixed_size_simd<float, 5> values = getValues();
...
const auto result = values[indexes];
// result::value_type is float
// result::size is 8

4.2.1. Design option - C++23 multi-index subscript

Sometimes the subscripts to use in a permute might be known at compile time, but to use them in a permutation requires them to be put into an index simd first:

constexpr simd<int> indexes = {3, 6, 1, 6}; // Note - assumes initialiser list from P2876R0.
auto output = values[indexes];

In C++23 multi-subscript operator was introduced and this could be used to allow a more compact representation:

auto output = values[3, 6, 1, 6];
// output::value_type is values::value_type
// output::size is 4

It would be good if it could also appear on the left-hand-side;

output_simd[2, 5, 6] = x; // Overwrite selected elements with a single value.

Should non-constant indexes be allowed too? Our experience with GCC and LLVM suggests that work would be needed on their code generation for non-constant variable indexes to be improved though, as they are both currently poor at this.

Should operator[] be reserved for slicing or sub-ranges?

4.2.2. Implementation experience

We implemented the runtime-indexed permute API in Intel’s example std::simd implementation. Small index operations (e.g., indexing one native sized simd by another) were mapped directly onto the underlying native instruction and so were as efficient as calling an intrinsic. More complicated cases for permutation, such as where either the index or data SIMD parameters were bigger than a native register are also handled with comparable efficiency to hand-written intrinsic code.

4.3. Permutation using simd masks (compress/expand)

A third variant of permutation is where a simd_mask is used as a proxy for an index sequence instead. The presence or absence of an element in the simd_mask will cause the element to be used or not. The following diagrams illustrate this:

…

On the left the values in the SIMD are compressed; only those elements which have their respective simd_mask element set will be copied into the next available space in the output SIMD. Their relative order will be maintained, and the selected elements will be stored contiguously in the output. The output value will have the same number of elements as the input value, with any unused elements being left in a valid but unspecified state. The behavior of the function is similar to std::copy_if, although other simd libraries may call this function compression or compaction instead. The expansion function on the right of the diagram has the opposite effect, copying values from a contiguous region to potentially non-contiguous positions indicated by a mask bit. The two functions have prototypes as follows:

template<typename T, typename Abi>
constexpr simd<T, Abi>
compress(const simd<T, Abi>& value, const simd_mask<T, MAbi>& mask);

template<typename T, typename Abi>
constexpr simd<T, Abi>
compress(const simd<T, Abi>& value, const simd_mask<T, MAbi>& mask, T fill_value);

template<typename T, typename Abi>
constexpr simd<T, Abi>
expand const simd<T, Abi>& value, const simd_mask<T, Abi>& mask, const simd<T, Abi>& original);

In both functions all simd or simd_mask values must have the same value_type and size.

A compression operation is typically used to throw away unwanted values (i.e., values which do not satisfy the mask condition). Unwanted positions in the output will be left unspecified, unless the user supplies a third parameter specifying the value which should be used to fill those unwanted positions.

An expansion operation is typically used to overwrite selected elements of some existing SIMD value with new values taken from the contiguous beginning of another SIMD value. The expansion operation therefore has a slightly different meaning for its fill value.

4.3.1. Design option - allow mask and values to be different sizes

Consider a compression operation. If the mask was bigger than the SIMD value being compressed then at runtime some values would be omitted, which is likely an error. Conversely, if the mask was smaller than the values being compressed then some values will never be selected because there can never be an active mask bit which would select them. This is likely also an error. Therefore the mask and the SIMD value being compressed should be the same size so that every element could potentially be selected, regardless of the runtime value. If the user wants to use a bigger or small mask then they should use resize one or other to make this intent explicit.

A similar argument follows for expansion.

4.3.2. Design option - unused value element initialisation

Consider a compression operation:

fixed_size_simd<int, 10> values = ...;
fixed_size_simd_mask<int, 6> mask = ...;

auto compressed = compress(values, mask);
// compressed::value_type is int
// compressed::size is 6

The output value has the same size as the mask selector, but the actual mask bits won’t be known until run-time. This raises the question of what values should be put into the unused output elements.

The current proposal would leave the unused elements in a valid but unspecified state. This is comparable to functions like std::shift_left and std::shift_right. The alternative is for the compress function to leave the unused values in a value-initialized state.

The advantage of an unspecified state is that the code doesn’t need to make a special effort to insert values which might not be used anyway, but has the disadvantage that the unspecified state might catch out the unwary user. Using a value initialized state helps the unwary user by providing a default state which is sane and repeatable, but may come with a performance cost.

Intel have an example implementation of std::simd, and our experience with that demonstrates that when native support is available (e.g., with AVX-512 ISA) it makes no difference to performance whether the unused values are initialising to specific value or left uninitialized, since the instruction itself inserts the values into unused positions as an integrated part of its operation. However, a synthesized code sequence is more expensive when setting the elements to a specific value since it is necessary to also compute the population count of the mask, turn that into a mask and then arrange for the unused values to be overwritten with something else using that mask.

Given that the compress function is essentially only extracting valid values from the input SIMD and discarding the others, and that there is a potential performance penalty that comes from initialising unused values, then we think that the default should be to leave unused elements in unspecified states.

Related to this is what to do when a programmer does want to initialize unused values to a known value. With the proposed interface the programmer would be required to compute a new partial mask from their original selection mask (i.e., compute the population count of the original mask, and then turn that into a mask of the lower elements), and then use that to conditionally blend in their desired value. This can be inefficient, and for targets like Intel AVX-512, it doesn’t exploit the hardware’s ability to automatically insert a value anyway. For this reason we propose that an overload of compress is provided that accepts a third parameter giving a value to insert into unused elements:

template<typename T, typename Abi>
simd<T, Abi> compress(const simd<T, Abi>& value, const simd_mask<T, Abi>& mask, T default_value);

This makes the programmer’s intent explicit in the code, it simplifies the code, and it allows those targets which support automatic insertion of a value to efficiently make use of that feature.

4.3.3. Implementation Experience

In Intel’s example implementation of std::simd we have implemented permute-by-mask. On modern processors which support AVX-512 or VBMI2 we found the mapping from compress or operator[] to be efficient, and allowed a natural representation of this compaction operation where needed. Implementing permute-by-mask on older processors which lacked native support was a little harder, but could still be implemented as efficiently as an experienced programmer could achieve using hand-written intrinsics. Determining the most efficient implementation under all conditions however showed that less experienced programmers would struggle to implement this feature. Therefore, making this feature available in std::simd itself removed any non-portable calls to native compression/expansion instructions, and also allowed the library implementer to use efficient sequences for different scenarios.

4.4. Memory-based permutes

When permuting values which are stored in memory, the operations are normally called gather (reading data from memory), or scatter (writing values to memory). We propose that two functions are provided which implement these operations:

template<std::contiguous_iterator Iter, std::integral_value Idx, typename Abi>
constexpr rebind_t<std::iter_value_t<Iter>, simd<Idx, Abi>>
gather(Iter in, const simd<Idx, Abi>& indexes);

template<std::contiguous_iterator Iter,
         typename T, typename TAbi,
         std::integral_value Idx, typename IdxAbi>
constexpr void
scatter(const simd<T, TAbi>, Iter out, const simd<Idx, IdxAbi>& indexes);

The gather operation could be used as follows:

int array[1024];
...
const auto r1 = gather(array, indexes);

The scatter would be used as follows:

int array[1024];
simd<int> values = ...;
simd<int> indexes = ...;
...
scatter(values, array, indexes);

4.4.1. Design option - allow subscripting a contiguous iterator

If [DNMSO] is allowed, then a pointer or std::contiguous_iterator would be used directly:

m = v.begin();
auto data = m[simd_values];

m[simd_values] = inputs; // Scatter to memory

4.4.2. Design option - allow gather/scatter to take masks

gather/scatter could be overloaded to accept simd_mask parameter, rather than explicit indexes:

template<std::contiguous_iterator Iter, std::integral_value Idx, typename Abi>
constexpr rebind_t<std::iter_value_t<Iter>, simd_mask<Idx, Abi>>
gather(Iter in, const simd<Idx, Abi>& indexes);

template<std::contiguous_iterator Iter,
         typename T, typename TAbi,
         std::integral_value Idx, typename IdxAbi>
constexpr void
scatter(const simd<T, TAbi>, Iter out, const simd_mask<Idx, IdxAbi>& indexes);

They can be used as follows:

auto data = gather(ptr, simd_mask); // Copy_if
scatter(ptr, simd, simd_mask);

Note that the masked gather essentially behaves like copy_if, overwriting output values where the respective predicate holds true. There is no existing function that is similar to the masked scatter.

5. Named functions

The APIs discussed in this document are very flexible and can be used to build effectively arbitrary permutations. However, there are inevitably certain patterns that are very common and it could be convenient to provide named functions for those patterns. For example:

auto dupEven(simdable v) {
    return permute(v, [](size_t idx) -> size_t { return (idx / 2) * 2; });
};
auto dupOdd(simdable v) {
    return permute(v, [](size_t idx) -> size_t { return idx | 1; });
};
auto swapOddEven(simdable auto v) {
    return permute(v, [](size_t idx) -> size_t { return idx ^1; });
}
auto even(simdable auto v) {
    return permute<v::size() / 2>(v, [](size_t idx) -> size_t { return idx * 2; };);
}

In many cases there are already names in C++ that reflect the operation. Providing overloads in std::simd which create SIMD functions with similar names to their existing C++ counterparts makes it clear to the programmer what the function is doing when applied to a SIMD value. For example:

std::rotate
std::shift_left
std::shift_right
std::copy_if
std::remove_if
std::slice // tricky - used for valarray at the moment, but it
           // also captures concepts like odd and even
std::stable_partition // Use a mask to split the values

These functions could be added at a later date.

6. Summary

In this document we have described four styles of interface for handling permutes: compile-time generated, simd-indexed, mask-indexed, and memory based. In combination, these interfaces allow all types of common permute to be expressed in a way which is clean, concise, and efficient for the compiler to implement.

7. Wording

7.1. Add new section [simd.static_permute]

simd static permute [simd.static_permute]

constexpr static int simd_zero_element   = /* Implementation defined */;
constexpr static int simd_uninit_element = /* Implementation defined */;

Constraints:

  • These cannot take values which could be mistaken for valid index values, so they must be outside the range [0..simd_abi::max_fixed_size].

Remarks:

  • Special constants which can be returned by the permutation generator function to indicate special behavior of that element.

// Free function to generate compile-time permute of simd
template<std::size_t SizeSelector = 0, typename T, typename Abi, typename PermuteGenerator>
constexpr resize_t<output-size, simd<T, Abi>>
permute(const simd<T, Abi>>& v, PermuteGenerator&& fn)

// Free function to generate compile-time permute of simd_mask
template<std::size_t SizeSelector = 0, typename T, typename Abi, typename PermuteGenerator>
constexpr resize_t<output-size, simd_mask<T, Abi>>
permute(const simd_mask<T, Abi>>& v, PermuteGenerator&& fn)

Constraints:

  • std::integral<std::invoke_result<PermuteGenerator, std::size_t> || std::integral<std::invoke_result<PermuteGenerator, std::size_t, std::size_t> is true.

Returns:

  • A simd object of size output-size (see below) where the ith element is initialized as follows. For each output index [0..output-size) the result of the expression simd(gen(integral_constant<size_t, i>())), or simd(gen(integral_constant<size_t, i>(), integral_constant<size_t,output-size)) is computed. The result index x is used to select a value from v as follows:

    • A value in the range [0..size) will use v[x].

    • A value in the range [size..0) will use v[v.size - x]

    • The value simd_zero_element will use T().

    • The value simd_uninit_element will use a valid but unspecified value of the compiler’s choosing.

    • Any other value is undefined.

    The output-size will be the same as v.size if SizeSelector is zero, otherwise it will be set to SizeSelector.

Remarks:

  • The calls to the generator are unsequenced with respect to each other.

7.2. Add new section [simd.dynamic_permute]

simd dynamic permute [simd.dynamic_permute]
// Free function to permute simd by dynamic index
template<typename T, typename AbiT, std::integral U, typename AbiU>
constexpr resize_simd_t<<simd_size_v<U, AbiU>, simd<T, AbiT>>
permute(const simd<T, AbiT>& v, const simd<U, AbiU>& indexes)

// Free function to permute simd_mask by dynamic index
template<typename T, typename AbiT, std::integral U, typename AbiU>
constexpr resize_simd_t<<simd_size_v<U, AbiU>, simd_mask<T, AbiT>>
permute(const simd_mask<T, AbiT>& v, const simd<U, AbiU>& indexes)

// Member function to permute simd by dynamic index using subscript operator
template<std::integral U, typename AbiU>
constexpr resize_simd_t<<simd_size_v<U, AbiU>, simd>
simd::operator[](const simd<U, AbiU>& indexes)

// Member function to permute simd_mask by dynamic index using subscript operator
template<std::integral U, typename AbiU>
constexpr resize_simd_t<<simd_size_v<U, AbiU>, simd_mask>
simd_mask::operator[](const simd<U, AbiU>& indexes)

Preconditions:

  • All values in indexes must be in the range [0..size].

  • All index elements must be integral types.

Returns:

  • A simd object of size simd_size_v<U, UAbi> where the ith element is a copy of v[indexes[i]] if i is in the range [0..v.size) or undefined otherwise.

7.3. Add new section [simd.mask_permute]

simd mask permute [simd.mask_permute]
// Free function to compress simd values using a mask selector
(1) template<typename T, typename Abi>
constexpr simd<T, Abi>
compress(const simd<T, AbiT>& v, const simd_mask<T, Abi>& selector)

// Free function to compress simd_mask elements using a mask selector
(2) template<typename T, typename Abi>
constexpr simd_mask<T, Abi>
compress(const simd_mask<T, AbiT>& v, const simd_mask<T, Abi>& selector)

// Free function to compress simd values using a mask selector
(3) template<typename T, typename Abi>
constexpr simd<T, Abi>
compress(const simd<T, AbiT>& v, const simd_mask<T, Abi>& selector, T fill_value)

// Free function to compress simd_mask elements using a mask selector
(4) template<typename T, typename Abi>
constexpr simd_mask<T, Abi>
compress(const simd_mask<T, AbiT>& v, const simd_mask<T, Abi>& selector, T fill_value)

Returns:

  • A simd or simd_mask object in which the first [0..reduce_count(selector)) values are copies of those elements in v whose corresponding mask element is set. The relative order of the values from v is unchanged.

  • For (1) and (2) output values in the range [reduce_count(selector)...v.size)) will be valid but unspecified values of type T.

  • For (3) and (4) output values in the range [reduce_count(selector)...v.size)) will be copies of fill_value.

Remarks:

  • The compress operation is used to throw away unwanted values, so the fill_value is being used purely to put sane values into unused positions. This is in contrast to expand where the operation is typically used to overwrite selected elements of an existing SIMD value and so unused positions come from another SIMD value.

// Free function to expand simd values using a mask selector
template<typename T, typename Abi>
constexpr simd<T, Abi>
expand(const simd<T, AbiT>& v, const simd_mask<T, Abi>& selector,
       const simd<T, Abi>& original = ())

// Free function to expand simd_mask elements using a mask selector
template<typename T, typename Abi>
constexpr simd_mask<T, Abi>
expand(const simd_mask<T, AbiT>& v, const simd_mask<T, Abi>& selector,
       const simd<T, Abi>& original = ())

Returns:

  • A simd or simd_mask object in which the first [0..reduce_count(selector)) values are copied to those output elements whose respective selector elements are set. Any output elements whose respective selector elements are not set will have the corresponding element from original copied into that position.

Remarks:

  • expand is often used to overwrite selected positions in an existing SIMD value, so these functions take a complete SIMD value as the source from which to take unused mask selector elements. This is in contrast to compress where the compression operation is effectively throwing away unwanted elements and the fill_value is being used to put sane values into unused positions.

7.4. Add new section [simd.memory_permute]

simd memory permute [simd.memory_permute]
template<std::contiguous_iterator Iter, std::integral_value Idx, typename Abi>
constexpr rebind_t<std::iter_value_t<Iter>, simd<Idx, Abi>>
gather(Iter in, const simd<Idx, Abi>& indexes);

Constraints:

  • std::integral<Idx> is true.

Preconditions:

  • All values in indexes must refer to elements which are within the range of the contiguous input iterator.

Returns:

  • A simd object with the same number of elements as indexes and the type of the element to which Iter refers. The ith element of the object will be a copy of the value in[indexes[i]].

template<typename T, typename TAbi,
         std::contiguous_iterator Iter,
         std::integral_value Idx, typename IdxAbi>
constexpr void
scatter(const simd<T, TAbi>& value, Iter out, const simd<Idx, IdxAbi>& indexes);

Constraints:

  • std::integral<Idx> is true.

  • simd_size_v<simd<T, TAbi>> == simd_size_v<simd<Idx, IdxAbi>> is true.

  • std::convertible_to<T, std::iter_value_t<Iter>> is true.

Preconditions:

  • All values in indexes must refer to elements which are within the range of the contiguous output iterator.

Effects:

  • For all i in the range [0..simd_size_v<Idx, IdxAbi>) the value at out[indexes[i]] will be a copy of value[i].

Remarks:

  • The order in which the individual memory writes occur is unspecified.

8. Acknowledgments

Thank you to Matthias Kretz for useful offline discussions regarding the behaviour and contents of the permutation API.

9. Polls

9.1. Telecon Meeting - 2nd May 2023

Poll: We should promise more committee time to pursuing P2664R2 (Proposal to extend std::simd with permutation API), knowing that our time is scarce and this will leave less time for other work.

SF F N A SA
6 4 1 0 0

Poll: Pursue extended indices in the permute callable (e.g., negative indices, special values for zero element, etc.).

SF F N A SA
5 4 1 0 0

Poll: Pursue multi-index subscripting for dynamic permutations in the initial revision.

SF F N A SA
0 1 8 2 0

Poll : Pursue operator[](simd) subscripting for dynamic permutations in the initial revision.

SF F N A SA
1 6 2 0 0

Poll: Pursue operator[](simd_mask) subscripting for dynamic expand (output[] = input) in the initial revision.

SF F N A SA
0 0 7 2 0

References

Informative References

[DNMSO]
Matthias Kretz. Non-Member Subscript Operator. URL: https://web-docs.gsi.de/~mkretz/DNMSO.pdf
[Highway]
Google. Google Highway. URL: https://github.com/google/highway/
[P0918R1]
Tim Shen. More simd<> Operations. 29 March 2018. URL: https://wg21.link/p0918r1
[P1915R0]
Matthias Kretz. Expected Feedback from simd in the Parallelism TS 2. 7 October 2019. URL: https://wg21.link/p1915r0
[P1928R3]
Matthias Kretz. Merge data-parallel types from the Parallelism TS 2. 3 February 2023. URL: https://wg21.link/p1928r3
[P2638R0]
Daniel Towner. Intel's response to P1915R0 for std::simd parallelism in TS 2. 22 September 2022. URL: https://wg21.link/p2638r0