CUDA中关于C++特性的限制
CUDA中关于C++特性的限制
CUDA官方文档中对C++语言的支持和限制,懒得每次看英文文档,自己尝试翻译一下(没有放lambda表达式的相关内容,太过于复杂,我选择不用)。官方文档https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#c-cplusplus-language-support
__CUDA_ARCH__
在如下3种情况下,开发者不应该依赖于__CUDA_ARCH__
宏是否定义或__CUDA_ARCH__
的具体值来确定代码:
typedef
typedef
用于下列几种情况时:
__global__
函数及其函数模版__device__
和__constant__
变量- 纹理和surfaces
如下所示:
#if !defined(__CUDA_ARCH__)
typedef int mytype;
#else
typedef double mytype;
#endif
__device__ mytype xxx; // error: xxx's type depends on __CUDA_ARCH__
__global__ void foo(mytype in, // error: foo's type depends on __CUDA_ARCH__
mytype *ptr) {
*ptr = in;
}
函数模版实例化
__global__
函数实例化:
__device__ int result;
template <typename T>
__global__ void kern(T in) {
result = in;
}
__host__ __device__ void foo(void) {
#if !defined(__CUDA_ARCH__)
kern<<<1,1>>>(1); // error: "kern<int>" 的实例化必须依赖于__CUDA_ARCH__未定义
}
单独编译
extern
的函数或变量定义:
#if !defined(__CUDA_ARCH__)
void foo(void) {/*...*/ } // error: foo函数实现依赖于__CUDA_ARCH__未定义
#endif
特别是若头文件中定义的函数实现依赖于__CUDA_ARCH__
,而用到该函数的多个对象在不同arch
下编译会导致该函数的行为不一致,从而引起函数冲突。
CUDA还规定,若设备端代码调用了一个被声明为extern
的函数,则该函数和设备端代码必须在同一arch
下编译。
空间描述符
内存空间描述符
__device__
、__shared__
、__managed__
、__constant__
内存空间描述符不可用于如下情况:
class
、struct
、union
数据成员- 形式参数
- 在主机端执行的函数中不可声明非extern变量
- 在设备端执行的函数中,除
__shared__
外另外三个描述符描述的变量不可声明为既非extern亦非static
此外,若有此四类描述符的变量的类型为自定义类(class
或struct
),则该类必须拥有空构造函数和空析构函数。
关于C++类在什么情况下会拥有空构造函数和空析构函数可参考《深度探索C++对象模型》
在全编译模式下,此四类描述符变量不能被定义为extern
变量,无论主机端设备端(当然,动态共享内存声明不包含在内),但在单独编译模式下可以。
__shared__
变量不能将初始化作为其声明的一部分。
托管变量
__managed__
托管内存变量具有如下限制:
- 托管变量的地址非常量
- 托管变量不应有
const
限定 - 托管变量不应为引用类型
- 当CUDA运行时可能不处于有效状态时,不得使用托管变量的地址或值:
- 在对象的静态/动态初始化或析构过程中,而该对象属于静态或本地作用域
- 在exit后被调用的代码中(如gcc的
__attribute__((destructor))
属性) - 在CUDA运行时被初始化前的代码中(如gcc的
__attribute__((constructor))
属性)
- 托管变量不可作为
decltype()
表达式的unparenthesized参数 - 托管变量和动态分配的托管内存一样具有连贯性和一致性行为(coherence and consistency)
- 当含托管变量的CUDA程序运行在多GPU设备中时,该托管变量只会被分配一次,而不是每个GPU分配一个
- 在主机端执行的函数中不可使用非外部链接(
extern
)的托管变量 - 在设备端执行的函数中不可使用非外部链接(
extern
)或静态链接(static
)的托管变量
示例:
__device__ __managed__ int xxx = 10; // OK
struct S1_t {
int field;
S1_t(void) : field(xxx) { };
};
S1_t temp1; // error: use of managed variable
// (xxx) in dynamic initialization
struct S2_t {
~S2_t(void) { xxx = 10; }
};
S2_t temp2; // error: use of managed variable
// (xxx) in the destructor of
// object with static storage
// duration
__device__ __managed__ const int yyy = 10; // error: const qualified type
__device__ __managed__ int &zzz = xxx; // error: reference type
int *ptr = &xxx; // error: use of managed variable
// (xxx) in static initialization
template <int *addr> struct S3_t { };
S3_t<&xxx> temp; // error: address of managed
// variable(xxx) not a
// constant expression
__global__ void kern(int *ptr) {
assert(ptr == &xxx); // OK
xxx = 20; // OK
}
int main(void) {
int *ptr = &xxx; // OK
kern<<<1,1>>>(ptr);
cudaDeviceSynchronize();
xxx++; // OK
decltype(xxx) qqq; // error: managed variable(xxx) used
// as unparenthized argument to
// decltype
decltype((xxx)) zzz = yyy; // OK
}
const常量
__device__
, __constant__
和__shared__
变量不允许被声明为constexpr
的。
若常量V
是一个被const
或constexpr
限定符修饰的变量或类的静态变量,且V
没有被内存空间描述符(__device__
、__shared__
、__constant__
)修饰,则常量V
是一个主机端常量。但常量V
仍然可以被设备端代码直接访问,只要V
满足如下条件:
V
在使用点前用一个常量表达式初始化V
没有被volatile
限定符修饰V
是一个内置整型(int
)或内置浮点型(float
),但constexpr
比const
宽松,只要是非long double
的标量即可。
对于constexpr
常量V
,若函数F
是一个__device__ constexpr
或__host__ __device__ constexpr
函数,且该函数被常量表达式调用,则即使常量V
是一个非标量类型,也可以被函数F
直接使用。
显然,设备端代码不能引用V
或取V
的地址。
默认情况下,一个constexpr
函数不能被执行空间不兼容的另一个函数调用,但可以通过nvcc选项--expt-relaxed-constexpr
移除这个限制,从而可以在主机端调用__device__ constexpr
函数,反之亦然,也可以在设备端调用__host__ constexpr
函数。开发者可以通过__CUDACC_RELAXED_CONSTEXPR__
宏是否定义来判断编译器是否开启这个选项。
需要注意即使模版函数被constexpr
关键字标记,但该模版函数的实例化函数不一定就是constexpr
函数。
示例:
const int xxx = 10;
struct S1_t { static const int yyy = 20; };
constexpr int host_arr[] = { 1, 2, 3};
constexpr __device__ int get(int idx) { return host_arr[idx]; }
extern const int zzz;
const float www = 5.0;
__device__ void foo(void) {
int local1[xxx]; // OK
int local2[S1_t::yyy]; // OK
int val1 = xxx; // OK
const float val5 = www; // OK
int val2 = S1_t::yyy; // OK
int val3 = zzz; // error: zzz在使用点前没有被常量表达式初始化
const int &val3 = xxx; // error: __device__不能引用一个主机端常量
const int *val4 = &xxx; // error: __device__不能取一个主机端常量的地址
int v1 = xxx + 4 + S1_t::yyy; // OK
v1 += get(2); // OK
v1 += get(idx); // get(idx)不是一个常量表达式
v1 += host_arr[2]; // host_arr[2]不是一个标量
}
const int zzz = 20; // error: 注意zzz是在使用点后被初始化的
函数和类
__global__
函数
__global__
函数传参是通过常量内存传入设备端的,且规定参数大小不得大于4KB。此外__global__
函数不支持可变长参数。另外,开发者不能将一个操作符函数(如operator+、operator-
等等)声明为__global__
的,目前__global__
函数尚不支持递归,不支持作为类的静态成员函数,支持类的友元声明但不支持在友元声明同时进行定义,例如:
class S1_t {
friend __global__
void foo1(void); // OK: 友元声明但未定义
template<typename T>
friend __global__
void foo2(void); // OK: __global__函数模版也是一样
friend __global__
void foo3(void) { } // error: 友元声明的同时进行定义
template<typename T>
friend __global__
void foo4(void) { } // error: __global__函数模版也是一样
};
我们可以取到函数的函数指针,但主机端代码获取到的__global__
函数指针不可用于设备端代码,反之亦然。显然,主机端代码不能获取device
函数的函数指针,设备端代码也不能获取__host__
函数的函数指针。类似的,不允许在设备端调用创建于主机端的对象的虚函数,反之亦然。
__global__
函数或模版不能被声明为constexpr
的,其参数不能为std::initializer_list
或va_list
类型,不能为右值引用类型。
不允许将带有虚函数的类的对象作为参数传递给__global__
函数,同样的,不允许将虚继承的派生类的对象作为参数传递给__global__
函数。
不能在__global__
函数实例化或__device__
、__constant__
变量实例化的类型模板参数、无类型模板参数、模板模板参数中使用如下类型或模版:
- 定义为
__host__
、__host__ __device__
的类型或模版 - 类型或模版是某个类的
private
或protected
成员且该类的父类(若有)没有定义在__device__
或__global__
函数中 - 匿名类型
- 上述任何类型的复合
例:
template <typename T>
__global__ void myKernel(void) { } //__global__模版函数
class myClass {
private:
struct inner_t { };
public:
static void launch(void) {
myKernel<inner_t><<<1,1>>>(); // error: inner_t类是private成员
}
};
template <typename T> __device__ T d1; //__device__模版变量
template <typename T1, typename T2> __device__ T1 d2;
void fn() {
struct S1_t { };
d1<S1_t> = {}; // error: S1_t是__host__的
auto lam1 = [] { };
d2<int, decltype(lam1)> = 10; // error: lam1是一个匿名类型
}
__global__
支持可变参数模版,但只允许一个pack
参数,且该pack
参数必须置于模版参数最后。
static
修饰符
CUDA尚不支持类的static
静态数据成员,除非同时被static const
限定符修饰。而在__device__
、__global__
执行空间的函数中,仅允许普通变量(无任何内存空间描述符)和__shared__
变量使用static
修饰符,而在__device__ __host__
函数中则只允许普通变量使用static
修饰符。此外,需要注意若static
变量类型为自定义类class
,则该类必须拥有空构造函数和空析构函数。此外,static
变量不允许动态初始化,例如:
struct S1_t {
int x;
};
struct S2_t {
int x;
__device__ S2_t(void) { x = 10; } //非空构造函数
};
struct S3_t {
int x;
__device__ S3_t(int p) : x(p) { } //非空构造函数
};
__device__ void f1() {
static int i1; // OK
static S1_t i3; // OK,空构造函数
static int i2 = 11; // OK,静态初始化
static S1_t i4 = {22}; // OK,静态初始化
static __shared__ int i5; // OK,__device__函数__shared__变量可用static修饰
int x = 33;
static int i6 = x; // error: 动态初始化
static S1_t i7 = {x}; // error: 动态初始化
static S2_t i8; // error: 非空构造函数
static S3_t i9(44); // error: 非空构造函数
}
函数的执行空间
若函数F
在首次声明时被显式或隐式声明为默认函数(如果首次显式声明为默认函数时有指定执行空间,指定的执行空间会被忽略,但如果不是在首次声明时显式默认,则执行空间为指定的执行空间,下述规则忽略),则函数F
的执行空间描述符(__host__
、__device__
)为所有调用函数F
的函数的执行空间描述符的集合(__global__
也视为__device__
)。例如:
class Base {
int x;
public:
__host__ __device__ Base(void) : x(10) {}
};
class Derived : public Base { //隐式声明了一个默认构造函数
int y;
};
class Other: public Base { //隐式声明了一个默认构造函数
int z;
};
__device__ void foo(void) {
Derived D1; //Derived的默认构造函数仅被foo函数调用,因此
//Derived::Derived()的执行空间为__device__
Other D2;
}
__host__ void bar(void) {
Other D3; //Other的默认构造函数被foo和bar调用,因此
//Other::Other()的执行空间为__host__ __device__
}
需要注意,当派生类重写基类的虚函数时,必须保证执行空间与基类虚函数的执行空间一致。但若函数F
、D
为虚析构函数,且D
没有隐式定义或在非首次声明的声明中显式默认,则F
覆盖的每个虚析构函数D
的执行空间的集合既为F
的执行空间。例如:
struct Base1 { virtual __host__ __device__ ~Base1() {/*...*/} }; //~Base1()显式定义非默认
struct Derived1 : Base1 { }; // ~Derived1()的执行空间为 __host__ __device__
struct Base2 { virtual __device__ ~Base2(); }; //首次声明非显式默认
__device__ Base2::~Base2() = default; //非首次声明的声明中显示默认
struct Derived2 : Base2 { }; // ~Derived2()的执行空间为__device__
命名空间范围内匿名union
的成员变量不能被__global__
或__device__
函数引用。
封闭类成员函数的执行空间与定义该封闭类的的最内层的那个指定了执行空间的函数的执行空间相同,若所有嵌套函数都没有指定执行空间,或该类不是定义在函数中,则其成员函数的执行空间为__host__
。
C++特性
CUDA不允许对内置变量(gridDim
、blockIdx
、blockDim
、threadIdx
、warpSize
)赋值。
CUDA在设备端同样支持volatile
限定符。
默认情况下,std::move
和std::forward
函数的执行空间为__host__ __device__
,因此开发者同样可以在设备端调用这两个函数。
CUDA新增__int128_
、_Complex
、__float128
类型,但这些类型只能在主机端使用,且__float128
类型只支持64位Linux平台,同时注意编译器可能会以精度较低的浮点数类型处理__float128
类型的常量表达式。
CUDA设备端代码不支持long double
类型,不支持thread_local
限定符。
CUDA还支持gcc等编译器的deprecated
属性,nvcc选项-Wno-deprecated-declarations
将禁用所有弃用警告,而-Werror=deprecated-declarations
选项会将所有弃用警告转换为error。
C++ RTTI(运行时类型识别)特性(typeid
运算符、std::type_info
、dynamic_cast
运算符)仅支持主机端代码,不支持设备端代码。类似的,C++异常仅支持主机端代码,不支持设备端代码(包括__global__
函数)。
CUDA目前尚不支持设备端的STL。
默认情况下,std::initializer_list
的成员函数默认为__host__ __device__
执行空间,因此开发者可以在主机端和设备端都能调用std::initializer_list
的成员函数,示例:
#include <initializer_list>
__device__ int foo(std::initializer_list<int> in);
__device__ void bar(void) {
foo({4,5,6}); // (a) OK
int i = 4;
foo({i,5,6}); // (b) OK
}