E.V.E
v2023.02.15
 
Loading...
Searching...
No Matches
Conditional operations

When you call a function on one or more SIMD values, you expect the computation to be performed on every elements of its parameters. Sometimes, you may want to make the application of a given function dependent on some condition.

Let's explore the functionalities EVE provides for achieving those results.

Explicit Selection

Let's say the function we want to write computes the product of two values a and b if a is equal to b and their difference otherwise.

The scalar code is looking like:

auto square_or_diff( float a, float b)
{
return a == b ? a * b : a - b;
}

The SIMD version of this code can't use if nor the ternary operator directly. The correct approach is to use the eve::if_else function.

#include <eve/module/core.hpp>
#include <eve/wide.hpp>
#include <iostream>
auto square_or_diff( w_t const& a, w_t const& b )
{
std::cout << (a == b) << "\n";
std::cout << a*b << "\n";
std::cout << a-b << "\n";
return eve::if_else( a == b, a * b, a - b );
}
int main()
{
w_t v1 = { 1, 2, 3 , 4 };
w_t v2 = { 1, -2, 10, 4 };
std::cout << square_or_diff(v1,v2) << "\n";
}
constexpr auto if_else
Select value based on conditional mask or values.
Definition if_else.hpp:192
Wrapper for SIMD registers.
Definition wide.hpp:86

The eve::if_else call explicitly requests we pass:

  • the condition, i.e the comparison between a and b
  • the value to use whenever an element of said condition evaluates to true, here the product of a and b
  • the value to use whenever an element of said condition evaluates to false, here the difference of a and b
Warning
Contrary to a if ... else statement, eve::if_else will evaluates all its arguments before performing its selection even if potential short-cut can be applied later on.

Conditional Function Syntax

Let's define a sqrt_positive function that computes the square root of its argument if it's positive or returns it unchanged otherwise. One can write:

#include <eve/module/core.hpp>
#include <eve/wide.hpp>
#include <iostream>
auto sqrt_positive( w_t const& a )
{
return eve::if_else( a >= 0, eve::sqrt(a), a);
}
int main()
{
w_t v = { 1, -2, 10, -3.5 };
std::cout << sqrt_positive(v) << "\n";
}
constexpr auto sqrt
Computes the square root of the parameter.
Definition sqrt.hpp:80

This code is perfectly valid and will produce the correct result. However, it has some issues:

  • the code looks like the important part is the test
  • the code can't be optimized in case the current architecture support masked operations (i.e. AVX512 or sve)

To go beyond those limitations, EVE functions supports – whenever it makes sense – a conditional call syntax:

#include <eve/module/core.hpp>
#include <eve/wide.hpp>
#include <iostream>
auto sqrt_positive( w_t const& a )
{
return eve::sqrt[a >= 0](a);
}
int main()
{
w_t v = { 1, -2, 10, -3.5 };
std::cout << sqrt_positive(v) << "\n";
}

The code of sqrt_positive now works differently:

  • the a >= 0 expression is evaluated
  • the eve::sqrt[a >= 0] expression builds a new callable object that will perform the conditional call to eve::sqrt
  • this bound callable object is then called over a
  • Wherever the condition is true, the eve::sqrt function will be applied.
  • Wherever the condition is false, the value of the first argument of the function will be returned.

The fact the conditional syntax builds a new callable object is interesting because it ensures that any optimization over the conditional computation can be captured and that this new callable can be passed as-if to algorithms without having to worry about changing the number of arguments requested.

If required, the callable object produced by the conditional syntax can be stored into a variable:

auto sqrt_positive_temp( w_t const& a )
{
auto const f = eve::sqrt[a >= 0];
return f(a);
}

Conditional Expressions

If passing a simple logical expression is the most common use-case of the conditional syntax, one may requires more flexibility. To do so, EVE provides various objects to express more elaborated conditions.

Mask with alternative

One may want to use the conditional syntax to call a function but instead of returning the first argument if the condition is false, one may want to return an arbitrary value. This use case is handled by the eve::if_ helper by wrapping logical expression so that an alternative value can be specified.

Let's modify sqrt_positive so that, if the argument is not positive, 0 is returned instead.

#include <eve/module/core.hpp>
#include <eve/wide.hpp>
#include <iostream>
auto sqrt_positive( w_t const& a )
{
return eve::sqrt[ eve::if_(a >= 0) ](a);
}
auto sqrt_positive_else( w_t const& a )
{
return eve::sqrt[ eve::if_(a >= 0).else_(0) ](a);
}
int main()
{
w_t v = { 1, -2, 10, -3.5 };
std::cout << sqrt_positive(v) << "\n";
std::cout << sqrt_positive_else(v) << "\n";
}
auto else_(V const &v) const
Extends a conditional expression with an alternative value.
Definition conditional.hpp:111
Extensible wrapper for SIMD conditional.
Definition compress_copy_scalar.hpp:16

Context-sensitive mask

Some algorithms require conditional function calls but use logical expression relative to the element index inside a eve::simd_value rather than its value. One may want for example to not compute an expression on the first and last element of such eve::simd_value.

A frequent example is trying to load data from memory while ignoring trailing garbage or out of bounds values:

#include <eve/module/core.hpp>
#include <array>
#include <iostream>
auto load_not_first( float* ptr )
{
return eve::load[ eve::ignore_first(1).else_(99) ](ptr);
}
auto load_not_last( float* ptr )
{
return eve::load[ eve::ignore_last(1).else_(42) ](ptr);
}
int main()
{
std::array<float,eve::wide<float>::size()> data;
for(std::size_t i=0; i < data.size(); ++i) data[i] = 1+i;
std::cout << load_not_first(&data[0] - 1) << "\n";
std::cout << load_not_last(&data[1]) << "\n";
}
constexpr auto else_(V const &v) const
Extends a conditional expression with an alternative value.
Definition conditional.hpp:429
Conditional expression ignoring the k first lanes from a eve::simd_value.
Definition conditional.hpp:459
constexpr auto else_(V const &v) const
Extends a conditional expression with an alternative value.
Definition conditional.hpp:317
Conditional expression ignoring the k last lanes from a eve::simd_value.
Definition conditional.hpp:332

Here, the eve::ignore_first and eve::ignore_last conditionals take a number of elements as parameter that describe which zone of the eve::simd_value won't be affected. By default, the value of the not loaded lanes are undefined. As for other eve::conditional_expr, we can affix them with an alternative (99 and 42 respectively) to replace the not loaded pieces.

But what if we want to apply our operation to every element but the first and last one ? Clearly, calling two operations with two different conditional masks is sub-optimal and EVE provides some more conditional expressions to express this need.

The first is to use the eve::keep_between helper:

#include <eve/module/core.hpp>
#include <iostream>
auto load_between( float* ptr )
{
return eve::load[ eve::keep_between(1,3).else_(-6.3f) ]( ptr );
}
int main()
{
std::array<float,eve::wide<float>::size()-1> data;
for(std::size_t i=0; i < data.size(); ++i) data[i] = 1+i;
std::cout << load_between(&data[0] - 1) << "\n";
}
constexpr auto else_(V const &v) const
Extends a conditional expression with an alternative value.
Definition conditional.hpp:489
Conditional expression keeping all lanes between two position.
Definition conditional.hpp:523

eve::keep_between uses ad-hoc indexes, which makes the code a bit too size dependent. One can also use the same conditional but use a similar interface to eve::ignore_first.

#include <eve/module/core.hpp>
#include <iostream>
auto load_between( float* ptr )
{
return eve::load[ (eve::ignore_first(1) && eve::ignore_last(1)).else_(-6.3f) ]( ptr );
}
int main()
{
std::array<float,eve::wide<float>::size()-1> data;
for(std::size_t i=0; i < data.size(); ++i) data[i] = 1+i;
std::cout << load_between(&data[0] - 1) << "\n";
}

The output is obviously the same.

Conclusion

Conditional operations on SIMD values is a good way to keep a high level code over some complex computations. EVE provides different levels of abstraction for such operations as well as various helpers to specify how the conditions can be computed based either on values or indexes.