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__内存空间描述符不可用于如下情况:

  • classstructunion数据成员
  • 形式参数
  • 在主机端执行的函数中不可声明非extern变量
  • 在设备端执行的函数中,除__shared__外另外三个描述符描述的变量不可声明为既非extern亦非static

此外,若有此四类描述符的变量的类型为自定义类(classstruct),则该类必须拥有空构造函数和空析构函数。

关于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是一个被constconstexpr 限定符修饰的变量或类的静态变量,且V没有被内存空间描述符(__device____shared____constant__)修饰,则常量V是一个主机端常量。但常量V仍然可以被设备端代码直接访问,只要V满足如下条件:

  • V在使用点前用一个常量表达式初始化
  • V没有被volatile限定符修饰
  • V是一个内置整型(int)或内置浮点型(float),但constexprconst宽松,只要是非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_listva_list类型,不能为右值引用类型。

不允许将带有虚函数的类的对象作为参数传递给__global__函数,同样的,不允许将虚继承的派生类的对象作为参数传递给__global__函数。

不能在__global__函数实例化或__device____constant__变量实例化的类型模板参数、无类型模板参数、模板模板参数中使用如下类型或模版:

  • 定义为__host____host__ __device__的类型或模版
  • 类型或模版是某个类的privateprotected成员且该类的父类(若有)没有定义在__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__
}

需要注意,当派生类重写基类的虚函数时,必须保证执行空间与基类虚函数的执行空间一致。但若函数FD为虚析构函数,且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不允许对内置变量(gridDimblockIdxblockDimthreadIdxwarpSize)赋值。

CUDA在设备端同样支持volatile限定符。

默认情况下,std::movestd::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_infodynamic_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
}
posted @ 2020-06-04 22:00  bookfree  阅读(4305)  评论(0编辑  收藏  举报