int design choices in C++ AMP

Some of you might wonder why we chose int versus unsigned int versus size_t in some of the C++ AMP API signatures, particularly those in index, extent, array, etc. In this blog post, let’s take a close look at these integral type choices. We will discuss the rationale behind these choices, and explain some caveats that you may need to be aware of.

index<N>::value_type – int

The index<N> type represents a specific position in an N-dimensional space. In geometrical terms, a position is a vector that represents a point in space in relation to an arbitrary reference origin. Therefore, we need a signed integral typeas the value type of index<N> because the displacement from the reference origin to the point could be positive or negative. The following code snippet from the image blur sample shows how negative indices could be useful in real world applications:

 for (int dy = -1; dy <= 1; dy++)
 {
     for (int dx = -1; dx <= 1; dx++)
     {
         r += img[idx + index<2>(dy,dx)];
         samples++;
     }
 }

As we mentioned in the restrict(amp) restriction post, 64-bit integral types are not supported in a restrict(amp) functions, and thus, intis the natural choice for index<N>::value_type. Accordingly, we use intthroughout index<N>’s APIs, such as constructors, arithmetic operators and compound arithmetic operators, etc. For example,

index(int i0, int i1) restrict(cpu,amp); // N==2
explicit index(const int components[]) restrict(cpu,amp);
template <int N>friend index<N> operator+(const index<N>&amp; lhs, int rhs) restrict(cpu,amp);
index&amp; operator+=(int rhs) restrict(cpu,amp);

extent<N>::value_type – int ?

It’s natural to pick int as the value_type of index<N>, but it’s a bit counter-intuitive to make extent<N>::value_type int too because some people would intuitively consider an extent<N> object as sizes of a region in the N-dimensional space, and unsigned int is more appropriate to represent sizes.

We choose int for a practical reason. Often we need to compare an index against an extent to see if the index is within the region. If extent<N>’s value_type were unsigned int, such comparison would have resulted in the compiler warning C4018: 'expression': signed/unsigned mismatch.

Note that although an extent<N> object could have negative bounds, it is required to use extents with positive bounds for parallel_for_each and constructors of C++ AMP containers. This requirement enables us to avoid some bounds checking and transformation in our runtime when mapping those containers to DirectX resources, and thus, optimize the runtime for the most common cases. A runtime exception will be triggered if an invalid extent is provided to construct an array , array_view, or texture, or to invoke parallel_for_each. For example,

 #include <amp.h>
#include <iostream>

using namespace concurrency;
int main()
{
    extent<1> e(-3);
    try
    {
        array<int, 1> a(e); 
    }
    catch (const runtime_exception &e)
    {
        std::cout << e.what() << std::endl;
    }
}

      

Output: Invalid - values for each dimension must be > 0

Since extent<N>::value_type is int, similar to index<N>, we use int in most of extent<N>’s APIs, including constructors, arithmetic and compound arithmetic operators. One exception is the size() member function, whose return type is unsigned int. This is because a size should be a non-negative number, and thus an unsigned integral type was chosen.

Why not size_t?

But then, you may ask, why not size_t? Isn’t size_t recommended to be used since it’s guaranteed to be big enough to represent the size of any object, and it makes the code more portable?

The short answer is that size_t is a typedef of an unsigned 64-bit integer type on a 64-bit system, and we can only support 32-bit integral types in restrict(amp) functions for this release.

The long answer is that we want to make our compilation model efficient and future proofing. Theoretically we could typedef size_t to a 32-bit unsigned integral type for code inside restrict(amp) functions even on 64-bit host system since they are compiled to be executed on an AMP compatible accelerator, which could have its own specifications independent from the host architecture. However, that would result in different interpretation and layouts of some code and data structures on host and device code, and thus require marshaling and complicated coding, and a separate compilation pass for amp-restricted code. In addition, in a foreseeable near future, when an accelerator is able to share memory with a CPU and directly share data structures, having accelerator-specific data layouts will be very problematic. Therefore, we decided not to make size_t a portable type for amp code for this release. If it’s used in an amp-restricted function, and the code is compiled for 64-bit systems, the following compilation error will be reported:

error C3581: 'size_t': unsupported type in amp restricted code

Note that this restriction only applies to amp-restricted functions. size_t is still widely used in many cpu-only C++ AMP APIs.

Compiler Warning (level 3) C4267

All of our C++ AMP containers have constructors that take an extent<N> object to represent the shape. Because the value type of extent<N> is int, these containers also have convenient constructors for rank 1 to 3 that directly take int parameters so that you don’t need to construct an extent<N> object beforehand. It’s also very common in C++ AMP code to use std containers like vector to initialize an array or array_view. Unfortunately, this often leads to a code pattern that would trigger the level 3 warning C4267 when compiling for 64-bit systems:

 vector<int> v(1024);
array_view<int> a(v.size(), v);

      

warning C4267: 'argument' : conversion from 'size_t' to 'int', possible loss of data

You could add a static_cast to silent this warning if you know the vector’s size is within int’s data ranges.

array_view<int> a(static_cast<int>(v.size()), v);

Parameter type of operator[] and operator() of AMP containers – int?

You can use operator[] or operator() to access elements of an array, array_view, or texture. Usually you pass an index<N> object to these functions. For rank 1 to 3, we also provide convenient overloads that take int parameters so that you don’t need to construct an index<N> object beforehand. As you can see, we choose int instead of unsigned int as parameter types for these convenient accessors because these parameters are used to construct an index<N> object internally and the value type of index<N> is int.

Optimization with static_cast<unsigned int>

If you like to study header files closely, you might have spotted several static_cast<unsigned int> in our AMP header files. They are actually optimizations we did because on accelerators, it could be faster to do unsigned int division and modulo operations than on signed integers, depending on hardware and drivers. You may want to keep that in mind and apply this trick in your C++ AMP code if applicable.

This concludes this blog post on the integral type choices in some of C++ AMP APIs and the rationale behind these design decisions. As always, you are welcome to ask questions and provide feedback below or on our MSDN forum.