5.3. C++ 语言支持
nvcc按照以下规范处理 CUDA 和设备代码:
C++03(ISO/IEC 14882:2003),
--std=c++03标志。C++11(ISO/IEC 14882:2011),
--std=c++11标志。C++14(ISO/IEC 14882:2014),
--std=c++14标志。C++17(ISO/IEC 14882:2017),
--std=c++17标志。C++20(ISO/IEC 14882:2020),
--std=c++20标志。C++23(ISO/IEC 14882:2024),
--std=c++23标志。
向nvcc传入-std=c++<version>标志会开启与指定版本相关的所有 C++ 特性,并使用对应的 C++ 方言选项调用主机预处理器、编译器和链接器。
编译器支持所支持标准的所有语言特性,但受以下各节中列出的限制约束。
5.3.1. C++11 语言特性
语言特性 |
C++11 提案 |
NVCC/CUDA Toolkit 7.x |
|---|---|---|
✅ |
||
|
✅ |
|
使用右值初始化类对象 |
✅ |
|
非静态数据成员初始化器 |
✅ |
|
可变参数模板 |
✅ |
|
扩展可变参数模板模板形参 |
✅ |
|
✅ |
||
静态断言 |
✅ |
|
|
✅ |
|
多声明符 |
✅ |
|
移除作为存储类说明符的 auto |
✅ |
|
新的函数声明符语法 |
✅ |
|
✅ |
||
表达式的声明类型 |
✅ |
|
不完整的返回类型 |
✅ |
|
右尖括号 |
✅ |
|
函数模板的默认模板实参 |
✅ |
|
解决表达式的 SFINAE 问题 |
✅ |
|
别名模板 |
✅ |
|
extern 模板 |
✅ |
|
空指针常量 |
✅ |
|
强类型枚举 |
✅ |
|
枚举的前置声明 |
✅ |
|
标准化的属性语法 |
✅ |
|
✅ |
||
对齐支持 |
✅ |
|
有条件支持的行为 |
✅ |
|
将未定义行为改为可诊断的错误 |
✅ |
|
委托构造函数 |
✅ |
|
继承构造函数 |
✅ |
|
显式转换运算符 |
✅ |
|
新的字符类型 |
✅ |
|
Unicode 字符串字面量 |
✅ |
|
原始字符串字面量 |
✅ |
|
字面量中的通用字符名 |
✅ |
|
用户定义的字面量 |
✅ |
|
标准布局类型 |
✅ |
|
✅ |
||
弃置函数 |
✅ |
|
扩展的友元声明 |
✅ |
|
扩展 |
✅ |
|
✅ |
||
不受限制的联合体 |
✅ |
|
✅ |
||
基于范围的 for |
✅ |
|
显式 |
✅ |
|
对垃圾回收和基于可达性的泄漏检测的最低限度支持 |
❌ |
|
允许移动构造函数抛出异常 [noexcept] |
✅ |
|
定义移动特殊成员函数 |
✅ |
|
并发 |
||
序列点 |
❌ |
|
原子操作 |
❌ |
|
强比较并交换 |
❌ |
|
双向栅栏 |
❌ |
|
内存模型 |
❌ |
|
数据依赖排序:原子操作与内存模型 |
❌ |
|
传播异常 |
❌ |
|
允许在信号处理函数中使用原子操作 |
❌ |
|
线程局部存储 |
❌ |
|
并发情况下的动态初始化与析构 |
❌ |
|
C++11 中的 C99 特性 |
||
|
✅ |
|
C99 预处理器 |
✅ |
|
|
✅ |
|
扩展整数类型 |
❌ |
|
5.3.2. C++14 语言特性
语言特性 |
C++14 提案 |
NVCC/CUDA Toolkit 9.x |
|---|---|---|
调整某些 C++ 按语境转换 |
✅ |
|
二进制字面量 |
✅ |
|
✅ |
||
泛化的 lambda 捕获(初始化捕获) |
✅ |
|
泛型(多态)lambda 表达式 |
✅ |
|
✅ |
||
放宽对 constexpr 函数的要求 |
✅ |
|
成员初始化器与聚合体 |
✅ |
|
明确内存分配 |
❌ |
|
带大小的释放 |
❌ |
|
|
✅ |
|
使用单引号作为数字分隔符 |
✅ |
5.3.3. C++17 语言特性
语言特性 |
C++17 提案 |
NVCC/CUDA Toolkit 11.x |
|---|---|---|
移除三字符组 |
✅ |
|
|
✅ |
|
折叠表达式 |
✅ |
|
命名空间和枚举项的属性 |
✅ |
|
嵌套命名空间定义 |
✅ |
|
允许对所有非类型模板实参进行常量求值 |
✅ |
|
扩展 |
✅ |
|
从花括号初始化器列表推导 |
✅ |
|
允许在模板模板形参中使用 typename |
✅ |
|
|
✅ |
|
|
✅ |
|
|
✅ |
|
扩展聚合初始化 |
✅ |
|
|
✅ |
|
一元折叠与空形参包 |
✅ |
|
泛化基于范围的 for 循环 |
✅ |
|
lambda 按值捕获 |
✅ |
|
|
✅ |
|
C++ 的十六进制浮点字面量 |
✅ |
|
过度对齐数据的动态内存分配 |
✅ |
|
保证的复制消除 |
✅ |
|
为惯用 C++ 代码细化表达式求值顺序 |
✅ |
|
|
✅ |
|
带初始化器的选择语句 |
✅ |
|
类模板的模板实参推导 |
✅ |
|
使用 |
✅ |
|
使用属性命名空间而无需重复 |
✅ |
|
忽略不支持的非标准属性 |
✅ |
|
✅ |
||
移除 |
✅ |
|
移除已弃用的 |
✅ |
|
使异常说明成为类型系统的一部分 |
✅ |
|
用于 C++17 的 |
✅ |
|
重新表述继承构造函数(核心问题 1941 等) |
✅ |
|
✅ |
||
DR 150,模板模板实参的匹配 |
✅ |
|
移除动态异常说明 |
✅ |
|
using 声明中的包展开 |
✅ |
|
|
✅ |
|
DR 727,类内显式实例化 |
✅ |
5.3.4. C++20 语言特性
GCC 版本 ≥ 10.0、Clang 版本 ≥ 10.0、Microsoft Visual Studio ≥ 2022,以及 nvc++ 版本 ≥ 20.7。
注意
以“DR:”为前缀的条目是缺陷报告的解决方案。它们对标准进行修正,也适用于较早的 C++ 标准模式(例如 C++17);此处列出它们是为了完整性,并非 C++20 特有。
语言特性 |
C++20 提案 |
NVCC/CUDA Toolkit 12.x |
|---|---|---|
位域的默认成员初始化器 |
✅ |
|
修正 |
✅ |
|
允许 lambda 捕获 |
✅ |
|
用于预处理器逗号省略的 |
✅ |
|
指派初始化器 |
✅ |
|
用于泛型 lambda 的熟悉的模板语法 |
✅ |
|
vector 的列表推导 |
✅ |
|
概念 |
P0734R0 |
✅ |
带初始化器的基于范围的 for 语句 |
✅ |
|
简化隐式 lambda 捕获 |
✅ |
|
ADL 与不可见的函数模板 |
✅ |
|
默认化复制构造函数的 |
✅ |
|
减少对 |
✅ |
|
一致的比较( |
P0515R3 |
✅ |
对特化的访问检查 |
✅ |
|
可默认构造且可赋值的无状态 lambda |
✅ |
|
不求值语境中的 lambda |
✅ |
|
对空对象的语言支持 |
✅ |
|
放宽基于范围的 for 循环的定制点查找规则 |
✅ |
|
✅ |
||
放宽结构化绑定的定制点查找规则 |
✅ |
|
去掉 typename! |
✅ |
|
允许在 lambda 初始化捕获中进行包展开 |
✅ |
|
|
✅ |
|
弃用通过 |
✅ |
|
非类型模板形参中的类类型 |
✅ |
|
非类型模板形参的不一致之处 |
✅ |
|
带填充位的原子比较并交换 |
✅ |
|
用于可变大小类的高效带大小 delete |
✅ |
|
允许在常量表达式中调用虚函数 |
✅ |
|
禁止聚合体具有用户声明的构造函数 |
✅ |
|
|
✅ |
|
有符号整数采用二进制补码 |
✅ |
|
|
✅ |
|
立即函数( |
✅ |
|
|
✅ |
|
嵌套的 |
✅ |
|
放宽 |
✅ |
|
特性测试宏 |
✅ |
|
模块 |
P1103R3 |
❌ |
协程 |
❌ |
|
使用圆括号初始化聚合体 |
✅ |
|
DR: new 表达式中的数组大小推导 |
✅ |
|
DR: 从 |
✅ |
|
更严格的 Unicode 要求 |
✅ |
|
结构化绑定扩展 |
✅ |
|
弃用 |
✅ |
|
弃用 |
✅ |
|
|
✅ |
|
|
✅ |
|
聚合体的类模板实参推导 |
✅ |
|
别名模板的类模板实参推导 |
✅ |
|
允许转换为未知边界的数组 |
✅ |
|
|
✅ |
|
布局兼容性与指针可互转换性特征 |
✅ |
|
DR: 检查抽象类类型 |
✅ |
|
DR: 更多隐式移动 |
✅ |
|
DR: 伪析构函数终止对象生存期 |
✅ |
5.3.5. C++23 语言特性
GCC 版本 ≥ 14.0、Clang 版本 ≥ 18.0、Microsoft Visual Studio(不支持),以及 nvc++ 版本 ≥ 24.3。
注意
以“DR:”为前缀的条目是缺陷报告的解决方案。它们对标准进行修正,也适用于较早的 C++ 标准模式(例如 C++17、C++20);此处列出它们是为了完整性,并非 C++23 特有。
注意
NVCC 列中的N/A表示该特性不适用于设备代码(例如,移除未使用的标准措辞,如垃圾回收支持,或由主机定义的行为)。
语言特性 |
C++23 提案 |
NVCC/CUDA Toolkit ≥ 13.3 |
|---|---|---|
核心问题 411、1656 和 2333 的拟议解决方案;字符和字符串字面量中的数值转义与通用字符转义 |
✅ |
|
(有符号) |
✅ |
|
使 lambda 中的 |
✅ |
|
|
✅ |
|
移除垃圾回收支持 |
N/A |
|
DR: 使用 Unicode 标准附录 31 的 C++ 标识符语法 |
✅ |
|
DR: 允许重复属性 |
✅ |
|
到 bool 的窄化按语境转换 |
❌ |
|
在行拼接前去除空白字符 |
✅ |
|
强制按声明顺序布局 |
✅ |
|
混合字符串字面量拼接 |
N/A |
|
constexpr 函数中的非字面类型变量(以及标签和 goto) |
✅ |
|
推导 |
✅ |
|
一致的字符字面量编码 |
✅ |
|
添加对预处理指令 |
✅ |
|
诊断文本的字符编码 |
✅ |
|
扩展初始化语句以允许别名声明 |
✅ |
|
更改 lambda 尾置返回类型的作用域 |
✅ |
|
多维下标运算符 |
✅ |
|
字符集与编码 |
✅ |
|
|
✅ |
|
C++20 核心提案缺失的特性测试宏 |
✅ |
|
lambda 表达式上的属性 |
✅ |
|
对 |
✅ |
|
移除不可编码的宽字符字面量和多字符宽字符字面量 |
✅ |
|
复合语句末尾的标签(C 兼容性) |
✅ |
|
带界定符的转义序列 |
✅ |
|
放宽 |
❌ |
|
更简单的隐式移动 |
✅ |
|
命名的通用字符转义 |
✅ |
|
|
✅ |
|
|
✅ |
|
扩展浮点类型与标准名称 |
❌ |
|
可移植的假设 |
✅ |
|
支持将 UTF-8 作为可移植的源文件编码 |
✅ |
|
DR: |
✅ |
|
DR: 撤销对 volatile 按位复合赋值操作的弃用 |
❌ |
|
DR: 放宽对 |
N/A |
|
DR: 在常量表达式中使用未知指针和引用 |
❌ |
|
DR: 你正在寻找的相等运算符 |
❌ |
|
允许在 |
✅ |
|
延长基于范围的 for 循环初始化器中临时对象的生存期 |
✅ |
|
DR: |
✅ |
5.3.6. CUDA C++ 标准库
CUDA 提供了 C++ 标准库(STL)的一种实现,称为libcu++。该库具有以下优势:
主机和设备上均可使用这些功能。
兼容 CUDA Toolkit 最近两个主要版本支持的所有GPU 架构。
兼容当前和上一个主要版本的所有CUDA Toolkit。
提供近期标准版本(包括 C++20、C++23 和 C++26)中可用的 C++ 标准库特性的 C++17 向后移植版本。
支持扩展数据类型,例如 128 位整数(
__int128)、半精度浮点数(__half)、Bfloat16(__nv_bfloat16)和四倍精度浮点数(__float128)。针对设备代码进行了高度优化。
此外,libcu++还提供 C++ 标准库中没有的扩展特性,以提高生产力和应用程序性能。这些特性包括数学函数、内存操作、同步原语、容器扩展、CUDA 内建函数的高级抽象、C++ PTX 封装等。
libcu++既作为CUDA Toolkit的一部分提供,也作为开源CCCL代码库的一部分提供。
5.3.7. C 标准库函数
5.3.7.1. clock()和clock64()
__host__ __device__ clock_t clock();
__device__ long long clock64();
在设备代码中执行时,它返回每个多处理器各自拥有的计数器的值,该计数器在每个时钟周期递增。在核函数开始和结束时对这个计数器采样,将两个值相减,并为每个线程记录结果,即可估算设备执行该线程所花费的时钟周期数。但是,这个值并不代表设备执行该线程指令所花费的实际时钟周期数。由于线程以时间片方式运行,前者大于后者。
提示
对应的CUDA C++ 函数
cuda::std::clock()在<cuda/std/ctime>头文件中提供。
5.3.7.2. printf()
__host__ __device__ __tile__ int printf(const char* format[, arg, ...]);
该函数将核函数中的格式化输出打印到主机端输出流。
核函数内的printf()函数的行为与标准 C 库的printf()函数类似。用户应查阅其主机系统的手册页,以获取对printf()行为的完整说明。本质上,以format传入的字符串会被输出到主机上的一个流中。
printf()命令的执行方式与其他任何设备端函数相同:按线程执行,并在调用线程的上下文中执行。在多线程核函数中,对printf()的一次直接调用将由每个线程使用该线程指定的数据来执行。因此,主机流中将出现多个版本的输出字符串,每个版本对应一个遇到printf()的线程。
C 标准的printf()返回打印的字符数,而 CUDA 的printf()与之不同,返回的是已解析的参数个数。如果格式字符串后没有参数,则返回 0。如果格式字符串为NULL,则返回-1。如果发生内部错误,则返回 -2。
在内部,printf()使用共享的数据结构,因此调用printf()可能会改变线程的执行顺序。具体而言,调用printf()的线程可能会经过比不调用printf()的线程更长的执行路径,而该路径的长度取决于printf()的参数。但是,请注意,除显式的__syncthreads()屏障外,CUDA 不对线程的执行顺序作任何保证。因此,无法判断执行顺序是被printf()改变的,还是被硬件中的其他调度行为改变的。
printf()函数在分块代码中的行为与在 SIMT 设备代码中的行为不同。在分块代码中,
参数除了可以是标量,还可以是分块。传入分块参数时,分块中的每个元素都按相应的格式说明符打印。
返回值始终是所提供的参数个数,即使格式字符串为
NULL或发生内部错误也是如此。格式字符串必须是字面量。
如果参数个数与格式说明符的个数不匹配,则会报告错误。在 SIMT 设备代码中,这种情况会产生警告。
格式说明符
与标准printf()一样,格式说明符采用以下形式:%[flags][width][.precision][size]type
支持以下字段。有关所有行为的完整说明,请参阅广泛可获取的文档。
标志:
#、' '、0、+、-宽度:
*、0-9精度:
0-9大小:
h、l、ll类型:
%cdiouxXpeEfgGaAs
限制
printf()输出的最终格式化在主机系统上进行。这意味着格式字符串必须能够被主机系统的编译器和 C 库理解。虽然已经尽一切努力,确保 CUDA 的printf()函数所支持的格式说明符是最常见主机编译器所支持的格式说明符的通用子集,但具体行为仍取决于主机操作系统。
printf()接受标志和类型的所有有效组合。这是因为它无法确定在对最终输出进行格式化的主机系统上,哪些组合有效,哪些组合无效。因此,如果程序输出的格式字符串包含无效组合,则输出可能是未定义的。
除格式字符串外,printf()函数最多可以接受 32 个实参。任何额外的实参都会被忽略,而格式说明符将按原样输出。
由于long类型在 Windows 平台(32 位)和 Linux 平台(64 位)上的大小不同,在 Linux 机器上编译后又在 Windows 机器上运行的核函数,对于所有包含%ld的格式字符串都会产生损坏的输出。为确保安全,建议编译平台与执行平台保持一致。
主机端缓冲区
printf()的输出缓冲区在核函数启动前被设置为固定大小。该缓冲区是循环缓冲区,因此,如果核函数执行期间产生的输出超出缓冲区的容量,较早的输出就会被覆盖。只有在执行以下操作之一时,才会刷新缓冲区:
通过
<<< >>>或cuLaunchKernel()启动核函数:在启动开始时刷新;如果CUDA_LAUNCH_BLOCKING环境变量设置为 1,则在启动结束时也会刷新,通过
cudaDeviceSynchronize()、cuCtxSynchronize()、cudaStreamSynchronize()、cuStreamSynchronize()、cudaEventSynchronize()或cuEventSynchronize()进行同步,通过
cudaMemcpy*()或cuMemcpy*()的任何阻塞版本进行内存复制,通过
cuModuleLoad()或cuModuleUnload()加载/卸载模块,通过
cudaDeviceReset()或cuCtxDestroy()销毁上下文。在执行通过
cudaLaunchHostFunc()或cuLaunchHostFunc()添加的流回调之前。
请注意,程序退出时不会自动刷新缓冲区。
以下 API 函数用于设置和获取缓冲区的大小,该缓冲区用于将printf()实参和内部元数据传输到主机。默认大小为一兆字节。
cudaDeviceGetLimit(size_t* size,cudaLimitPrintfFifoSize)cudaDeviceSetLimit(cudaLimitPrintfFifoSize, size_t size)
示例
以下代码示例:
#include <stdio.h>
__global__ void helloCUDA(float value) {
printf("Hello thread %d, value=%f\n", threadIdx.x, value);
}
int main() {
helloCUDA<<<1, 5>>>(1.2345f);
cudaDeviceSynchronize();
return 0;
}
将输出:
Hello thread 2, value=1.2345
Hello thread 1, value=1.2345
Hello thread 4, value=1.2345
Hello thread 0, value=1.2345
Hello thread 3, value=1.2345
请注意,每个线程都会遇到printf()命令。因此,输出的行数与网格中的线程数相同。
参见Compiler Explorer上的示例。
以下代码示例:
#include <stdio.h>
__global__ void helloCUDA(float value) {
if (threadIdx.x == 0)
printf("Hello thread %d, value=%f\n", threadIdx.x, value);
}
int main() {
helloCUDA<<<1, 5>>>(1.2345f);
cudaDeviceSynchronize();
return 0;
}
将输出:
Hello thread 0, value=1.2345
显然,if()语句限制了哪些线程会调用printf(),因此只能看到一行输出。
参见Compiler Explorer上的示例。
以下代码示例:
#include "cuda_tile.h"
#include <cstdio>
namespace ct = cuda::tiles;
__tile_global__ void kernel() {
auto ints = ct::iota<ct::tile<int, ct::shape<4, 4>>>();
printf("%i\n", ints);
}
int main() {
kernel<<<1,1>>>();
cudaDeviceSynchronize();
return 0;
}
将输出:
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]
5.3.7.3. memcpy()和memset()
__host__ __device__ __tile__ void* memcpy(void* dest, const void* src, size_t size);
该函数将size个字节从src所指向的内存位置复制到dest所指向的内存位置。
__host__ __device__ __tile__ void* memset(void* ptr, int value, size_t size);
该函数将ptr所指向的内存块中的size个字节设置为value,该值被解释为unsigned char。
提示
建议使用<cuda/std/cstring>头文件中提供的cuda::std::memcpy()和cuda::std::memset()函数,作为memcpy和memset的更安全版本。
5.3.7.4. malloc()和free()
__host__ __device__ void* malloc(size_t size);
// or cuda::std::malloc(), cuda::std::calloc() in the <cuda/std/cstdlib> header
函数malloc()(设备端)、cuda::std::malloc()和cuda::std::calloc()从设备堆中分配至少size个字节,并返回指向所分配内存的指针。如果内存不足以满足请求,则返回NULL。返回的指针保证对齐到 16 字节边界。
__device__ void* __nv_aligned_device_malloc(size_t size, size_t align);
// or cuda::std::aligned_alloc() in the <cuda/std/cstdlib> header
函数__nv_aligned_device_malloc()和C++cuda::std::aligned_alloc()从设备堆中分配至少size个字节,并返回指向所分配内存的指针。如果内存不足以满足所请求的大小或对齐要求,则返回NULL。所分配内存的地址是align的倍数。align必须是非零的二的幂。
__host__ __device__ void free(void* ptr);
// or cuda::std::free() in the <cuda/std/cstdlib> header
设备端函数free()和cuda::std::free()释放ptr所指向的内存,该指针必须由之前对malloc()、cuda::std::malloc()、cuda::std::calloc()、__nv_aligned_device_malloc()或cuda::std::aligned_alloc()的调用返回。如果ptr为NULL,则对free()或cuda::std::free()的调用会被忽略。使用同一个ptr重复调用free()或cuda::std::free()会产生未定义行为。
给定 CUDA 线程通过malloc()、cuda::std::malloc()、cuda::std::calloc()、__nv_aligned_device_malloc()或cuda::std::aligned_alloc()分配的内存,在 CUDA 上下文的整个生存期内都会保持已分配状态,或者直到通过调用free()或cuda::std::free()显式释放为止。其他 CUDA 线程可以使用这些内存,包括后续核函数启动中的线程。任何 CUDA 线程都可以释放另一个线程分配的内存;但是,应注意确保同一个指针不会被释放多次。
堆内存 API
必须在任何在设备代码中分配或释放内存的程序运行之前指定设备内存堆的大小,这也包括使用new和delete关键字的情况。如果任何程序在未显式指定堆大小的情况下使用设备内存堆,则会分配一个八兆字节的默认堆。
以下 API 函数用于获取和设置堆大小:
cudaDeviceGetLimit(size_t* size, cudaLimitMallocHeapSize)cudaDeviceSetLimit(cudaLimitMallocHeapSize, size_t size)
实际提供的堆大小将至少为size个字节。cuCtxGetLimit()和cudaDeviceGetLimit()返回当前请求的堆大小。
堆的实际内存分配发生在模块加载到上下文中时,加载既可以通过 CUDA 驱动程序 API 显式进行(参见模块),也可以通过 CUDA 运行时 API 隐式进行。如果内存分配失败,模块加载会产生CUDA_ERROR_SHARED_OBJECT_INIT_FAILED错误。
模块加载后,堆大小便无法更改,而且它不会根据需要动态调整大小。
为设备堆保留的内存,是在通过cudaMalloc()等主机端 CUDA API 调用分配的内存之外额外保留的。
与主机内存 API 的互操作性
通过设备端函数malloc()、cuda::std::malloc()、cuda::std::calloc()、__nv_aligned_device_malloc()、cuda::std::aligned_alloc()或new关键字分配的内存,不能通过cudaMalloc、cudaMemcpy或cudaMemset等运行时或驱动程序 API 调用来使用或释放。同样,通过主机运行时 API 分配的内存,不能使用设备端函数free()、cuda::std::free()或delete关键字来释放。
按线程分配的示例:
#include <stdlib.h>
#include <stdio.h>
__global__ void single_thread_allocation_kernel() {
size_t size = 123;
char* ptr = (char*) malloc(size);
memset(ptr, 0, size);
printf("Thread %d got pointer: %p\n", threadIdx.x, ptr);
free(ptr);
}
int main() {
// Set a heap size of 128 megabytes.
// Note that this must be done before any kernel is launched.
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128 * 1024 * 1024);
single_thread_allocation_kernel<<<1, 5>>>();
cudaDeviceSynchronize();
return 0;
}
将输出:
Thread 0 got pointer: 0x20d5ffe20
Thread 1 got pointer: 0x20d5ffec0
Thread 2 got pointer: 0x20d5fff60
Thread 3 got pointer: 0x20d5f97c0
Thread 4 got pointer: 0x20d5f9720
请注意,每个线程都会遇到malloc()和memset()命令,因此会获得并初始化自己分配的内存。
参见Compiler Explorer上的示例。
按线程块分配的示例:
#include <stdlib.h>
__global__ void block_level_allocation_kernel() {
__shared__ int* data;
// The first thread in the block performs the allocation and shares the pointer
// with all other threads through shared memory, so that access can be coalesced.
if (threadIdx.x == 0) {
size_t size = blockDim.x * 64; // 64 bytes per thread are allocated.
data = (int*) malloc(size);
}
__syncthreads();
// Check for failure
if (data == nullptr)
return;
// Threads index into the memory, ensuring coalescence
for (int i = 0; i < 64; ++i)
data[i * blockDim.x + threadIdx.x] = threadIdx.x;
// Ensure all threads complete before freeing
__syncthreads();
// Only one thread may free the memory!
if (threadIdx.x == 0)
free(data);
}
int main() {
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128 * 1024 * 1024);
block_level_allocation_kernel<<<10, 128>>>();
cudaDeviceSynchronize();
return 0;
}
参见Compiler Explorer上的示例。
在核函数启动之间持续保留内存分配的示例:
#include <stdlib.h>
#include <stdio.h>
const int NUM_BLOCKS = 20;
__device__ int* data_ptrs[NUM_BLOCKS]; // Per-block pointer
__global__ void allocate_memory_kernel() {
// Only the first thread in the block performs the allocation
// since we need only one allocation per block.
if (threadIdx.x == 0)
data_ptrs[blockIdx.x] = (int*) malloc(blockDim.x * 4);
__syncthreads();
// Check for failure
if (data_ptrs[blockIdx.x] == nullptr)
return;
// Zero the data with all threads in parallel
data_ptrs[blockIdx.x][threadIdx.x] = 0;
}
// Simple example: store the thread ID into each element
__global__ void use_memory_kernel() {
int* ptr = data_ptrs[blockIdx.x];
if (ptr != nullptr)
ptr[threadIdx.x] += threadIdx.x;
}
// Print the content of the buffer before freeing it
__global__ void free_memory_kernel() {
int* ptr = data_ptrs[blockIdx.x];
if (ptr != nullptr)
printf("Block %d, Thread %d: final value = %d\n",
blockIdx.x, threadIdx.x, ptr[threadIdx.x]);
// Only free from one thread!
if (threadIdx.x == 0)
free(ptr);
}
int main() {
cudaDeviceSetLimit(cudaLimitMallocHeapSize, 128*1024*1024);
// Allocate memory
allocate_memory_kernel<<<NUM_BLOCKS, 10>>>();
// Use memory
use_memory_kernel<<<NUM_BLOCKS, 10>>>();
use_memory_kernel<<<NUM_BLOCKS, 10>>>();
use_memory_kernel<<<NUM_BLOCKS, 10>>>();
// Free memory
free_memory_kernel<<<NUM_BLOCKS, 10>>>();
cudaDeviceSynchronize();
return 0;
}
参见Compiler Explorer上的示例。
5.3.7.5. alloca()
__host__ __device__ void* alloca(size_t size);
alloca()函数在调用方的栈帧内分配size个字节的内存。返回值是指向所分配内存的指针。从设备代码中调用此函数时,该内存的起始地址按 16 字节对齐。当调用方从alloca()返回时,该内存会自动释放。
注意
在 Windows 平台上,使用alloca()函数之前必须包含<malloc.h>头文件。对alloca()的调用可能导致栈溢出;用户需要相应地调整栈大小。
示例:
__device__ void device_function(int num_items) {
int4* ptr = (int4*) alloca(num_items * sizeof(int4));
// use of ptr
...
}
5.3.8. Lambda 表达式
__host__。示例:
auto global_lambda = [](){ return 0; }; // __host__
void host_function() {
auto lambda1 = [](){ return 1; }; // __host__
[](){ return 3; }; // __host__, closure type (body of a lambda expression)
}
__device__ void device_function() {
auto lambda2 = [](){ return 2; }; // __device__
}
__global__ void kernel_function(void) {
auto lambda3 = [](){ return 3; }; // __device__
}
__host__ __device__ void host_device_function() {
auto lambda4 = [](){ return 4; }; // __host__ __device__
}
__tile__ void tile_function() {
auto lambda5 = [](){ return 5; }; // __tile__
}
__tile__ __device__ void tile_device_function() {
auto lambda6 = [](){ return 6; }; // __tile__ __device__
}
using function_ptr_t = int (*)();
__device__ void device_function(float value,
function_ptr_t ptr = [](){ return 4; } /* __host__ */) {}
参见Compiler Explorer上的示例。
5.3.8.1. Lambda 表达式与__global__函数参数
只有当执行空间为__device__或__host__ __device__时,lambda 表达式或闭包类型才能用作__global__函数的实参。全局作用域或命名空间作用域中的 lambda 表达式不能用作__global__函数中的实参。
示例:
template <typename T>
__global__ void kernel(T input) {}
__device__ void device_function() {
// device kernel call requires separate compilation (-rdc=true flag)
kernel<<<1, 1>>>([](){});
kernel<<<1, 1>>>([] __device__() {}); // extended lambda
kernel<<<1, 1>>>([] __host__ __device__() {}); // extended lambda
}
auto global_lambda = [] __host__ __device__() {};
void host_function() {
kernel<<<1, 1>>>([] __device__() {}); // CORRECT, extended lambda
kernel<<<1, 1>>>([] __host__ __device__() {}); // CORRECT, extended lambda
// kernel<<<1, 1>>>([](){}); // ERROR, closure type with host execution space
// kernel<<<1, 1>>>(global_lambda); // ERROR, extended lambda, but at global scope
}
参见Compiler Explorer上的示例。
5.3.8.2. 扩展 lambda
nvcc的--extended-lambda标志允许在 lambda 表达式中显式标注执行空间。这些标注应出现在 lambda 引入符之后、可选的 lambda 声明符之前。--extended-lambda标志时,nvcc会定义宏__CUDACC_EXTENDED_LAMBDA__。扩展 lambda定义在
__host__或__host__ __device__函数的直接块或嵌套块的作用域内。扩展设备 lambda是使用
__device__关键字标注的 lambda 表达式。扩展主机-设备 lambda是使用
__host__ __device__关键字标注的 lambda 表达式。
与标准 lambda 表达式不同,扩展 lambda 可以用作__global__函数中的类型实参。
示例:
void host_function() {
auto lambda1 = [] {}; // NOT an extended lambda: no explicit execution space annotations
auto lambda2 = [] __device__ {}; // extended lambda
auto lambda3 = [] __host__ __device__ {}; // extended lambda
auto lambda4 = [] __host__ {}; // NOT an extended lambda
}
__host__ __device__ void host_device_function() {
auto lambda1 = [] {}; // NOT an extended lambda: no explicit execution space annotations
auto lambda2 = [] __device__ {}; // extended lambda
auto lambda3 = [] __host__ __device__ {}; // extended lambda
auto lambda4 = [] __host__ {}; // NOT an extended lambda
}
__device__ void device_function() {
// none of the lambdas within this function are extended lambdas,
// because the enclosing function is not a __host__ or __host__ __device__ function.
auto lambda1 = [] {};
auto lambda2 = [] __device__ {};
auto lambda3 = [] __host__ __device__ {};
auto lambda4 = [] __host__ {};
}
auto global_lambda = [] __host__ __device__ { }; // NOT an extended lambda because it is not defined
// within a __host__ or __host__ __device__ function
5.3.8.3. 扩展 lambda 类型特征
编译器提供了类型特征,用于在编译时检测扩展 lambda 的闭包类型。
bool __nv_is_extended_device_lambda_closure_type(type);
如果type是为扩展__device__lambda 创建的闭包类,则该函数返回true,否则返回false。
bool __nv_is_extended_device_lambda_with_preserved_return_type(type);
如果type是为扩展__device__lambda 创建的闭包类,并且该 lambda 在定义时使用了尾置返回类型,则该函数返回true,否则返回false。如果尾置返回类型的定义引用了任何 lambda 参数名称,则不会保留返回类型。
bool __nv_is_extended_host_device_lambda_closure_type(type);
如果type是为扩展__host__ __device__lambda 创建的闭包类,则该函数返回true,否则返回false。
无论是否启用 lambda 或扩展 lambda,都可以在所有编译模式中使用 lambda 类型特征。如果扩展 lambda 模式未激活,这些特征将始终返回false。
示例:
auto lambda0 = [] __host__ __device__ { };
void host_function() {
auto lambda1 = [] { };
auto lambda2 = [] __device__ { };
auto lambda3 = [] __host__ __device__ { };
auto lambda4 = [] __device__ () -> double { return 3.14; }
auto lambda5 = [] __device__ (int x) -> decltype(&x) { return 0; }
using lambda0_t = decltype(lambda0);
using lambda1_t = decltype(lambda1);
using lambda2_t = decltype(lambda2);
using lambda3_t = decltype(lambda3);
using lambda4_t = decltype(lambda4);
using lambda5_t = decltype(lambda5);
// 'lambda0' is not an extended lambda because it is defined outside function scope
static_assert(!__nv_is_extended_device_lambda_closure_type(lambda0_t));
static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(lambda0_t));
static_assert(!__nv_is_extended_host_device_lambda_closure_type(lambda0_t));
// 'lambda1' is not an extended lambda because it has no execution space annotations
static_assert(!__nv_is_extended_device_lambda_closure_type(lambda1_t));
static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(lambda1_t));
static_assert(!__nv_is_extended_host_device_lambda_closure_type(lambda1_t));
// 'lambda2' is an extended device-only lambda
static_assert(__nv_is_extended_device_lambda_closure_type(lambda2_t));
static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(lambda2_t));
static_assert(!__nv_is_extended_host_device_lambda_closure_type(lambda2_t));
// 'lambda3' is an extended host-device lambda
static_assert(!__nv_is_extended_device_lambda_closure_type(lambda3_t));
static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(lambda3_t));
static_assert(__nv_is_extended_host_device_lambda_closure_type(lambda3_t));
// 'lambda4' is an extended device-only lambda with preserved return type
static_assert(__nv_is_extended_device_lambda_closure_type(lambda4_t));
static_assert(__nv_is_extended_device_lambda_with_preserved_return_type(lambda4_t));
static_assert(!__nv_is_extended_host_device_lambda_closure_type(lambda4_t));
// 'lambda5' is not an extended device-only lambda with preserved return type
// because it references the operator()'s parameter types in the trailing return type.
static_assert(__nv_is_extended_device_lambda_closure_type(lambda5_t));
static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(lambda5_t));
static_assert(!__nv_is_extended_host_device_lambda_closure_type(lambda5_t));
}
5.3.8.4. 扩展 lambda 的限制
在调用主机编译器之前,CUDA 编译器会将扩展 lambda 表达式替换为在命名空间作用域中定义的占位类型的实例。该占位类型的模板实参要求取得包含原始扩展 lambda 表达式的函数的地址。这是正确执行任何模板实参涉及扩展 lambda 闭包类型的__global__函数模板所必需的。外围函数按以下方式确定。
根据定义,扩展 lambda 位于__host__或__host__ __device__函数的直接块或嵌套块的作用域内。
如果该函数不是 lambda 表达式的
operator(),则将其视为该扩展 lambda 的外围函数。否则,该扩展 lambda 定义在一个或多个外围 lambda 表达式的
operator()的直接块或嵌套块的作用域内。如果最外层的 lambda 表达式定义在函数
F的直接块或嵌套块的作用域内,则F就是所确定的外围函数。否则,外围函数不存在。
示例:
void host_function() {
auto lambda1 = [] __device__ { }; // enclosing function for lambda1 is "host_function()"
auto lambda2 = [] {
auto lambda3 = [] {
auto lambda4 = [] __host__ __device__ { }; // enclosing function for lambda4 is "host_function"
};
};
}
auto global_lambda = [] {
auto lambda5 = [] __host__ __device__ { }; // enclosing function for lambda5 does not exist
};
扩展 lambda 的限制
扩展 lambda 不能定义在另一个扩展 lambda 表达式内部。示例:
void host_function() { auto lambda1 = [] __host__ __device__ { // ERROR, extended lambda defined within another extended lambda auto lambda2 = [] __host__ __device__ { }; }; }
扩展 lambda 不能定义在泛型 lambda 表达式内部。示例:
void host_function() { auto lambda1 = [] (auto) { // ERROR, extended lambda defined within a generic lambda auto lambda2 = [] __host__ __device__ { }; }; }
如果扩展 lambda 定义在一个或多个嵌套 lambda 表达式的直接块或嵌套块的作用域内,则最外层的 lambda 表达式必须定义在某个函数的直接块或嵌套块的作用域内。示例:
auto lambda1 = [] { // ERROR, outer enclosing lambda is not defined within a non-lambda-operator() function auto lambda2 = [] __host__ __device__ { }; };
扩展 lambda 的外围函数必须具有名称,并且其地址必须可访问。如果外围函数是类成员,则必须满足以下条件:
包含该成员函数的所有外围类都必须具有名称。
该成员函数在其所属类中的访问权限不得为私有或受保护。
所有外围类在各自的外层类中的访问权限都不得为私有或受保护。
示例:
void host_function() { auto lambda1 = [] __device__ { return 0; }; // OK { auto lambda2 = [] __device__ { return 0; }; // OK auto lambda3 = [] __device__ __host__ { return 0; }; // OK } } struct MyStruct1 { MyStruct1() { auto lambda4 = [] __device__ { return 0; }; // ERROR, address of the enclosing function is not accessible } }; class MyStruct2 { void foo() { auto temp1 = [] __device__ { return 10; }; // ERROR, enclosing function has private access in parent class } struct MyStruct3 { void foo() { auto temp1 = [] __device__ { return 10; }; // ERROR, enclosing class MyStruct3 has private access in its parent class } }; };
在定义扩展 lambda 的位置,必须能够无歧义地取得外围例程的地址。然而,这并不总是可行,例如,当别名声明遮蔽了同名的模板类型实参时。示例:
template <typename T> struct A { using Bar = void; void test(); }; template<> struct A<void> { }; template <typename Bar> void A<Bar>::test() { // In code sent to host compiler, nvcc will inject an address expression here, of the form: // (void (A< Bar> ::*)(void))(&A::test)) // However, the class typedef 'Bar' (to void) shadows the template argument 'Bar', // causing the address expression in A<int>::test to actually refer to: // (void (A< void> ::*)(void))(&A::test)) // which doesn't take the address of the enclosing routine 'A<int>::test' correctly. auto lambda1 = [] __host__ __device__ { return 4; }; } int main() { A<int> var; var.test(); }
扩展 lambda 不能定义在函数的局部类中。示例:
void host_function() { struct MyStruct { void bar() { // ERROR, bar() is member of a class that is local to a function auto lambda2 = [] __host__ __device__ { return 0; }; } }; }
扩展 lambda 的外围函数不能具有推导出的返回类型。示例:
auto host_function() { // ERROR, the return type of host_function() is deduced auto lambda3 = [] __host__ __device__ { return 0; }; }
主机-设备扩展 lambda 不能是泛型 lambda,即具有
auto参数类型的 lambda。示例:void host_function() { // ERROR, __host__ __device__ extended lambdas cannot be a generic lambda auto lambda1 = [] __host__ __device__ (auto i) { return i; }; // ERROR, a host-device extended lambda cannot be a generic lambda auto lambda2 = [] __host__ __device__ (auto... i) { return sizeof...(i); }; }
如果外围函数是函数模板或成员模板的实例化,或者该函数是类模板的成员,则这些模板必须满足以下约束:
模板最多只能具有一个可变参数,并且该参数必须列在模板参数列表的最后。
模板参数必须具有名称。
模板实例化的实参类型不能涉及函数局部的类型(扩展 lambda 的闭包类型除外),也不能涉及作为
private或protected类成员的类型。
示例 1:
template <template <typename...> class T, typename... P1, typename... P2> void bar1(const T<P1...>, const T<P2...>) { // ERROR, enclosing function has multiple parameter packs auto lambda = [] __device__ { return 10; }; } template <template <typename...> class T, typename... P1, typename T2> void bar2(const T<P1...>, T2) { // ERROR, for enclosing function, the parameter pack is not last in the template parameter list auto lambda = [] __device__ { return 10; }; } template <typename T, T> void bar3() { // ERROR, for enclosing function, the second template parameter is not named auto lambda = [] __device__ { return 10; }; }
示例 2:
template <typename T> void bar4() { auto lambda1 = [] __device__ { return 10; }; } class MyStruct { struct MyNestedStruct {}; friend int main(); }; int main() { struct MyLocalStruct {}; // ERROR, enclosing function for device lambda in bar4() is instantiated with a type local to main bar4<MyLocalStruct>(); // ERROR, enclosing function for device lambda in bar4 is instantiated with a type // that is a private member of a class bar4<MyStruct::MyNestedStruct>(); }
使用 Microsoft Visual Studio 主机编译器时,外围函数必须具有外部链接。存在此限制是因为主机编译器不支持将非外部链接函数的地址用作模板实参。CUDA 编译器的转换需要这些地址才能支持扩展 lambda。
使用 Microsoft Visual Studio 主机编译器时,不得在
if constexpr块的主体内定义扩展 lambda。扩展 lambda 对捕获的变量有以下限制:
在发送给主机编译器的代码中,变量可能会先按值传递给一系列辅助函数,然后才用于直接初始化表示该扩展 lambda 闭包类型的类类型的字段。然而,C++ 标准规定,应使用被捕获的变量直接初始化闭包类型的字段。
变量只能按值捕获。
如果数组的维数大于 7,则无法捕获该数组类型的变量。
对于数组类型的变量,在发送给主机编译器的代码中,首先对闭包类型的数组字段进行默认初始化,然后从被捕获数组变量的相应元素向每个数组元素进行复制赋值。因此,在主机代码中,数组元素类型必须既可默认构造,又可复制赋值。
不能捕获作为可变实参包元素的函数参数。
被捕获变量的类型不能是函数局部的类型(扩展 lambda 闭包类型除外),也不能是
private或protected类成员。主机-设备扩展 lambda 不支持初始化捕获。不过,设备扩展 lambda 支持初始化捕获,但初始化器为数组或类型为
std::initializer_list的情况除外。扩展 lambda 的函数调用运算符不是
constexpr。扩展 lambda 的闭包类型不是字面类型。声明扩展 lambda 时不能使用constexpr和consteval说明符。在词法上嵌套于扩展 lambda 内部的
if-constexpr块内,不能隐式捕获变量,除非该变量已在if-constexpr块之外被隐式捕获,或出现在该扩展 lambda 的显式捕获列表中。
示例:
void host_function() { // CORRECT, an init-capture is allowed for an extended device-only lambda auto lambda1 = [x = 1] __device__ () { return x; }; // ERROR, an init-capture is not allowed for an extended host-device lambda auto lambda2 = [x = 1] __host__ __device__ () { return x; }; int a = 1; // ERROR, an extended __device__ lambda cannot capture variables by reference auto lambda3 = [&a] __device__ () { return a; }; // ERROR, by-reference capture is not allowed for an extended device-only lambda auto lambda4 = [&x = a] __device__ () { return x; }; struct MyStruct {}; MyStruct s1; // ERROR, a type local to a function cannot be used in the type of a captured variable auto lambda6 = [s1] __device__ () { }; // ERROR, an init-capture cannot be of type std::initializer_list auto lambda7 = [x = {11}] __device__ () { }; std::initializer_list<int> b = {11,22,33}; // ERROR, an init-capture cannot be of type std::initializer_list auto lambda8 = [x = b] __device__ () { }; int var = 4; auto lambda9 = [=] __device__ { int result = 0; if constexpr(false) { //ERROR, An extended device-only lambda cannot first-capture 'var' in if-constexpr context result += var; } return result; }; auto lambda10 = [var] __device__ { int result = 0; if constexpr(false) { // CORRECT, 'var' already listed in explicit capture list for the extended lambda result += var; } return result; }; auto lambda11 = [=] __device__ { int result = var; if constexpr(false) { // CORRECT, 'var' already implicit captured outside the 'if-constexpr' block result += var; } return result; }; }
解析函数时,CUDA 编译器会为函数中的每个扩展 lambda 分配一个计数器值。该计数器值用于传递给主机编译器的替换后的具名类型中。因此,函数内是否存在扩展 lambda 不应依赖于
__CUDA_ARCH__的特定值,也不应依赖于__CUDA_ARCH__未被定义。示例:template <typename T> __global__ void kernel(T in) { in(); } __host__ __device__ void host_device_function() { // ERROR, the number and relative declaration order of // extended lambdas depend on __CUDA_ARCH__ #if defined(__CUDA_ARCH__) auto lambda1 = [] __device__ { return 0; }; auto lambda2 = [] __host__ __device__ { return 10; }; #endif auto lambda3 = [] __device__ { return 4; }; kernel<<<1, 1>>>(lambda3); }
如上所述,CUDA 编译器会将主机函数中定义的设备扩展 lambda 替换为在命名空间作用域中定义的占位类型。除非特征
__nv_is_extended_device_lambda_with_preserved_return_type()对该扩展 lambda 的闭包类型返回true,否则该占位类型不会定义与原始 lambda 声明等效的operator()函数。因此,在主机代码中尝试确定此类 lambda 的operator()函数的返回类型或参数类型可能无法正确工作,因为主机编译器处理的代码与 CUDA 编译器处理的输入代码在语义上不同。不过,在设备代码中对operator()函数的返回类型或参数类型进行自省是可以的。注意,此限制不适用于特征__nv_is_extended_device_lambda_with_preserved_return_type()返回true的主机或设备扩展 lambda。示例:#include <cuda/std/type_traits> const char& getRef(const char* p) { return *p; } void foo() { auto lambda1 = [] __device__ { return "10"; }; // ERROR, attempt to extract the return type of a device lambda in host code cuda::std::result_of<decltype(lambda1)()>::type xx1 = "abc"; auto lambda2 = [] __host__ __device__ { return "10"; }; // CORRECT, lambda2 represents a host-device extended lambda cuda::std::result_of<decltype(lambda2)()>::type xx2 = "abc"; auto lambda3 = [] __device__ () -> const char* { return "10"; }; // CORRECT, lambda3 represents a device extended lambda with preserved return type cuda::std::result_of<decltype(lambda3)()>::type xx2 = "abc"; static_assert(cuda::std::is_same_v<cuda::std::result_of<decltype(lambda3)()>::type, const char*>); auto lambda4 = [] __device__ (char x) -> decltype(getRef(&x)) { return 0; }; // lambda4's return type is not preserved because it references the operator()'s // parameter types in the trailing return type. static_assert(!__nv_is_extended_device_lambda_with_preserved_return_type(decltype(lambda4))); }
对于仅设备扩展 lambda:
仅在设备代码中支持对
operator()的参数类型进行自省。仅在设备代码中支持对
operator()的返回类型进行自省,除非特征函数__nv_is_extended_device_lambda_with_preserved_return_type()返回true。
如果扩展 lambda 从主机代码传递到设备代码,例如作为
__global__函数的实参传递,那么无论是否定义了__CUDA_ARCH__宏,也无论其值为何,lambda 体中捕获变量的任何表达式都必须保持不变。之所以有此限制,是因为 lambda 的闭包类布局取决于编译器处理 lambda 表达式时遇到被捕获变量的顺序。如果设备编译与主机编译中的闭包类布局不同,程序可能会执行错误。示例:__device__ int result; template <typename T> __global__ void kernel(T in) { result = in(); } void foo(void) { int x1 = 1; // ERROR, "x1" is only captured when __CUDA_ARCH__ is defined. auto lambda1 = [=] __host__ __device__ { #ifdef __CUDA_ARCH__ return x1 + 1; #else return 10; #endif }; kernel<<<1, 1>>>(lambda1); }
如前所述,在发送给主机编译器的代码中,CUDA 编译器会将仅设备扩展 lambda 表达式替换为占位类型实例。该占位类型在主机代码中不定义函数指针转换运算符;不过,在设备代码中会提供该转换运算符。注意,此限制不适用于主机-设备扩展 lambda。示例:
template <typename T> __global__ void kernel(T in) { int (*fp)(double) = in; fp(0); // CORRECT, conversion in device code is supported auto lambda1 = [](double) { return 1; }; } void foo() { auto lambda_device = [] __device__ (double) { return 1; }; auto lambda_host_device = [] __host__ __device__ (double) { return 1; }; kernel<<<1, 1>>>(lambda_device); kernel<<<1, 1>>>(lambda_host_device); // CORRECT, conversion for a __host__ __device__ lambda is supported in host code int (*fp1)(double) = lambda_host_device; // ERROR, conversion for a device lambda is not supported in host code int (*fp2)(double) = lambda_device; }
如前所述,在发送给主机编译器的代码中,CUDA 编译器会将仅设备扩展 lambda 表达式或主机-设备扩展 lambda 表达式替换为占位类型实例。此占位类型可能定义 C++ 特殊成员函数,例如构造函数和析构函数。因此,对于扩展 lambda 的闭包类型,某些标准 C++ 类型特征在 CUDA 前端编译器中可能产生与主机编译器中不同的结果。受影响的类型特征如下::
std::is_trivially_copyable、std::is_trivially_constructible、std::is_trivially_copy_constructible、std::is_trivially_move_constructible、std::is_trivially_destructible。必须注意确保在实例化__global__、__device__、__constant__或__managed__函数模板或变量模板时不使用这些特征的结果。示例:#include <cstdio> #include <type_traits> template <bool b> void __global__ kernel() { printf("hi"); } template <typename T> void kernel_launch() { // ERROR, this kernel launch may fail, because CUDA frontend compiler and host compiler // may disagree on the result of std::is_trivially_copyable_v trait on the // closure type of the extended lambda kernel<std::is_trivially_copyable_v<T>><<<1,1>>>(); cudaDeviceSynchronize(); } int main() { int x = 0; auto lambda1 = [=] __host__ __device__ () { return x; }; kernel_launch<decltype(lambda1)>(); }
CUDA 编译器会针对1-12中所述的部分情况生成编译器诊断信息;对于情况13-17,不会生成诊断信息,但主机编译器可能无法编译所生成的代码。
5.3.8.5. 主机-设备 lambda 优化注意事项
与仅设备 lambda 不同,主机-设备 lambda 可以从主机代码中调用。如前所述,CUDA 编译器会将主机代码中定义的扩展 lambda 表达式替换为具名占位类型的实例。主机-设备扩展 lambda 的占位类型通过间接函数调用来调用原始 lambda 的operator()。如果未启用扩展 lambda 模式,这些特征将始终返回 false。
间接函数调用的存在可能导致主机编译器对主机-设备扩展 lambda 的优化程度低于对隐式或显式仅为__host__的 lambda 的优化程度。在后一种情况下,主机编译器可以轻松地将 lambda 体内联到调用上下文中。不过,当遇到主机-设备扩展 lambda 时,主机编译器可能无法轻松地内联原始 lambda 体。
5.3.8.6. *this按值捕获
根据 C++11/C++14 规则,当在非static类成员函数中定义 lambda,且 lambda 体引用了类成员变量时,必须按值捕获该类的this指针,而不是所引用的成员变量。如果该 lambda 是在主机函数中定义并在 GPU 上执行的仅设备扩展 lambda 或主机-设备扩展 lambda,那么当this指针指向主机内存时,在 GPU 上访问所引用的成员变量会导致运行时错误。
示例:
#include <cstdio>
template <typename T>
__global__ void foo(T in) { printf("value = %d\n", in()); }
struct MyStruct {
int var;
__host__ __device__ MyStruct() : var(10) {};
void run() {
auto lambda1 = [=] __device__ {
// reference to "var" causes the 'this' pointer (MyStruct*) to be captured by value
return var + 1;
};
// Kernel launch fails at run time because 'this->var' is not accessible from the GPU
foo<<<1, 1>>>(lambda1);
cudaDeviceSynchronize();
}
};
int main() {
MyStruct s1;
s1.run();
}
C++17 通过引入新的*this捕获模式解决了此问题。在此模式下,编译器会复制*this所指代的对象,而不是按值捕获this指针。P0018R3中更详细地介绍了*this捕获模式。
使用--extended-lambda标志时,CUDA 编译器支持对__device__和__global__函数中定义的 lambda,以及主机代码中定义的仅设备扩展 lambda 使用*this捕获模式。
下面将上述示例修改为使用*this捕获模式:
#include <cstdio>
template <typename T>
__global__ void foo(T in) { printf("\n value = %d", in()); }
struct MyStruct {
int var;
__host__ __device__ MyStruct() : var(10) { };
void run() {
// note the "*this" capture specification
auto lambda1 = [=, *this] __device__ {
// reference to "var" causes the object denoted by '*this' to be captured by
// value, and the GPU code will access 'copy_of_star_this->var'
return var + 1;
};
// Kernel launch succeeds
foo<<<1, 1>>>(lambda1);
cudaDeviceSynchronize();
}
};
int main() {
MyStruct s1;
s1.run();
}
对于主机代码中定义的未加注解的 lambda 或主机-设备扩展 lambda,不允许使用*this捕获模式,除非所选语言方言启用了*this捕获。下面是受支持和不受支持的用法示例:
struct MyStruct {
int var;
__host__ __device__ MyStruct() : var(10) { };
void host_function() {
// CORRECT, use in an extended device-only lambda
auto lambda1 = [=, *this] __device__ { return var; };
// Use in an extended host-device lambda
// Error if *this capture not enabled by language dialect
auto lambda2 = [=, *this] __host__ __device__ { return var; };
// Use in an non-annotated lambda in host function
// Error if *this capture not enabled by language dialect
auto lambda3 = [=, *this] { return var; };
}
__device__ void device_function() {
// CORRECT, use in a lambda defined in a device-only function
auto lambda1 = [=, *this] __device__ { return var; };
// CORRECT, use in a lambda defined in a device-only function
auto lambda2 = [=, *this] __host__ __device__ { return var; };
// CORRECT, use in a lambda defined in a device-only function
auto lambda3 = [=, *this] { return var; };
}
__host__ __device__ void host_device_function() {
// CORRECT, use in an extended device-only lambda
auto lambda1 = [=, *this] __device__ { return var; };
// Use in an extended host-device lambda
// Error if *this capture not enabled by language dialect
auto lambda2 = [=, *this] __host__ __device__ { return var; };
// Use in an unannotated lambda in a host-device function
// Error if *this capture not enabled by language dialect
auto lambda3 = [=, *this] { return var; };
}
};
5.3.8.7. 实参依赖查找(ADL)
如前所述,CUDA 编译器在调用主机编译器之前,会将扩展 lambda 表达式替换为占位类型。该占位类型的一个模板实参使用包含原始 lambda 表达式的函数的地址。这可能导致额外的命名空间参与任何实参类型涉及该扩展 lambda 表达式闭包类型的主机函数调用的实参依赖查找(ADL)。因此,主机编译器可能选中错误的函数。
示例:
namespace N1 {
struct MyStruct {};
template <typename T>
void my_function(T);
}; // namespace N1
namespace N2 {
template <typename T>
int my_function(T);
template <typename T>
void run(T in) { my_function(in); }
} // namespace N2
void bar(N1::MyStruct in) {
// For extended device-only lambda, the code sent to the host compiler is replaced with
// the placeholder type instantiation expression
// ' __nv_dl_wrapper_t< __nv_dl_tag<void (*)(N1::MyStruct in),(&bar),1> > { }'
//
// As a result, the namespace 'N1' participates in ADL lookup of the
// call to "my_function()" in the body of N2::run, causing ambiguity.
auto lambda1 = [=] __device__ { };
N2::run(lambda1);
}
在上述示例中,CUDA 编译器将扩展 lambda 替换为涉及N1命名空间的占位类型。因此,N1命名空间参与了N2::run()体中针对my_function(in)的 ADL 查找,由于发现了多个重载候选项,即N1::my_function和N2::my_function,导致主机编译失败。
5.3.9. 多态函数包装器
nvfunctional头文件提供了一个多态函数包装器类模板nvstd::function。此类模板的实例可以存储、复制和调用任何可调用目标,例如 lambda 表达式。nvstd::function可以在主机代码和设备代码中使用。
示例:
#include <nvfunctional>
__host__ int host_function() { return 1; }
__device__ int device_function() { return 2; }
__host__ __device__ int host_device_function() { return 3; }
__global__ void kernel(int* result) {
nvstd::function<int()> fn1 = device_function;
nvstd::function<int()> fn2 = host_device_function;
nvstd::function<int()> fn3 = [](){ return 10; };
*result = fn1() + fn2() + fn3();
}
__host__ __device__ void host_device_test(int* result) {
nvstd::function<int()> fn1 = host_device_function;
nvstd::function<int()> fn2 = [](){ return 10; };
*result = fn1() + fn2();
}
__host__ void host_test(int* result) {
nvstd::function<int()> fn1 = host_function;
nvstd::function<int()> fn2 = host_device_function;
nvstd::function<int()> fn3 = [](){ return 10; };
*result = fn1() + fn2() + fn3();
}
无效情况:
主机代码中的
nvstd::function实例不能使用__device__函数的地址进行初始化,也不能使用其operator()为__device__函数的仿函数进行初始化。同样,设备代码中的
nvstd::function实例不能使用__host__函数的地址进行初始化,也不能使用其operator()为__host__函数的仿函数进行初始化。在运行时,不能将
nvstd::function实例从主机代码传递到设备代码(反之亦然)。如果
__global__函数从主机代码启动,则不能在__global__函数的参数类型中使用nvstd::function。
无效情况示例:
#include <nvfunctional>
__device__ int device_function() { return 1; }
__host__ int host_function() { return 3; }
auto lambda_host = [] { return 0; };
__global__ void k() {
nvstd::function<int()> fn1 = host_function; // ERROR, initialized with address of __host__ function
nvstd::function<int()> fn2 = lambda_host; // ERROR, initialized with address of functor with
// __host__ operator() function
}
__global__ void kernel(nvstd::function<int()> f1) {}
void foo(void) {
auto lambda_device = [=] __device__ { return 1; };
nvstd::function<int()> fn1 = device_function; // ERROR, initialized with address of __device__ function
nvstd::function<int()> fn2 = lambda_device; // ERROR, initialized with address of functor with
// __device__ operator() function
kernel<<<1, 1>>>(fn2); // ERROR, passing nvstd::function from host to device
}
nvstd::function在nvfunctional头文件中的定义如下:
namespace nvstd {
template <typename RetType, typename ...ArgTypes>
class function<RetType(ArgTypes...)> {
public:
// constructors
__device__ __host__ function() noexcept;
__device__ __host__ function(nullptr_t) noexcept;
__device__ __host__ function(const function&);
__device__ __host__ function(function&&);
template<typename F>
__device__ __host__ function(F);
// destructor
__device__ __host__ ~function();
// assignment operators
__device__ __host__ function& operator=(const function&);
__device__ __host__ function& operator=(function&&);
__device__ __host__ function& operator=(nullptr_t);
template<typename F>
__device__ __host__ function& operator=(F&&);
// swap
__device__ __host__ void swap(function&) noexcept;
// function capacity
__device__ __host__ explicit operator bool() const noexcept;
// function invocation
__device__ RetType operator()(ArgTypes...) const;
};
// null pointer comparisons
template <typename R, typename... ArgTypes>
__device__ __host__
bool operator==(const function<R(ArgTypes...)>&, nullptr_t) noexcept;
template <typename R, typename... ArgTypes>
__device__ __host__
bool operator==(nullptr_t, const function<R(ArgTypes...)>&) noexcept;
template <typename R, typename... ArgTypes>
__device__ __host__
bool operator!=(const function<R(ArgTypes...)>&, nullptr_t) noexcept;
template <typename R, typename... ArgTypes>
__device__ __host__
bool operator!=(nullptr_t, const function<R(ArgTypes...)>&) noexcept;
// specialized algorithms
template <typename R, typename... ArgTypes>
__device__ __host__
void swap(function<R(ArgTypes...)>&, function<R(ArgTypes...)>&);
} // namespace nvstd
5.3.10. C/C++ 语言限制
5.3.10.1. 不支持的特性
设备代码中不支持运行时类型信息(RTTI)和异常:
typeid关键字dynamic_cast关键字try/catch/throw关键字
设备代码中不支持
long double。所有平台均不支持三字符组。Windows 上不支持双字符组。
用户定义的
operator new、operator new[]、operator delete或operator delete[]不能用于替换编译器提供的相应内建实现,无论在主机上还是设备上,这都被视为未定义行为。
5.3.10.2. 命名空间保留
除非另有说明,否则向顶层命名空间cuda::、nv::或cooperative_groups::,或向这些命名空间内的任何嵌套命名空间添加定义,均属于未定义行为。我们允许将cuda::用作子命名空间,如下所示:
示例:
namespace cuda { // same for "nv" and "cooperative_groups" namespaces
struct foo; // ERROR, class declaration in the "cuda" namespace
void bar(); // ERROR, function declaration in the "cuda" namespace
namespace utils {} // ERROR, namespace declaration in the "cuda" namespace
} // namespace cuda
namespace utils {
namespace cuda {
// CORRECT, namespace "cuda" may be used nested within a non-reserved namespace
void bar();
} // namespace cuda
} // namespace utils
// ERROR, Equivalent to adding symbols to namespace "cuda" at global scope
using namespace utils;
5.3.10.3. 指针和内存地址
仅允许在相关内存所在的同一执行空间中对指针进行解引用(*pointer、pointer->member、pointer[0])。以下情况会导致未定义行为,最常见的情况是段错误和应用程序终止。
以下限制适用于函数:
不允许在主机代码中获取
__device__函数的地址。在主机代码中获取的
__global__函数的地址不能在设备代码中使用。同样,在设备代码中获取的__global__函数的地址不能在主机代码中使用。
按照内存空间说明符一节所述,通过cudaGetSymbolAddress()获取的__device__或__constant__变量的地址只能在主机代码中使用。
5.3.10.4. 变量
5.3.10.4.1. 局部变量
在主机上执行的函数内,不允许在非extern变量声明上使用__device__、__tile__、__shared__、__managed__和__constant__内存空间说明符。
示例:
__host__ void host_function() {
int x; // CORRECT, __host__ variable
__device__ int y; // ERROR, __device__ variable declaration within a host function
__tile__ int z; // ERROR, __tile__ variable declaration within a host function
__shared__ int w; // ERROR, __shared__ variable declaration within a host function
__managed__ int h; // ERROR, __managed__ variable declaration within a host function
__constant__ int i; // ERROR, __constant__ variable declaration within a host function
extern __device__ int j; // CORRECT, extern __device__ variable
}
在设备上执行的函数内,不允许在既非extern也非static的变量声明上使用__device__、__tile__、__constant__和__managed__内存空间说明符。
__device__ void device_function() {
int x; // CORRECT, __device__ variable
__constant__ int y; // ERROR, __constant__ variable declaration within a device function
__managed__ int z; // ERROR, __managed__ variable declaration within a device function
extern __device__ int k; // CORRECT, extern __device__ variable
}
另请参阅静态变量一节。
5.3.10.4.2. const限定的变量
在全局作用域、命名空间作用域或类作用域中声明的、不带内存空间注解(__device__、__tile__或__constant__)的const限定变量被视为主机变量。设备代码不能包含对该变量的引用,也不能获取其地址。
如果满足以下条件,该变量可以直接在设备代码中使用:
它在使用点之前已使用常量表达式进行初始化,
其类型不带
volatile限定,并且它具有以下类型之一:
内建整型,或
内建浮点类型,但主机编译器为 Microsoft Visual Studio 时除外。
从 C++14 开始,建议使用constexpr或inline constexpr(C++17)变量,而不是const限定的变量。constexpr变量不受相同的类型限制,可以直接在设备代码中使用。
__managed__变量不支持const限定的类型。
示例:
const int ConstVar = 10;
const float ConstFloatVar = 5.0f;
inline constexpr float ConstexprFloatVar = 5.0f; // C++17
struct MyStruct {
static const int ConstVar = 20;
// static const float ConstFloatVar = 5.0f; // ERROR, static const variables cannot be float
static inline constexpr float ConstexprFloatVar = 5.0f; // CORRECT
};
extern const int ExternVar;
__device__ void foo() {
int array1[ConstVar]; // CORRECT
int array2[MyStruct::ConstVar]; // CORRECT
const float var1 = ConstFloatVar; // CORRECT, except when the host compiler is Microsoft Visual Studio.
constexpr float var2 = ConstexprFloatVar; // CORRECT
// int var3 = ExternVar; // ERROR, "ExternVar" is not initialized with a constant expression
// int& var4 = ConstVar; // ERROR, reference to host variable
// int* var5 = &ConstVar; // ERROR, address of host variable
}
请参阅Compiler Explorer上的示例。
5.3.10.4.3. volatile限定的变量
注意
支持volatile关键字是为了保持与 ISO C++ 的兼容性。不过,它的其余未弃用用法即便有适用于 GPU 的,也为数甚少。
对volatile限定的对象进行读写不是原子操作,并且会被编译为一条或多条volatile 指令,这些指令不保证:
内存操作的顺序,或
硬件执行的内存操作次数与 PTX 指令的数量一致。
在分块代码中,volatile关键字对内存访问的行为没有影响。
CUDA C++ 的volatile不适用于:
线程间同步:请改用通过cuda::atomic_ref、cuda::atomic或原子函数实现的原子操作。
原子内存操作提供线程间同步保证,并且性能优于
volatile操作。不过,CUDA C++ 的volatile操作不提供任何线程间同步保证,因此不适合此用途。以下示例展示了如何使用原子操作在两个线程之间传递消息。#include <cuda/atomic> __global__ void kernel(int* flag, int* data) { cuda::atomic_ref<int, cuda::thread_scope_device> atomic_ref{*flag}; if (threadIdx.x == 0) { // Consumer: blocks until flag is set by producer, then reads data while(atomic_ref.load(cuda::memory_order_acquire) == 0) ; if (*data != 42) __trap(); // Errors if wrong data read } else if (threadIdx.x == 1) { // Producer: writes data then sets flag *data = 42; atomic_ref.store(1, cuda::memory_order_release); } }
#include <cuda/atomic> __global__ void kernel(cuda::atomic<int, cuda::thread_scope_device>* flag, int* data) { if (threadIdx.x == 0) { // Consumer: blocks until flag is set by producer, then reads data while(flag->load(cuda::memory_order_acquire) == 0) ; if (*data != 42) __trap(); // Errors if wrong data read } else if (threadIdx.x == 1) { // Producer: writes data then sets flag *data = 42; flag->store(1, cuda::memory_order_release); } }
__global__ void kernel(int* flag, int* data) { if (threadIdx.x == 0) { // Consumer: blocks until flag is set by producer, then reads data while(atomicAdd(flag, 0) == 0) ; // Load with Relaxed Read-Modify-Write __threadfence(); // SequentiallyConsistent fence if (*data != 42) __trap(); // Errors if wrong data read } else if (threadIdx.x == 1) { // Producer: writes data then sets flag *data = 42; __threadfence(); // SequentiallyConsistent fence atomicExch(flag, 1); // Store with Relaxed Read-Modify-Write } }
内存映射 IO(MMIO):请改为通过内联 PTX 使用PTX MMIO 操作。
PTX MMIO 操作严格保持所执行的内存访问次数。不过,CUDA C++ 的
volatile操作不保持所执行的内存访问次数,可能以不确定的方式执行比请求次数更多或更少的访问。这使其不适用于 MMIO。以下示例展示了如何使用 PTX MMIO 操作来读取和写入寄存器。__global__ void kernel(int* mmio_reg0, int* mmio_reg1) { // Write to MMIO register: int value = 13; asm volatile("st.relaxed.mmio.sys.u32 [%0], %1;" : : "l"(mmio_reg0), "r"(value) : "memory"); // Read MMIO register: asm volatile("ld.relaxed.mmio.sys.u32 %0, [%1];" : "=r"(value) : "l"(mmio_reg1) : "memory"); if (value != 42) __trap(); // Errors if wrong data read }
5.3.10.4.4. static变量
设备函数中允许使用static局部变量。
包含静态变量的函数在每个执行空间中使用不同的静态变量。例如,
__host__ __device__函数具有一个用于主机执行的静态变量副本,以及一个用于设备执行的副本。__tile__ __device__函数具有一个用于分块执行的副本,以及一个用于 SIMT 执行的副本。
如果函数具有__host__执行空间说明符,则仅在定义了__CUDA_ARCH__时,才允许使用具有显式内存空间(例如static __device__/__tile__/__constant__/__shared__/__managed__)的static变量。
下面展示了函数作用域static变量的合法和非法用法示例。
struct TrivialStruct {
int x;
};
struct NonTrivialStruct {
__device__ NonTrivialStruct(int x) {}
};
__device__ void device_function(int x) {
static int v1; // CORRECT, implicit __device__ memory space specifier
static int v2 = 11; // CORRECT, implicit __device__ memory space specifier
// static int v3 = x; // ERROR, dynamic initialization is not allowed
static __managed__ int v4; // CORRECT, explicit
static __device__ int v5; // CORRECT, explicit
static __constant__ int v6; // CORRECT, explicit
static __shared__ int v7; // CORRECT, explicit
static TrivialStruct s1; // CORRECT, implicit __device__ memory space specifier
static TrivialStruct s2{22}; // CORRECT, implicit __device__ memory space specifier
// static TrivialStruct s3{x}; // ERROR, dynamic initialization is not allowed
// static NonTrivialStruct s4{3}; // ERROR, dynamic initialization is not allowed
}
请参阅Compiler Explorer上的示例。
__host__ __device__ void host_device_function() {
static int v1; // CORRECT, implicit __device__ memory space specifier
// static __device__ int v2; // ERROR, __device__-only variable inside a host-device function
#ifdef __CUDA_ARCH__
static __device__ int v3; // CORRECT, declaration is only visible during device compilation
#else
static int v4; // CORRECT, declaration is only visible during host compilation
#endif
}
请参阅Compiler Explorer上的示例。
#include <cassert>
__host__ __device__ int host_device_function() {
static int v = 0;
v++;
return v;
}
__global__ void kernel() {
int ret = host_device_function(); // v = 1
assert(ret == 4); // FAIL
}
int main() {
host_device_function(); // v = 1
host_device_function(); // v = 2
int ret = host_device_function(); // v = 3
assert(ret == 3); // OK
kernel<<<1, 1>>>();
cudaDeviceSynchronize();
}
请参阅Compiler Explorer上的示例。
5.3.10.4.5. extern变量
在全程序编译模式下编译时,不能使用extern关键字将__device__、__tile__、__shared__、__managed__和__constant__变量定义为具有外部链接的变量。在分离编译模式下,此限制也适用于__tile__变量。
唯一的例外是共享内存的动态分配一节所述的动态分配的__shared__变量。
__device__ int x; // OK
extern __device__ int y; // ERROR in whole program compilation mode
extern __shared__ int z; // OK
5.3.10.5. 函数
5.3.10.5.1. 递归
__global__、__tile_global__和__tile__函数不支持递归,而__device__和__host__ __device__函数没有此限制。
5.3.10.5.2. 外部链接
具有外部链接的设备变量或函数在跨多个翻译单元使用时需要分离编译模式。
在分离编译模式下,如果要求某个特定翻译单元中必须存在__device__或__global__函数的定义,则该函数的参数类型和返回类型在该翻译单元中必须是完整类型。此概念也称为单一定义规则使用,或 ODR 使用。
示例:
//first.cu:
struct S; // forward declaration
__device__ void foo(S); // ERROR, type 'S' is an incomplete type
__device__ auto* ptr = foo; // ODR-use, address taken
int main() {}
//second.cu:
struct S {}; // struct definition
__device__ void foo(S) {} // function definition
# compiler invocation
$ nvcc -std=c++14 -rdc=true first.cu second.cu -o prog
nvlink error : Prototype doesn't match for '_Z3foo1S' in '/tmp/tmpxft_00005c8c_00000000-18_second.o',
first defined in '/tmp/tmpxft_00005c8c_00000000-18_second.o'
nvlink fatal : merge_elf failed
5.3.10.5.3. 形式参数
不允许在形式参数上使用__device__、__tile__、__shared__、__managed__和__constant__内存空间说明符。
void device_function1(__device__ int x) { } // ERROR, __device__ parameter
void device_function2(__shared__ int x) { } // ERROR, __shared__ parameter
5.3.10.5.4. __global__函数参数
__global__或__tile_global__函数具有以下限制:
它不能具有可变数量的实参,即不能使用 C 省略号语法
...和va_list类型。允许使用 C++11 可变参数模板,但须遵守__global__ 可变参数模板一节中所述的限制。函数参数通过常量内存传递到设备,其总大小限制为 32,764 字节。
函数参数不能为
std::initializer_list类型。多态类参数(
virtual)被视为未定义行为。允许使用 lambda 表达式和闭包类型,但须遵守lambda 表达式与 __global__ 函数参数一节中所述的限制。
对于
__tile_global__函数,函数参数不能是按值传递的类、结构体或联合体。
5.3.10.5.5. __global__函数实参传递
从设备代码启动__global__函数时,每个实参都必须是可平凡复制且可平凡析构的。
从主机代码启动__global__函数时,各实参类型可以是非平凡可复制的,也可以是非平凡可析构的。不过,这些类型的处理并不遵循标准 C++ 模型,如下所述。用户代码必须确保此工作流程不会影响程序的正确性。此工作流程在两个方面偏离了标准 C++:
使用原始内存复制,而非调用复制构造函数
CUDA Runtime 通过复制原始内存内容将核函数实参传递给
__global__函数,最终使用memcpy完成复制。如果某个实参是非平凡可复制的,并提供了用户定义的复制构造函数,则在从主机到设备的复制过程中,会跳过该构造函数调用的操作和副作用。示例:
#include <cassert> struct MyStruct { int value = 1; int* ptr; MyStruct() = default; __host__ __device__ MyStruct(const MyStruct&) { ptr = &value; } }; __global__ void device_function(MyStruct my_struct) { // this assert fails because "my_struct" is obtained by copying // the raw memory content and the copy constructor is skipped. assert(my_struct.ptr == &my_struct.value); // FAIL } void host_function(MyStruct my_struct) { assert(my_struct.ptr == &my_struct.value); // CORRECT } int main() { MyStruct my_struct; host_function(my_struct); device_function<<<1, 1>>>(my_struct); // copy constructor invoked in the host-side only cudaDeviceSynchronize(); }
请参阅Compiler Explorer上的示例。
析构函数的调用可能早于
__global__函数的执行结束核函数启动相对于主机执行是异步的。因此,如果
__global__函数的某个实参具有非平凡析构函数,那么该析构函数甚至可能在__global__函数执行结束之前就在主机代码中执行。这可能导致析构函数具有副作用的程序出错。示例:
#include <cassert> __managed__ int var = 0; struct MyStruct { __host__ __device__ ~MyStruct() { var = 3; } }; __global__ void device_function(MyStruct my_struct) { assert(var == 0); // FAIL, MyStruct::~MyStruct() sets the value to 3 } int main() { MyStruct my_struct; // GPU kernel execution is asynchronous with host execution. // As a result, MyStruct::~MyStruct() could be executed before // the kernel finishes executing. device_function<<<1, 1>>>(my_struct); cudaDeviceSynchronize(); }
请参阅Compiler Explorer上的示例。
5.3.10.6. 类
5.3.10.6.1. 类类型变量
使用__device__、__tile__、__constant__、__managed__或__shared__内存空间的变量定义,不能具有包含非空构造函数或非空析构函数的类类型。如果某个类类型的构造函数是平凡的,或者在翻译单元中的某一点满足以下所有条件,则将其视为空构造函数:
该构造函数已经定义。
该构造函数没有形参,其初始化列表为空,函数体为空复合语句。
其类没有
virtual函数、virtual基类或非static数据成员初始化器。其所有基类的默认构造函数均可视为空构造函数。
对于该类中所有具有类类型(或该类类型的数组类型)的非
static数据成员,其默认构造函数均可视为空构造函数。
如果某个类的析构函数是平凡的,或者在翻译单元中的某一点满足以下所有条件,则将其视为空析构函数:
该析构函数已经定义。
该析构函数的函数体为空复合语句。
其类没有
virtual函数或virtual基类。其所有基类的析构函数均可视为空析构函数。
对于该类中所有具有类类型(或该类类型的数组类型)的非
static数据成员,其析构函数均可视为空析构函数。
5.3.10.6.2. 数据成员
不允许在class、struct和union数据成员上使用__device__、__tile__、__shared__、__managed__和__constant__内存空间说明符。
仅支持在编译时求值的static数据成员,例如const 限定的变量和constexpr变量。
struct MyStruct {
static inline constexpr int value1 = 10; // C++17
static constexpr int value2 = 10; // C++11
static const int value3 = 10;
// static int value4; // ERROR
};
5.3.10.6.3. 函数成员
__global__和__tile_global__函数不能是struct、class或union的成员。
允许在friend声明中声明__global__或__tile_global__函数,但不能在该声明中定义该函数。
示例:
struct MyStruct {
friend __global__ void f(); // CORRECT, friend declaration only
// friend __global__ void g() {} // ERROR, friend definition
};
请参阅Compiler Explorer上的示例。
5.3.10.6.4. 隐式声明的函数和非虚的显式预置函数
隐式声明的特殊成员函数是指用户未声明时由编译器为类声明的函数;显式预置函数是指用户声明但使用= default标记的函数。隐式声明或显式预置的特殊成员函数包括默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符和析构函数。
设F表示一个非virtual函数,该函数是隐式声明的,或在首次声明时被显式预置。F的执行空间说明符是所有调用它的函数的执行空间说明符的并集。注意,在此分析中,__global__调用方将被视为__device__调用方。例如:
class Base {
int x;
public:
__host__ __device__ Base() : x(10) {}
};
class Derived : public Base {
int y;
};
class Other: public Base {
int z;
};
__device__ void foo() {
Derived D1;
Other D2;
}
__host__ void bar() {
Other D3;
}
在此情况下,隐式声明的构造函数Derived::Derived()将被视为__device__函数,因为它仅由__device__函数foo()调用。隐式声明的构造函数Other::Other()将被视为__host__ __device__函数,因为它既由__device__函数foo()调用,也由__host__函数bar()调用。
此外,如果F是隐式声明的virtual函数(例如virtual析构函数),则对于每个被F重写的虚函数D,如果D不是隐式声明的,就将该虚函数的执行空间添加到F的执行空间集合中。
例如:
struct Base1 {
virtual __host__ __device__ ~Base1() {}
};
struct Derived1 : Base1 {}; // implicitly-declared virtual destructor
// ~Derived1() has __host__ __device__ execution space specifiers
struct Base2 {
virtual __device__ ~Base2() = default;
};
struct Derived2 : Base2 {}; // implicitly-declared virtual destructor
// ~Derived2() has __device__ execution space specifiers
5.3.10.6.5. 多态类
多态类,即具有virtual函数、派生自其他多态类或具有多态数据成员的类,受到以下限制:
将多态对象从设备复制到主机,或从主机复制到设备,包括复制
__global__函数的实参,属于未定义行为。重写的
virtual函数的执行空间必须与基类中该函数的执行空间匹配。
示例:
struct MyClass {
virtual __host__ __device__ void f() {}
};
__global__ void kernel(MyClass my_class) {
my_class.f(); // undefined behavior
}
int main() {
MyClass my_class;
kernel<<<1, 1>>>(my_class);
cudaDeviceSynchronize();
}
请参阅Compiler Explorer上的示例。
struct BaseClass {
virtual __host__ __device__ void f() {}
};
struct DerivedClass : BaseClass {
__device__ void f() override {} // ERROR
};
请参阅Compiler Explorer上的示例。
5.3.10.6.6. Windows 特有的类布局
CUDA 编译器在类布局方面遵循 IA64 ABI,而 Microsoft Visual Studio 则不遵循。这使得无法在主机代码与设备代码之间对特殊对象进行按位复制,如下所述。
设T表示成员指针类型,或满足以下任一条件的类类型:
使用 Microsoft Visual Studio 编译时,类型为T的类、具有T类型基类的类,或具有T类型数据成员的类,在主机与设备上可能具有不同的类布局和大小。
将此类对象从设备复制到主机,或从主机复制到设备,包括复制__global__函数的实参,属于未定义行为。
5.3.10.7. 模板
如果某个类型满足以下任一条件,则不能将其用作__global__函数或__device__/__constant__变量(C++14)的模板实参:
该类型定义在
__host__或__host__ __device__函数作用域内。该类型没有名称,例如匿名结构体或 lambda 表达式,除非该类型是
__device__或__global__函数中的局部类型。该类型是带有
private或protected的类成员,除非该类是__device__或__global__函数中的局部类。该类型由上述任意类型复合而成。
示例:
template <typename T>
__global__ void kernel() {}
template <typename T>
__device__ int device_var; // C++14
struct {
int v;
} unnamed_struct;
void host_function() {
struct LocalStruct {};
// kernel<LocalStruct><<<1, 1>>>(); // ERROR, LocalStruct is defined within a host function
int data = 4;
// cudaMemcpyToSymbol(device_var<LocalStruct>, &data, sizeof(data)); // ERROR, same as above
auto lambda = [](){};
// kernel<decltype(lambda)><<<1, 1>>>(); // ERROR, unnamed type
// kernel<decltype(unnamed_struct)><<<1, 1>>>(); // ERROR, unnamed type
}
class MyClass {
private:
struct PrivateStruct {};
public:
static void launch() {
// kernel<PrivateStruct><<<1, 1>>>(); // ERROR, private type
}
};
请参阅Compiler Explorer上的示例。
5.3.10.8. 分块代码中的限制
带有__tile__或__tile_global__注解的函数还受到以下额外限制:
分块代码不支持以下语言构造:
do、while或for循环内,或 switch 语句内的 return 语句。switch 语句内的 continue 语句,且其中的
continue不包含在该 switch 下的循环中。switch 主体内嵌套的 if 块或循环块中的 case/default 标签。
虚函数调用。
goto 语句。
产生函数指针、函数引用、成员变量指针或成员函数指针的表达式。
通过函数指针和成员函数指针进行的调用。
通过成员变量指针进行的访问。
128 位整数类型或 128 位浮点类型。
包含位域的类型。
大小超过 16 MB 的类型。
具有虚基类或虚函数的类型。
使用非定位形式的
new或delete运算符进行动态内存分配或释放。
__tile__或__tile_global__函数必须在声明它的同一翻译单元中具有函数体。不支持使用
__tile__为虚函数添加注解。__tile_global__或__tile__函数不能使用 C 省略号语法...来接受数量可变的实参。__tile_global__或__tile__函数不能直接或间接递归。对于
__tile_global__函数,函数形参不能是按值传递的类、结构体或联合体。分块代码不能执行设备端核函数启动,也不能从设备端核函数调用中启动分块核函数。
分块代码不支持直接访问
__half、__nv_bfloat16及相关扩展浮点类型的__x成员变量。
5.3.11. C++11 限制
5.3.11.1. inline命名空间
如果在外围命名空间中定义了具有相同名称和类型签名的另一个实体,则不允许在inline命名空间内定义以下任一实体:
__global__或__tile_global__函数。__device__、__tile__、__constant__、__managed__、__shared__变量。表面或纹理类型的变量,例如
cudaSurfaceObject_t或cudaTextureObject_t。
示例:
__device__ int my_var; // global scope
inline namespace NS {
__device__ int my_var; // namespace scope
} // namespace NS
5.3.11.2. inline无名命名空间
以下实体不能在inline无名命名空间内的命名空间作用域中声明:
__global__或__tile_global__函数。__device__、__tile__、__constant__、__managed__、__shared__变量。表面或纹理类型的变量,例如
cudaSurfaceObject_t或cudaTextureObject_t。
5.3.11.3. constexpr函数
__global__函数不能声明为constexpr。默认情况下,与标准函数一样,不能从执行空间不兼容的函数中调用constexpr函数。在本节列出的示例代码中,UB表示“未定义行为”。
在主机编译阶段(未定义
__CUDA_ARCH__宏时),从主机函数中调用未带有显式或隐式__host__注解的constexpr函数,属于未定义行为。示例:constexpr __device__ int device_func() { return 0; } constexpr __tile__ int tile_func() { return 0; } constexpr __device__ __host___ int host_device_func() { return 0; } int main() { constexpr int x1 = device_func(); // UB: calling a __device__-only constexpr function from host code constexpr int x2 = tile_func(); // UB: calling a __tile__-only constexpr function from host code constexpr int x3 = host_device_func(); // OK }
在设备编译阶段(已定义
__CUDA_ARCH__宏时),从__device__或__global__函数中调用未带有显式或隐式__device__注解的constexpr函数,属于未定义行为。示例:constexpr int host_func() { return 0; } __device__ void dmain() { int x = host_func(); // UB: calling a host-only constexpr function from device code }
在设备编译阶段(已定义
__CUDA_ARCH__宏时),从__tile__或__tile_global__函数中调用未带有显式或隐式__tile__注解的constexpr函数,属于未定义行为。示例:constexpr int host_func() { return 0; } __tile__ void dmain() { int x = host_func(); // UB: calling a host-only constexpr function from tile code }
注意,即使相应的模板函数标有关键字constexpr,函数模板特化也可能不是constexpr函数。
宽松的 constexpr 函数支持
可以使用实验性的nvcc标志--expt-relaxed-constexpr来放宽此约束,具体如下所述。nvcc还会定义宏__CUDACC_RELAXED_CONSTEXPR__。在本节列出的示例代码中,UB表示“未定义行为”。
指定--expt-relaxed-constexpr标志后,编译器将支持跨执行空间调用,具体如下:
如果对
constexpr函数的跨执行空间调用发生在需要常量求值的上下文中,例如 constexpr 变量的初始化器中,则支持此调用。示例:constexpr __host__ int host_func(int x) { return x + 1; }; __global__ void doit() { constexpr int val = host_func(1); // OK: call is in a context that // requires constant evaluation. } __tile_global__ void tile_doit() { constexpr int val = host_func(1); // OK: call is in a context that // requires constant evaluation. } constexpr __device__ int device_func(int x) { return x + 1; } constexpr __tile__ int tile_func(int x) { return x + 2; } int main() { constexpr int val = device_func(1) + tile_func(1); // OK: call is in a context that // requires constant evaluation. }
否则:
在语言规则要求进行常量折叠的上下文之外,不支持从分块代码跨执行空间调用未带有显式或隐式
__tile__注解的constexpr函数。示例:constexpr __host__ int host_func(int x) { return x + 1; } __tile__ int doit(int in) { in = host_func(in); // UB: call occurs outside of a context that requires // constant evaluation. constexpr int other = host_func(10); // OK with -expt-relaxed-constexpr: // call is required to be evaluated at compile time }
在生成 SIMT 设备代码期间,会为仅限主机的
constexpr函数host_func的函数体生成设备代码,除非未使用host_func,或者仅在常量求值上下文中调用它。示例:// NOTE: "host_func" is emitted in generated device code because it is // called from device code in a non-constexpr context constexpr __host__ int host_func(int x) { return x + 1; } __device__ int doit(int in) { in = host_func(in); // OK, even though argument is not a constant expression return in; }
适用于
__device__函数的所有代码限制,也适用于从 SIMT 设备代码调用的仅限主机的constexpr函数H。不过,编译器可能不会针对H的这些限制发出任何构建时诊断信息。原因在于,诊断信息通常在解析期间生成,但在翻译单元后续位置遇到设备代码对H的调用之前,H可能已经完成了解析。例如,
H的函数体中不支持以下代码模式(与任何__device__函数一样),但可能不会生成编译器诊断信息:ODR 使用主机变量或仅限主机的非
constexpr函数。示例:int host_var1, host_var2; constexpr __host__ int* host_func(bool b) { return b ? &host_var1 : &host_var2; }; __device__ int doit(bool flag) { int *ptr; ptr = host_func(flag); // UB: host_func() attempts to refer to host variables 'host_var1' and 'host_var2'. // code will compile, but will NOT execute correctly. return *ptr; }
使用异常(
throw/catch)和 RTTI(typeid, dynamic_cast)。示例:struct Base { }; struct Derived : public Base { }; // NOTE: "host_func" is emitted in generated device code constexpr int host_func(bool b, Base *ptr) { if (b) { return 1; } else if (typeid(ptr) == typeid(Derived)) { // UB: use of typeid in code executing on the GPU return 2; } else { throw int{4}; // UB: use of throw in code executing on the GPU } } __device__ void doit(bool flag) { int val; Derived d; val = host_func(flag, &d); //UB: host_func() attempts use typeid and throw(), which are not allowed in code that executes on the GPU }
在生成主机代码期间,
constexpr非主机函数F的函数体会保留在发送给主机编译器的代码中。如果F的函数体尝试 ODR 使用命名空间作用域的设备变量或__tile__变量,或者非主机的非constexpr函数,则不支持从主机代码调用F(代码可能在没有编译器诊断信息的情况下完成构建,但在运行时可能出现错误行为)。示例:__device__ int device_var1, device_var2; constexpr __device__ int* device_func(bool b) { return b ? &device_var1 : &device_var2; }; __tile__ int tile_var1, tile_var2; constexpr __tile__ int* tile_func(bool b) { return b ? &tile_var1 : &tile_var2; }; int doit1(bool flag) { int *ptr; ptr = device_func(flag); // UB: device_func() attempts to refer to device variables 'device_var1' and 'device_var2' // code will compile, but will NOT execute correctly. return *ptr; } int doit2(bool flag) { int *ptr; ptr = tile_func(flag); // UB: tile_func() attempts to refer to __tile__ variables 'tile_var1' and 'tile_var2' // code will compile, but will NOT execute correctly. return *ptr; }
警告
由于存在上述限制,且编译器不会对不正确的用法给出诊断信息,因此建议避免从设备代码调用标准 C++ 头文件std::中的函数。这类函数的实现因主机平台而异。相反,强烈建议调用 CUDA C++ 标准库libcu++中cuda::std::命名空间内的等效功能。
5.3.11.4. constexpr变量
默认情况下,与标准变量一样,constexpr变量不能在执行空间不兼容的函数中使用。
在以下情况下,constexpr变量可以直接用于设备代码:
C++ 标量类型,不包括指针类型和成员指针类型:
nullptr_t.bool.整型:
char、signed char、unsigned、long long等。浮点类型:
float、double。枚举项:
enum和enum class。
类类型:具有
constexpr构造函数的class、struct和union。上述类型的原生数组,例如
int[],但仅限于在执行空间为__device__或__host__ __device__的constexpr函数内使用时。
不允许使用constexpr __managed__和constexpr __shared__变量。
示例:
constexpr int ConstexprVar = 4; // scalar type
struct MyStruct {
static constexpr int ConstexprVar = 100;
};
constexpr MyStruct my_struct = MyStruct{}; // class type
constexpr int array[] = {1, 2, 3};
__device__ constexpr int get_value(int idx) {
return array[idx]; // CORRECT
}
__device__ void foo(int idx) {
int v1 = ConstexprVar; // CORRECT
int v2 = MyStruct::ConstexprVar; // CORRECT
// const int &v3 = ConstexprVar1; // ERROR, reference to host constexpr variable
// const int *v4 = &ConstexprVar1; // ERROR, address of host constexpr variable
int v5 = get_value(2); // CORRECT, 'get_value(2)' is a constant expression.
// int v6 = get_value(idx); // ERROR, 'get_value(idx)' is not a constant expression
// int v7 = array[2]; // ERROR, 'array' is not scalar type.
MyStruct v8 = my_struct; // CORRECT
}
请参阅Compiler Explorer上的示例。
5.3.11.5. __global__可变参数模板
可变参数__global__或__tile_global__函数模板受到以下限制:
只允许有一个参数包。
参数包必须列在模板形参列表的最后。
示例:
template <typename... Pack>
__global__ void kernel1(); // CORRECT
// template <typename... Pack, template T>
// __global__ void kernel2(); // ERROR, parameter pack is not the last parameter
template <typename... TArgs>
struct MyStruct {};
// template <typename... Pack1, typename... Pack2>
// __global__ void kernel3(MyStruct<Pack1...>, MyStruct<Pack2...>); // ERROR, more than one parameter pack
请参阅Compiler Explorer上的示例。
5.3.11.6. 预置函数= default
CUDA 编译器按隐式声明的函数和显式预置函数中所述的方式,推断显式预置成员函数的执行空间。
编译器会忽略显式预置函数上的执行空间说明符,除非该函数在类外定义或是virtual函数。
示例:
struct MyStruct1 {
MyStruct1() = default;
};
void host_function() {
MyStruct1 my_struct; // __host__ __device__ constructor
}
__device__ void device_function() {
MyStruct1 my_struct; // __host__ __device__ constructor
}
struct MyStruct2 {
__device__ MyStruct2() = default; // WARNING: __device__ annotation is ignored
};
struct MyStruct3 {
__host__ MyStruct3();
};
MyStruct3::MyStruct3() = default; // out-of-line definition, not ignored
__device__ void device_function2() {
// MyStruct3 my_struct; // ERROR, __host__ constructor
}
struct MyStruct4 {
// MyStruct4::~MyStruct4 has host execution space, not ignored because virtual
virtual __host__ ~MyStruct4() = default;
};
__device__ void device_function3() {
MyStruct4 my_struct4;
// implicit destructor call for 'my_struct4':
// ERROR: call from a __device__ function 'device_function3' to a
// __host__ function 'MyStruct4::~MyStruct4'
}
请参阅Compiler Explorer上的示例。
5.3.11.7. [cuda::]std::initializer_list
[cuda::]std::initializer_list的成员函数视为具有__host__ __device__ __tile__执行空间说明符,因此可以从设备代码直接调用这些成员函数。nvcc标志--no-host-device-initializer-list会禁用此行为;此后,[cuda::]std::initializer_list的成员函数将被视为__host__函数,无法从设备代码直接调用。__global__或__tile_global__函数不能具有[cuda::]std::initializer_list类型的形参。
示例:
#include <initializer_list>
__device__ void foo(std::initializer_list<int> in) {}
__device__ void bar() {
foo({4,5,6}); // (a) initializer list containing only constant expressions.
int i = 4;
foo({i,5,6}); // (b) initializer list with at least one non-constant element.
// This form may have better performance than (a).
}
请参阅Compiler Explorer上的示例。
5.3.11.8. [cuda::]std::move, [cuda::]std::forward
默认情况下,CUDA 编译器隐式地将std::move和std::forward函数模板视为具有__host__ __device__ __tile__执行空间说明符,因此可以从设备代码直接调用它们。nvcc标志--no-host-device-move-forward会禁用此行为;此后,std::move和std::forward将被视为__host__函数,无法从设备代码直接调用。
提示
相反,cuda::std::move和cuda::std::forward始终具有__host__ __device__执行空间。
5.3.12. C++14 限制
5.3.12.1. 具有推导返回类型的函数
__global__或__tile_global__函数不能具有推导返回类型auto。
不允许在主机代码中对具有推导返回类型的__device__函数的返回类型进行内省。
注意
CUDA 前端编译器在调用主机编译器之前,会将函数声明更改为具有void返回类型。这可能会破坏主机代码中对__device__函数的推导返回类型的内省。因此,对于在设备函数体之外引用此类推导返回类型的情况,CUDA 编译器将发出编译时错误。
示例:
__device__ auto device_function(int x) { // deduced return type
return x; // decltype(auto) has the same behavior
}
__global__ void kernel() {
int x = sizeof(device_function(2)); // CORRECT, device code scope
}
// const int size = sizeof(device_function(2)); // ERROR, return type deduction on host
void host_function() {
// using T = decltype(device_function(2)); // ERROR, return type deduction on host
}
void host_fn1() {
// ERROR, referenced outside device function bodies
int (*p1)(int) = fn1;
struct S_local_t {
// ERROR, referenced outside device function bodies
decltype(fn2(10)) m1;
S_local_t() : m1(10) { }
};
}
// ERROR, referenced outside device function bodies
template <typename T = decltype(fn2)>
void host_fn2() { }
template<typename T> struct MyStruct { };
// ERROR, referenced outside device function bodies
struct S1_derived_t : MyStruct<decltype(fn1)> { };
5.3.12.2. 变量模板
使用 Microsoft 编译器时,__device__、__tile__或__constant__变量模板不能用const限定。
示例:
// ERROR on Windows (non-portable), const-qualified
template <typename T>
__device__ const T var = 0;
// CORRECT, ptr1 is not const-qualified
template <typename T>
__device__ const T* ptr1 = nullptr;
// ERROR on Windows (non-portable), ptr2 is const-qualified
template <typename T>
__device__ const T* const ptr2 = nullptr;
请参阅Compiler Explorer上的示例。
5.3.13. C++17 限制
5.3.13.1. inline变量
inline变量并不会提供超出常规变量的任何额外功能,也不具备任何实际优势。nvcc仅在分离编译模式下,或对于具有内部链接的变量,才允许具有__device__、__tile__、__constant__或__managed__内存空间的inline变量。注意
使用gcc/g++主机编译器时,使用__managed__内存空间说明符声明的inline变量可能对调试器不可见。
示例:
inline __device__ int device_var1; // CORRECT, when compiled in Separate Compilation mode (-rdc=true or -dc)
// ERROR, when compiled in Whole Program Compilation mode
static inline __device__ int device_var2; // CORRECT, internal linkage
namespace {
inline __device__ int device_var3; // CORRECT, internal linkage
inline __shared__ int shared_var; // CORRECT, internal linkage
static inline __device__ int device_var4; // CORRECT, internal linkage
inline __device__ int device_var5; // CORRECT, internal linkage
} // namespace
请参阅Compiler Explorer上的示例。
5.3.13.2. 结构化绑定
结构化绑定不能使用内存空间说明符声明,例如__device__、__tile__、__shared__、__constant__或__managed__。
示例:
struct S {
int x, y;
};
// __device__ auto [a, b] = S{4, 5}; // ERROR
5.3.14. C++20 限制
5.3.14.1. 三路比较运算符
__device__和__global__函数支持三路比较运算符(<=>),但某些用法隐式依赖于由主机实现提供的 C++ 标准库功能。使用这些运算符可能需要指定标志--expt-relaxed-constexpr以消除警告,并且该功能要求主机实现满足设备代码的要求。
示例:
#include <compare> // std::strong_ordering implementation
struct S {
int x, y;
auto operator<=>(const S&) const = default; // (a)
__host__ __device__ bool operator<=>(int rhs) const { return false; } // (b)
};
__host__ __device__ bool host_device_function(S a, S b) {
if (a <=> 1) // CORRECT, calls a user-defined host-device overload (b)
return true;
return a < b; // CORRECT, call to an implicitly-declared function (a)
// Note: it requires a device-compatible std::strong_ordering
// implementation provided in the header <compare>
// and the flag --expt-relaxed-constexpr
}
请参阅Compiler Explorer上的示例。
5.3.14.2. consteval函数
无论consteval函数的执行空间是什么,都可以从主机代码和设备代码中调用它们。
示例:
consteval int host_consteval() {
return 10;
}
__device__ consteval int device_consteval() {
return 10;
}
__device__ int device_function() {
return host_consteval(); // CORRECT, even if called from device code
}
__host__ __device__ int host_device_function() {
return device_function(); // CORRECT, even if called from host-device code
}
5.3.15. C++23 限制
除了上面的C++23 语言特性表中标明的不支持或不适用的特性之外,没有已知的 C++23 特有限制。该表中标记为 ❌ 或 N/A 的条目(包括缺陷报告的解决方案)反映的是缺失或不适用的特性,而不是额外的行为限制。
设备代码限制:
设备代码不支持固定宽度浮点类型
float16_t、float32_t、float64_t、float128_t和bfloat16_t。
5.3.15.1. 相等运算符(P2468R2)
尽管 NVCC 并未完全实现DR:你所寻求的相等运算符(P2468R2),但它近似实现了该行为,而且这种近似实现尚未导致用户代码中出现已知故障。