Developer Reference

Migrating OpenCL™ FPGA Designs to SYCL*

ID 767849
Date 5/08/2024
Public

C++ Features in Device Code

C++ offers many rich features that C and OpenCL extensions do not. The following are a few examples of C++ features you may find helpful when writing your SYCL* device code:

constexpr and static_assert

The constexpr feature has been available since C++11 (SYCL defaults to C++17). The constexpr keyword declares that the compiler can evaluate the expression value or function at compilation time. In SYCL, constexpr variables can replace preprocessor macros and are typically more flexible in their usage.

Suppose you want to define a constant called NUM_PIPELINES on the command line (using the -D<MACRO> compile flag), but you want it to default to some number. The following code demonstrates how you can achieve this in OpenCL and SYCL:

Defining a Constant With a Default Value
OpenCL SYCL
#ifndef NUM_PIPELINES 
#define NUM_PIPELINES 4 
#endif
#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

constexpr int kNumPipelines = NUM_PIPELINES;

Assume that you want to ensure that NUM_PIPELINES is in some range. The following code demonstrates how you can achieve this in OpenCL and SYCL:

Defining a Constant Within a Range
OpenCL SYCL
#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

// requires at least C11
static_assert(NUM_PIPELINES > 0, “error”);
static_assert(NUM_PIPELINES <= 64, “error”);
#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

constexpr int kNumPipelines = NUM_PIPELINES;

static_assert(kNumPipelines > 0, “error”);
static_assert(kNumPipelines <= 64, “error”);
NOTE:

In these examples, error is used as the string to static_assert for brevity. In general, you can use more meaningful messages here.

In the OpenCL version, the above code works only with C11 or higher. In both cases, the compiler emits an error if the conditions are not met. Assume you want to ensure that NUM_PIPELINES is a power of 2. In OpenCL, you cannot determine this at compile time easily. The only valid option is to check all the powers of 2 in the given range, which is not scalable as you increase the range. In SYCL, you can use constexpr functions to achieve this. The following table demonstrates how to add this check in both OpenCL and SYCL:

Checking for Powers of 2 in a Given Range
OpenCL SYCL
#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

// requires at least C11
static_assert(NUM_PIPELINES > 0, “error”);
static_assert(NUM_PIPELINES <= 32, “error”);
static_assert(NUM_PIPELINES == 1 ||
              NUM_PIPELINES == 2 ||
              NUM_PIPELINES == 4 ||
              NUM_PIPELINES == 16 ||
              NUM_PIPELINES == 32, “error”);	
#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

constexpr IsPow2(int n) { 
  return (n != 0) && ((n & (n - 1)) == 0);
}

constexpr int kNumPipelines = NUM_PIPELINES;

static_assert(kNumPipelines > 0, “error”);
static_assert(kNumPipelines <= 32, “error”);
static_assert(IsPow2(kNumPipelines), “error”);

Since the IsPow2 function is marked as constexpr, and its argument is also constexpr, the compiler can evaluate the IsPow2 function (and therefore the static_assert) at compile time.

The use of constexpr functions allows you to do more complex operations on constants. For example, given the previous code, imagine that you want to compute Log2(NUM_PIPELINES) and store that as another constant. In OpenCL, this would be a mess. You could define another macro, but you must change that macro manually every time NUM_PIPELINES changes. Moreover, if NUM_PIPELINES changes at the command line, you must change Log2(NUM_PIPELINES) also from the command line. This becomes even more difficult as you introduce more constants that depend on each other.

In SYCL, you can use constexpr functions to simplify the process. The following code shows how you can compute Log2 at compile time and then store the result as a constant:

#ifndef NUM_PIPELINES
#define NUM_PIPELINES 4
#endif

constexpr IsPow2(int n) { 
  return (n != 0) && ((n & (n - 1)) == 0);
}

constexpr Log2(int n) {
  T ret = 0;
  while (n >= 2) {
    ret++;
    n /= 2;
  }
  return ret;
}

constexpr int kNumPipelines = NUM_PIPELINES;

static_assert(kNumPipelines > 0, “error”);
static_assert(kNumPipelines <= 32, “error”);
static_assert(IsPow2(kNumPipelines), “error”);

constexpr int kLog2NumPipelines = Log2(kNumPipelines);

Types, Templating, and Type Traits

In general, you should write code that is modular and manageable. Assume that you want to change a design from 32-bit integers (int) to 16-bit integers (short). The following code shows how you can achieve this in OpenCL and SYCL:

Using Types
OpenCL SYCL
typedef int my_type;
using my_type = int;

Use my_type in the place of int. If you want to change from int to short, you just need to modify a single line. However, suppose you want to ensure that the type has certain characteristics. For example, you want to ensure that the type is integral and that its maximum value is more than some number (this is a good check for loop-index variables). In C++, you can achieve this by using the standard library's type_trait header. The following code shows how you can perform these checks in SYCL:

#include <type_traits>
#include <limits>

using my_type = int;

static_assert(std::is_integral_v<my_type>, “error”);
static_assert(std::numeric_limits<my_type>::max() >= 128, “error”);

Arbitrary Precision Integers

OpenCL has support for arbitrary precision integers with widths up to 64. In SYCL, you can achieve similar behavior by using the ac_int data type. The following example shows how you can construct a 10-bit arbitrary-precision integer in OpenCL and SYCL:

Constructing a 10-bit Arbitrary-Precision Integer
OpenCL SYCL
#include "ihc_apint.h"

int10_t x_signed;
uint10_t x_unsigned;
#include <sycl/ext/intel/ac_types/ac_int.hpp>

ac_int<10, true> x_signed;
ac_int<10, false> x_unsigned;

The C++ support in SYCL makes constructing and using arbitrary precision integers much easier. For example, since the ac_int width is templated, you can easily make the arbitrary-precision integer's width a compile time constant. This also enables the same constexpr functions and checks mentioned in the Types, Templating, and Type Traits section. The following table depicts the method for achieving this in OpenCL and SYCL (with the OpenCL code abbreviated):

Using Arbitrary-Precision Integers
OpenCL SYCL
#include "ihc_apint.h"

#ifndef WIDTH
// default value
#define WIDTH 10
#endif

#if WIDTH == 2
typedef int2_t my_ac_int_t;
typedef uint2_t my_uac_int_t;
#elif WIDTH == 3
typedef int3_t my_ac_int_t;
typedef uint3_t my_uac_int_t;
// … many more lines of code

my_ac_int_t x_signed;
my_uac_int_t x_unsigned;
#include <sycl/ext/intel/ac_types/ac_int.hpp>

#ifndef WIDTH
// default value
#define WIDTH 10
#endif
constexpr int kWidth = WIDTH;

using my_ac_int_t = ac_int<kWidth, true>;
using my_uac_int_t = ac_int<kWidth, false>;

my_ac_int_t x_signed;
my_uac_int_t x_unsigned;