CUDA 工具包附带了强大的库集合,这意味着您不需要编写自己的版本来满足许多常见问题。在本章中,我们将详细研究其中的两个库。即使你唯一的兴趣是学习高效的代码,了解其他库也是值得的。
下面的列表简要描述了 CUDA 工具包中安装的一些库。这些库都是非常高质量的,并且经过优化、测试,通常使用起来非常简单。这些库是主动更新的,CUDA 工具包的每个新版本通常都包含这些库中每个库的较新版本。
CUFFT —CUDA 快速傅里叶变换。这是快速傅里叶变换算法的 CUDA 加速实现,在无数研究领域中极其重要。
CUBLAS —CUDA 基本线性代数子程序。这是流行的 BLAS 库的 CUDA 实现。
CUSPARSE—该库包含 CUDA 加速算法,用于处理稀疏和密集向量和矩阵。
NPP—英伟达性能原语。该库目前包含用于处理视频和图像数据以及一些信号处理功能的几种算法的实现。它的设计非常类似于英特尔集成性能原语库,但目前还不是一个完整的实现。
CURAND —CUDA 随机数生成。这个库包含了许多强大而高效的随机数生成器的实现,它们被 CUDA 加速。我们将在本章后面的例子中更详细地研究 CURAND 库。
推力—推力是使用并行算法和数据结构的模板库。它被设计为标准模板库(STL)的 CUDA 加速实现,这是大多数 C++ 程序员所熟悉的。我们将在本章稍后部分详细讨论推力。
其他—网上有数不清的图书馆,藏书在不断扩充和更新。如需更多图书馆,请查看https://developer.nvidia.com/gpu-accelerated-libraries。英伟达网站没有提供详尽的列表,但网站上提到的库往往质量很高。
我们现在将更详细地研究前面提到的两个库:CURAND 和推力。其他库(前面列表中提到的那些库或无数库中的任何一个库)的实现和使用通常类似于 CURAND 和/或推力。以下各节旨在作为使用 CUDA 加速库的一般介绍,以 CURAND 和推力为例。
CURAND 是一个 CUDA 加速的伪随机数生成器集合。它包括许多不同的生成器,其中大多数生成极高质量的随机数,其序列具有非常长的周期。该库之所以有用,有两个原因:它能够非常快速地生成随机数集,因为它是 CUDA 加速的,而且它还包含特殊的生成器,旨在在设备代码中动态生成随机数。
CURAND 库的美妙之处在于,它不仅可以快速生成随机数块供设备(或主机)使用,还可以在内核代码中生成数字。包含两个 APIs 一个(主机 API)设计用于与主机一起生成随机数,并将生成的序列存储在全局内存(或系统内存)中。然后,主机可以调用内核,将随机数块作为参数传递给设备使用,或者主机可以使用随机数块。另一个应用编程接口(内核或设备应用编程接口)旨在内核中动态生成随机数,供 CUDA 线程立即使用。
要使用 CURAND 库函数,您必须在项目中包含curand.h
头,并在项目属性中链接到curand.lib
库文件。要在您的项目中包含各种标题和库,请在与之前相同的文件夹中查找,并找到curand.h
和curand.lib
。如果它们在那里,并且你在书的开头正确地设置了路径,那么你应该能够只在需要的地方#include
标题,并添加。lib 到你的链接列表,就像你对cudart.lib
做的那样。
curand.h
头是主机 API 的头。此外,您可以包含curand_kernel.h
头来使用设备应用编程接口,我们将很快对此进行研究。
主机应用编程接口可用于在设备的全局内存或系统内存中生成随机数块。下面的代码清单(清单 10.1)使用主机 API 生成了两个随机数块:一个在全局内存中,一个在系统内存中。主机应用编程接口可以在没有 CUDA 构建定制的情况下以标准方式使用。cpp 而不是。cu 文件。下面的代码(清单 10.1)是作为一个新项目创建的。这一部分并不是我们之前编码的延续,而是一个全新的示例项目。
// main.cpp
#include <iostream>
#include <ctime> // For seeding with time(NULL)
#include <cuda.h>
#include <curand.h> // CURAND host API header
using namespace std;
int main() {
// Count of numbers to generate
const int count = 20;
// Device generator stores in device global memory
curandGenerator_t deviceGenerator;
// Host generators store in system RAM
curandGenerator_t hostGenerator;
// Define device pointer for storing deviceGenerator results
float *d_deviceOutput;
// Define host arrays for storing hostGenerator's results
float h_deviceOutput[count], hostOutput[count];
// Allocate memory on device for deviceGenerator
cudaMalloc(&d_deviceOutput, sizeof(float)*count);
// Initialize the two generators
curandCreateGenerator(&deviceGenerator, CURAND_RNG_PSEUDO_DEFAULT);
curandCreateGeneratorHost(&hostGenerator, CURAND_RNG_PSEUDO_DEFAULT);
// Set the seed for both generators to 0\. Using the same seed will
// cause both generators create exactly the same sequence.
curandSetPseudoRandomGeneratorSeed(deviceGenerator, 0);
curandSetPseudoRandomGeneratorSeed(hostGenerator, 0);
// If you would like the generators to use an unpredictable seed and
// create different sequences, you can use time(NULL) as the seed.
// Alter the value in some way (I've multiplied my hostGenerators by 10
// in the following) otherwise time(NULL) will be exactly the same when
// you seed them.
// curandSetPseudoRandomGeneratorSeed(deviceGenerator, time(NULL));
// curandSetPseudoRandomGeneratorSeed(hostGenerator, time(NULL)*10);
// Generate a random block of uniform floats with both generators:
// The deviceGenerator outputs to global memory:
curandGenerateUniform(deviceGenerator, d_deviceOutput, count);
// The hostGenerator's output is in system RAM
curandGenerateUniform(hostGenerator, hostOutput, count);
// Copy the device results to the host
cudaMemcpy(h_deviceOutput, d_deviceOutput,
sizeof(float)*count, cudaMemcpyDeviceToHost);
// Free device and CURAND resources
cudaFree(d_deviceOutput);
curandDestroyGenerator(deviceGenerator);
curandDestroyGenerator(hostGenerator);
// Print out the generator sequences for comparison
for(int i = 0; i < count; i++) {
cout<<i<<". Device generated "<<h_deviceOutput[i]<<
" Host generated "<<hostOutput[i]<<endl;
}
return 0;
}
清单 10.1: CURAND 主机应用编程接口
清单 10.1 生成两个由 20 个随机浮点值组成的块。清单 10.1 中有两种类型的生成器:主机和设备(不要与这两个 API 混淆)。这两个生成器都属于主机应用编程接口,设备应用编程接口在内核内部生成其值。两个生成器的区别在于主机生成器(hostGenerator
)将结果序列存储在系统 RAM 中,而设备生成器(deviceGenerator
)将序列存储在全局内存中。前面清单中的两个生成器应该为它们的随机浮点块生成完全相同的值,因为它们使用相同的种子。事实上,每次执行程序时,生成器都会生成完全相同的数字序列。如果您注释掉将生成器播种到0
的行,并取消将生成器播种到time(NULL)
的行的注释,您应该会发现生成器会生成彼此不同的数字,并且每次执行程序时都会生成不同的值。
| | 注意:你不需要包含 cuda.h 头,因为 CUDA API 已经包含在 curand.h 头中了。同样,可以在不包含 ctime 头的情况下调用 time(NULL)函数。为了清楚起见,包含了这些标题。 |
要使用 CURAND 库生成随机数,首先需要创建生成器句柄。
可以用curandCreateGeneratorHost
创建主机生成器,也可以用curandCreateGenerator
创建设备生成器。每个函数的第一个参数是curandGenerator_t
,它将作为生成器句柄传递给后续的 CURAND 函数调用。
第二个参数是要使用的随机数生成器的类型。用计算机生成随机数有许多不同的算法,随机生成器的列表随着这个库的每一代而变化。要查看已安装的 CURAND 库版本中包含的算法的完整列表,右键单击CURAND_RNG_PSEUDO_DEFAULT
标识符,并从 Visual Studio 的上下文菜单中选择转到定义,或搜索文件CURAND.h
并检查其内容。
d_deviceOutput
指针是指向设备全局内存中数据的设备指针。在序列生成之后,这个指针可以被传递给内核使用。在清单 10.1 中,它被复制回主机并打印到屏幕上,以便与主机生成的值进行比较。
| | 提示:当使用主机应用编程接口供设备使用时,最好尽可能少地生成数字。这些数字应该在内核调用之前生成一次。当主机在算法中生成新的数字时,在主机和设备之间来回跳转会非常慢。 |
均匀分布发电
均匀分布的随机float
位于0.0f
和1.0f
之间。理论上,这些点之间的每个可能值与序列中生成的任何其他值一样可能。在现实中,任何两个实数之间都有无穷多个实数,但是在计算中,在0.0f
和1.0f
之间有大约 2 个 24 不同的浮点数。
| | 提示:任何范围内的随机数都可以从一组随机的规范化数字(如 curandGenerateUniform 生成的数字)中创建。例如,要将清单 10.1 中的序列转换为 100.0f 和 150.0f 之间的随机浮点数,您可以将每个序列乘以 50.0f(最小值和最大值之间的差值,150.0f–100.0f)并加上 100.0f。请注意,curandGenerateUniform 函数从 0.0 f 到 1.0f 生成,但不包括 0.0 f,1.0f 包括在内。 |
正态分布发电
有时我们需要生成正态分布的数字。这意味着最有可能生成的值将是最接近平均值的值。远离平均值的数字产生的可能性越来越小,数字被绘制出来的频率看起来就像我们熟悉的正态分布的钟形曲线。
在正态分布中,任何数字都是可能的,但是生成远离平均值的数字(在标准分数中)的机会很快变得非常小。使用curandGenerateNormal
功能根据一条正常曲线生成数字。该函数需要一个平均值和一个标准偏差值来描述生成数字的曲线的下降和幅度。例如,根据清单 10.2,将前面代码中对curandGenerateUniform
的调用替换为对curandGenerateNormal
的调用,调用基于标准正态曲线(这是一条具有平均值0.0f
和标准偏差1.0f
的正态曲线)。
// Generate a random block of floats from normal curves
curandGenerateNormal(deviceGenerator, d_deviceOutput, count, 0.0f, 1.0f);
curandGenerateNormal(hostGenerator, hostOutput, count, 0.0f, 1.0f);
清单 10.2:从普通曲线生成数字
你会看到生成的数字趋向于0.0f
(我们的正常曲线的平均值和curandGenerateNormal
函数的第 4 个参数),但是有几个是1.0f
或者更远离这个平均值。大部分(理论上 68.2%)生成的数字将在-1.0f
和1.0f
内,因为我们提供了1.0f
作为我们的标准偏差(curandGenerateNormal
函数的第 5 个参数)和0.0f
是-1.0
和1.0
的中间值。使用平均值的值0.0f
和标准偏差的值1.0f
意味着该数值将从标准正态曲线生成。
设备应用编程接口允许在内核中生成随机值,而无需离开设备。设备应用编程接口比主机应用编程接口稍微复杂一点,因为它需要设置和维护多个生成器的状态(每个线程一个)。在项目文件中包含curand_kernel.h
头以使用设备应用编程接口,并与主机应用编程接口一样,链接到项目属性中的curand.lib
库。生成发生在内核中,因此(与主机应用编程接口不同)您需要使用 CUDA 构建定制来启动内核并将代码存储在. cu 文件中。检查清单 10.3 中的代码,特别是InitRandGenerator
和Generate
内核。同样,这段代码旨在作为一个新项目;它没有建立在我们之前编码的任何东西上。
// main.cu
#include <iostream>
#include <cuda.h>
#include <curand_kernel.h>
#include <device_launch_parameters.h>
using namespace std;
// Kernel to init random generator states
__global__ void InitRandGenerator(curandStateMRG32k3a *state) {
int id = threadIdx.x + blockIdx.x * blockDim.x;
// Init the seed for this thread
curand_init(0, id, 0, &state[id]);
}
// Generate random numbers using the device API
__global__ void Generate(unsigned int *result, int count, curandStateMRG32k3a *state) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
// Make a local copy of the state
curandStateMRG32k3a localState = state[idx];
// Generate one number per thread
if(idx < count) {
// The numbers generated will be integers between 0 and 100
result[idx] = (unsigned int) curand(&localState)%100;
}
}
int main() {
// How many numbers to generate
const int count = 100;
// Declare the arrays to hold the results
unsigned int* h_a = new unsigned int[count];
unsigned int* d_a;
cudaMalloc(&d_a, sizeof(unsigned int)*count);
// Declare the array to hold the state of the generator
curandStateMRG32k3a *generatorState;
cudaMalloc(&generatorState, sizeof(curandStateMRG32k3a) * count);
// Call an init kernel to set up the state
InitRandGenerator<<<(count/64)+1, 64>>>(generatorState);
// Call a kernel to generate the random numbers
Generate<<<(count/64)+1, 64>>>(d_a, count, generatorState);
// Copy the results back to the host
cudaMemcpy(h_a, d_a, sizeof(unsigned int)*count, cudaMemcpyDeviceToHost);
// Print the results to the screen
for(int i = 0; i < count; i++)
cout<<i<<". "<<h_a[i]<<endl;
// Free resources
cudaFree(d_a);
cudaFree(generatorState);
delete[] h_a;
return 0;
}
清单 10.3: CURAND 设备应用编程接口
清单 10.3 使用 CURAND 设备 API 生成 100 个随机无符号int
。它将数字存储在全局内存中,然后将它们复制到主机并打印出来。这里需要记住的重要一点是,生成的数字可以立即在内核的代码中使用;我把它们复制回来纯粹是为了演示。
需要分配一个设备内存区域来保存生成器的状态;这可以使用cudaMalloc
来完成。我选择了curandStateMRG32k3a
状态,它将使用多重递归生成器算法(这是英特尔 MKL 矢量统计库中可用的MRG32k3a
算法的 CUDA 版本)。
在生成后续随机数之前,必须知道到目前为止为每个线程生成的数字的种子和当前位置。此状态维护此信息。
每个线程通过调用curand_init
设备函数(在InitRandGenerator
内核中),基于threadIdx
设置自己的种子。
由于threadIdx.x
在这个网格中被计算为唯一的,每个线程将生成不同的种子。一旦状态被播种,线程可以通过调用curand
生成一个随机数。curand
函数为每个线程返回状态的下一个 32 位序列。我们可以用模数运算符进行除法后的余数,得到某个范围内的数字(在清单 10.3 中,由于%100
,数字的范围将从 0 到 99)。
第**行 curandstatemrg32 k3a local state = state[idx];**为每个线程创建状态的本地副本。它对清单 10.3 的性能没有影响,但是如果您的内核为每个线程生成许多数字,那么使用本地副本会更快。创建一个本地副本意味着我们不需要使用数组下标,这比较慢。
| | 提示:创建生成器是一个缓慢的过程,如果可能的话,应该创建一次并重用它们,以避免每次设置状态的开销。 |
我们将详细查看的第二个库是推力库。推力库是 C++ 标准模板库(STL)的 CUDA 加速版本。该库提供用 CUDA 优化的数据类型和常用算法。
| | 提示:推力库中包含的许多标识符与 STL 中的标识符同名。当混合推力和 STL 代码时,避免使用 using 指令来包含整个名称空间可能会有所帮助。相反,完全限定引用。例如,使用标准::排序和推力::排序。 |
推力库中包含两种不同类型的基本矢量:host_vector
和device_vector
。主机向量存储在系统内存中并由主机控制,而设备向量存储在设备内存中并由设备控制。
推力头位于 CUDA 工具包主标题目录的推力子目录中。要在代码中使用主机或设备向量,请包含适当的标题thrust\host_vector.h
或thrust\device_vector.h
。
推力没有自己的库(也就是说,没有推力. lib,就像 CURAND 一样)。但是它确实使用了标准的 CUDA Runtime 库,所以在使用推力之前要确保你的项目链接到CUDART.lib
。
要定义推力矢量,请使用thrust::host_vector<type>
或thrust::device_vector<type>
,其中type
是矢量的数据类型。有各种各样的构造函数可以用来初始化向量,包括定义向量中每个元素的大小和初始值。清单 10.4 展示了定义向量、访问元素以及在各种向量库类型之间复制数据的一些基本示例。
// main.cu
#include <iostream>
#include <thrust\host_vector.h>
#include <thrust\device_vector.h>
#include <vector> // STL vector header
int main() {
// Defining vectors
// STL vector, 100 ints set to 18
std::vector<int> stdV(100, 18);
// Host vector of 100 floats
thrust::host_vector<float> h(100);
// Host vector, 100 ints set to 67
thrust::host_vector<int> hi(100, 67);
// Empty device vector of doubles
thrust::device_vector<double> dbv;
// Device vector, 100 ints set to 0
thrust::device_vector<int> di(100, 0);
//
// Adding and removing elements
//
// Print initial element count for h
std::cout<<"H contains "<<h.size()<<" elements"<<std::endl;
// Remove last element
h.pop_back();
std::cout<<"H contains "<<h.size()<<" elements"<<std::endl;
// Adding and removing elements from device vector is the same
dbv.push_back(0.5);
std::cout<<"Final elements of dbv is "<<dbv.back()<<std::endl;
dbv.pop_back();
//
// Copying data between vectors
//
// Print out the first element of h
std::cout<<"First element of hi is "<<hi[0]<<std::endl;
// Copying host vector to device vector
di = hi;
std::cout<<"First element of di is "<<di[0]<<std::endl;
// Copy from STD vector to device vector
di = stdV;
std::cout<<"First element of di is "<<di[0]<<std::endl;
return 0;
}
清单 10.4:基本向量操作
清单 10.4 显示了向量可以从主机复制到设备,甚至可以使用等号运算符 = 。拷贝到设备向量或从设备向量拷贝使用幕后的cudaMalloc
。在使用推力进行编码时,请记住这一点,否则代码的性能会大幅下降,尤其是在执行许多小副本时。
可以使用数组下标运算符[
]
访问元素。标准的 STL 矢量操作,如push_back
、pop_back
和back
(以及许多其他操作)都适用于推力矢量。当函数返回时,函数、设备和主机本地的向量被自动处理。
清单 10.5 展示了一个如何使用推力中可用的排序算法的例子。它创建并排序三个 20,000,000 个整数的列表。一个是用 STL 排序的,一个是推力主机向量,最后一个是推力设备向量。每个向量类型对整数进行五次排序,在排序之间重新生成一个随机列表。请注意,在本例中,设备向量实际上并不生成随机数,而是从主机向量复制随机数。
// main.cu
// Standard headers
#include <iostream>
#include <ctime>
// CUDA and Thrust headers
#include <thrust\host_vector.h>
#include <thrust\device_vector.h>
#include <thrust\sort.h>
// STL headers
#include <vector>
#include <algorithm>
// Helper defines and variable for timing
long startTime;
#define StartTimer() startTime=clock()
#define ElapsedTime (clock()-startTime)
int main() {
int count = 20000000; // 20 million elements to sort
// Vectors
std::vector<int> stlVec(count); // STL vector
thrust::host_vector<int> hostVec(count); // Thrust host vector
thrust::device_vector<int> devVec(count); // Thrust device vector
std::cout<<"Comparison of Thrust and STL sort times on vectors of size "<<
count<<"\n\n";
// Sort 5 times using STL vector
std::cout<<"STL vector:"<<std::endl;
for(int i = 0; i < 5; i++) {
std::generate(stlVec.begin(), stlVec.end(), rand);
StartTimer();
std::sort(stlVec.begin(), stlVec.end());
std::cout<<"Run["<<i<<"] Sorted in "<<ElapsedTime<<" millis"<<
std::endl;
}
// Sort 5 times using Thrust host vector
std::cout<<"\nThrust host vector:"<<std::endl;
for(int i = 0; i < 5; i++) {
thrust::generate(hostVec.begin(), hostVec.end(), rand);
StartTimer();
thrust::sort(hostVec.begin(), hostVec.end());
std::cout<<"Run["<<i<<"] Sorted in "<<ElapsedTime<<" millis"<<
std::endl;
}
// Sort 5 times using Thrust device vector
std::cout<<"\nThrust device vector:"<<std::endl;
for(int i = 0; i < 5; i++) {
// Host generates values for devVec
thrust::generate(hostVec.begin(),hostVec.end(),rand);
devVec = hostVec; // Copy from host since there's no rand()
StartTimer();
thrust::sort(devVec.begin(), devVec.end());
std::cout<<"Run["<<i<<"] Sorted in "<<ElapsedTime<<" millis"<<
std::endl;
}
std::cout<<"All tests complete!"<<std::endl;
return 0;
}
清单 10.5:向量排序比较
图 10.1:清单 10.5 的示例输出
推力设备向量显然非常快(图 10.1 显示设备向量比 STL 快 10 倍左右,比主机向量快 3 倍左右)。也许令人惊讶的是,宿主向量比 STL 快得多。当列表已经排序(或接近排序)时,STL 排序有一个特殊的技巧;它注意到元素是有序的,并提前退出排序。这给了 STL 一个特殊的优势,几乎排序列表。将重新生成随机列表的线放置在for
循环之外显示了这一点。当列表被排序时,STL 在被测试的机器上以大约 168 毫秒的时间进行排序。
将对sort
的所有调用替换为stable_sort
实现了稳定的排序。通过稳定的排序,保证相同元素的索引相对于彼此保持有序。稳定的排序减慢了 STL 向量,而推力向量相对不受影响。
作为最后一个例子,清单 10.6 展示了推力库的更多功能。它找出一个整数的因子,直到它的平方根。这是一种效率非常低的分解整数的方法(这只是一种蛮力搜索,内存占用非常不理想),但它展示了如何使用推力库的其他几个重要功能。
以下代码不适用于计算能力为 1.x 的设备。请记住将项目中的 SM 和计算值设置为 2.0 或更高。
// main.cu
#include <iostream>
// Thrust headers
#include <thrust\device_vector.h>
#include <thrust\transform.h>
#include <thrust\sequence.h>
#include <thrust\copy.h>
// Helper function, returns true if x is zero
// else return false
struct isZero {
__device__ bool operator()(const int x) {
return x == 0;
}
};
int main() {
int x = 600; // The integer to factorize
//
// Define Thrust device vectors
//
// dx vector will be filled with multiple copies of x
thrust::device_vector<int> dx((int)(std::sqrt((double)x)+1));
// dy vector will be filled with sequence (2, 3, 4... etc.)
thrust::device_vector<int> dy((int)(std::sqrt((double)x)+1));
// factors vector is used to store up to 20 factors
thrust::device_vector<int> factors(20);
// Fill dx with multiple copies of x
thrust::fill(dx.begin(), dx.end(), x);
// Fill dy with a sequence of integers from 2 up to
// the square root of x
thrust::sequence(dy.begin(), dy.end(), 2, 1);
// Set elements of dx[] to remainders after division
// of dx[] and dy[]
thrust::transform(dx.begin(), dx.end(), dy.begin(), dx.begin(),
thrust::modulus<int>());
// Multiply all remainders together
int product = thrust::reduce(dx.begin(), dx.end(), 1,
thrust::multiplies<int>());
// If this product is 0 we found factors!
if(!product && x >= 4) {
std::cout<<"Factors found!"<<std::endl;
// Compact dx to factors where dy is 0
thrust::copy_if(dy.begin(), dy.end(), dx.begin(),
factors.begin(), isZero());
// Print out the results
for(int i = 0; i < factors.size(); i++) {
if(factors[i] != 0)
std::cout<<factors[i]<<" is a factor of "<<
x<<std::endl;
}
}
else { // Otherwise x is prime
std::cout<<x<<" is prime, factors are 1 and "<<x<<std::endl;
}
return 0;
}
清单 10.6:分解整数
在清单 10.6 中的标题之后,您将看到结构的定义(isZero
)。该结构由一个布尔函数组成,该函数在代码后面用作谓词。
main
方法中使用了三个向量。dx
和dy
(与微积分无关)用作两个临时设备存储向量。dx
保存被因子分解的整数的多个副本,这是变量x
的值。dy
保存从 2 到x
平方根的所有整数。
为了用多个x
变量的副本填充dx
向量,使用了thrust::fill
函数。它需要两个迭代器,指定要填充的区域的开始和结束,以及第三个参数,即要填充的值。
为了创建从 2 到x
平方根的所有整数的列表,并将它们存储在dy
向量中,我使用了thrust::sequence
函数。sequence
有重载版本,但这里使用的版本取四个参数。前两个参数是被填充区域的开始和结束迭代器。第三个参数是开始时的值,最后一个参数是生成的值之间的步长或间隙。
下一步是将存储在dx
中的x
除以dy
中的顺序整数,用模数运算符取余数。在代码中,这是通过thrust::transform
功能实现的。变换对向量的许多元素执行相同的操作。代码中使用的转换函数有四个参数。前两个是输入的开始和结束迭代器。下一个是第二个输入的开始迭代器(模数需要两个操作数)。第三个参数是结果将被存储到的输出迭代器,最后一个参数是要在dx
和dy
向量的元素之间执行的函数。这个函数调用的结果是dx
中的每个x
s 被dy
中对应的元素除,除后的余数替换dx
中的原值。
下一个函数调用是使用thrust::reduce
的例子。一种reduce
算法(或并行归约)将向量(或数组或任何其他列表)的元素归约为一个值。例如,它可以产生数组中元素的总和、标准差、平均值或任何其他单个值。在代码中,它用于将dx
的所有元素相乘。如果其中任何一个是0
,那么这个乘法的结果将是0
,这意味着我们找到了因子(即找到了一些均匀划分x
的整数)。如果该乘法的结果不是0
,则表示dy
中的整数都不是x
的因子。如果从 2 到x
的平方根之间没有整数是x
的因子,那么x
就是质数。
这里使用的thrust::reduce
函数取四个参数。前两个是要缩减的数据的开始和结束迭代器。第三个参数是缩减的起始值。如果你要对一系列数字求和,你可以用 0 作为初始值,但是这里我们要相乘,所以我用了 1。最后一个参数是要使用的操作。我用了thrust::multiplies<int>()
的整数乘法。这种简化产生的float
( product
)是dx
向量中所有元素的乘积。这里使用的减少完全是浪费时间,仅供thrust::reduce
举例说明。
最后一个任务(如果我们找到了因子并且product
是0
)是将我们找到的因子复制到另一个向量中(T2 向量)。为此,我们希望将元素从dy
复制到factors
,其中dx
的元素是0
。这是一个用英语描述的复杂操作,但是通过调用thrust::copy_if
很容易实现。这个操作被称为流压缩。我们基于一些谓词将dy
中的元素压缩成另一个向量(即dx
中对应的元素是0
)。这里使用的函数(thrust::copy_if
)取五个参数;前两个是被压缩的流的开始和结束迭代器,也就是dy
。下一个称为模具参数;它是要与谓词进行比较的值的向量。这里,我们正在检查dx
的值是否为0
,所以模板为dx
。第四个参数是存储结果的开始迭代器factors.begin()
。最后一个参数是谓词,一个返回bool
的函数,它决定哪些元素被复制,哪些没有被复制。
isZero()
函数在应用程序的顶部定义;当其参数为0
时返回true
,对于任何其他值返回false
。这个谓词(isZero
)是一个__device__
函数。dx
向量中的每个元素将同时通过谓词函数传递;当结果为真时,dy
的对应元素被复制到factors
中的后续位置。流压缩的最终结果是x
的因子将存储在factors
向量中,并使用for
循环和cout
将它们打印到屏幕上。
这是对推力库某些方面的快速浏览。每一类算法(变换、约简和流压缩)都比这个简单的例子说明的要灵活得多。此外,推力库有一个非常强大和灵活的迭代器的集合,我们在这些例子中没有看到。有关推力库主题的进一步阅读,请参考 CUDA 工具包附带的推力快速入门指南。对于工具包的每个版本,PDF 文档的确切路径会有所不同;以下是 6.5 版的默认路径:
c:\程序文件\NVIDIA GPU 计算工具包\ CUDA \ v6.5 \ doc \ pdf \推力 _ 快速 _ 启动 _ 指南. pdf
《推力快速启动指南》也可从英伟达在线获得,网址为http://docs.nvidia.com/cuda/thrust/index.html。