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>& lhs, int rhs) restrict(cpu,amp);
index& 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.
Comments
Anonymous
May 02, 2012
Regarding "C4018: 'expression': signed/unsigned mismatch.", can't you users suffix their numbers with the u or U unsigned-suffix?Anonymous
May 02, 2012
Thanks for explaining the thoughts behind the design, I've been wondering about this since the start! :-) While I understand your points, I have to admit I've found it a somewhat frustrating distraction from productivity when adopting (CPU) C++ code to try it out in C++ AMP. I've been consistently using size_type member-types (typedefs) for STL-style containers and size_t for almost everything else (ptrdiff_t for the signed cases -- which I was about to suggest as a solution here until I've seen the 64-bit-unsupported part). My reasons are not just limited to the fact that I'm supposed to do that for 64-bit code working w/ 64-bit data ("int" is not merely sub-optimal for indexing in this case, it's incorrect), but also include major benefits derived from the simplicity of self-documenting code (it's very clear that a variable will represent size if its type is size_t or an appropriate container's size_type). Since it's become an idiomatic practice in good-style C++, it's also less confusing. Having to change all the size-types in the code just to make it compile under restrict(amp) can be somewhat frustrating. I'm wondering, perhaps a reasonable compromise would be to add STL-style member-types for concurrency::array and concurrency::array_view -- i.e., simple typedefs (just as in std::vector) that map to whatever your implementation-detail happens to be (currently: int)? If you recall a talk "C++11 Style – A Touch of Class" given recently by Bjarne Stroustrup (at Going Native 2012), interfaces are easier to use and understand when practicing type-rich programming. In particular, I mean this part: ecn.channel9.msdn.com/.../GN12Cpp11Style.pdf Type-rich Programming / Focus on interfaces Underspecified / overly general: void increase_speed(double); Better: void increase_speed(Speed); Even if "Speed" is merely a typedef to "double", it's immediately clear what it means. The benefits are also that it's an additive change, not a destructive change (so there's no loss of backward compatibility; even though C++ AMP is at beta stage it's arguably important -- this could actually give the C++ AMP devteam greater flexibility to change the implementation-details in the future, in case the need ever arises, without affecting the users) and follows the established (and well-known) conventions of the STL containers included in the C++ Standard (so it could even reduce the learning curve -- and allow one to (re)use some functions written for the standard containers that rely on the std. interface conventions). Just as a disclaimer, I do understand that C++ AMP is not STL, but if we could get as close to #7 from www.danielmoth.com/.../C-Accelerated-Massive-Parallelism.aspx as possible, I think it'd be a major win-win! :-) Thoughts?Anonymous
May 02, 2012
Correction to the above: I meant #6, not #7 // although that one is pretty cool, too ;-)Anonymous
May 02, 2012
@Yves - I think u or U unsigned-suffix is for literal constants. Here I was referring to something like "idx[0] < ext[0]".Anonymous
May 03, 2012
I see. Thanks.Anonymous
May 04, 2012
@MattPD Thanks for your feedbacks! I totally understand the porting inconvenience caused by the fact that 64-bits integral types especially size_t are not supported in restrict(amp) functions. This is a constraint based on the desire for as much hardware portability as possible. DirectX 11 is the Windows standard for hardware portability across GPUs. In the future, as GPUs continue to evolve, the Windows standard will also evolve to provide new features without compromising our promise to developers of portability. The main reason I wrote up this post was to make our C++ AMP users aware of these caveats so that they will be better prepared when hitting these problems, and know the workarounds. Hopefully DX will soon support 64-bit integral types so that we could remove this restriction in our next release. Adding STL-style member-types is a great suggestion! Actually, we do have STL-style member-types in some of our classes. For example, both index<N> and extent<N> have a member type called “value_type”, which you could use in your code instead of the bare “int” to make your code a bit more future proofing even if we change the value_types and other APIs in our future release. I agree with you that it would be a better design if we use STL-style member-types in all of our interfaces. But as you know, this is the first release of C++ AMP, we didn’t get enough time to reach the most refined design. Valuable feedback from our users like you would definitely help us get there in our next release. So keep feedbacks coming! :-) Thanks a lot! Lingli