Thrust快速入门教程(三) —— Algorithms

  Thrust提供了丰富的常用并行算法。这算法的功能与STL中的非常相似,于是我们使用了相同的名称(例如thrust::sortstd::sort)。

  所有的Thrust算法均提供了host端(主机端)和device端(设备端)的实现。当传入迭代器指向主机端时,将会调用主机端方法,当使用迭代器指向设备端时将调用设备端实现。

  在Thrust的算法中,的迭代器参数指向必须在同一存储位置,或者主机端或者设备端。除了thrust::copy,它可以任意的拷贝主机端和设备端的数据。


Transformations

  Transformations会对指定输入范围内的元素进行特定操作,并将其结果存储到指定位置。如thrust::fill

thrust::fill(D.begin(), D.begin() + 7, 9); // 将vector D的前7个元素设置为9

  此外transformations还包括thrust::sequencethrust::replacethrust::transform(衍生于C++的for_each)。完整的列表请参考文档。

  下面的代码演示了几个transformation的用法。其中thrust::negatethrust::modulus与C++中的仿函数定义是一样的。Thrust在thrust/functional.h中也提供常用的仿函数,如plusmultiplies等。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(void)
{
  // allocate three device_vectors with 10 elements
  thrust::device_vector X(10);
  thrust::device_vector Y(10);
  thrust::device_vector Z(10);

  // initialize X to 0,1,2,3, ....
  thrust::sequence(X.begin(), X.end());
  // compute Y = -X
  thrust::transform(X.begin(), X.end(), Y.begin(), thrust::negate());
  // fill Z with twos
  thrust::fill(Z.begin(), Z.end(), 2);
  // compute Y = X mod 2
  thrust::transform(X.begin(), X.end(), Z.begin(), Y.begin(), thrust::modulus());
  // replace all the ones in Y with tens
  thrust::replace(Y.begin(), Y.end(), 1, 10);
  // print Y
  thrust::copy(Y.begin(), Y.end(), std::ostream_iterator(std::cout, "\n"));

  return 0;    
}

  thrust/fuctuional.h中的函数提供了大部分内置算数和比较运算,但是我们想提供更多出色的功能。
  
  比如,计算y < - a * x + y,x、y为向量,a为常数。这其实就是我们所熟知的由BLAS提供的SAXPY(a*x+y)运算。如果我们在thrust中实现SAXPY我们有几个选择。
    
  一个是,我们需要使用两个transformations(一个加和一个乘法),还需要一个vector用于存储a的值。另一更佳选择是使用一个单独的由用户自己定义的transformation,这才是我们真正先要的。下面用源代码解释说明这两种方法。

struct saxpy_functor
{
  const float a;

  saxpy_functor(float _a) : a(_a) {}

  __host__ __device__
  float operator()(const float& x, const float& y) const
  { 
    return a * x + y;
  }
};

void saxpy_fast(float A, thrust::device_vector& X, thrust::device_vector& Y)
{
  // Y <- A * X + Y
  thrust::transform(X.begin(), X.end(), Y.begin(), Y.begin(), saxpy_functor(A));
}

void saxpy_slow(float A, thrust::device_vector& X, thrust::device_vector& Y)
{
  thrust::device_vector temp(X.size());

  // temp <- A
  thrust::fill(temp.begin(), temp.end(), A);

  // temp <- A * X
  thrust::transform(X.begin(), X.end(), temp.begin(), temp.begin(), thrust::multiplies());

  // Y <- A * X + Y
  thrust::transform(temp.begin(), temp.end(), Y.begin(), Y.begin(), thrust::plus());
}

  saxpy_fast和saxpy_slow都是有效的SAXPY实现,但saxpy_fast会比saxpy_slow更快。忽略temp vector的分配与代数运算的开销,内存读写的开销如下:

  • fast_saxpy: 2N次读取和 N次写入
  • slow_saxpy:4N次读取和3N次写入

  因为SAXPY受到存储器约束(它的性能受限于存储器带宽,而不是浮点计算性能),大量的读写操作使得saxpy_slow开销更加昂贵。而saxpy_fast执行速度与BLAS优化版本中的实现一样快。存储器约束算法通常使用kernel fusion(即融合,将多个操作组合为单一的kernel)的方法以最小化内存的读写交换。

  thrust::transform只支持一个或者两个输入参数的transformations(例如 f(x) -> y 和 f(x, y) -> z)。当transformation使用多于两个输入参数的时候需要使用其他方法了。

  Arbitrary_transformation展示了使用thrust::zip_interatorthrust::for_each的解决方案。


Reductions

  归约算法使用二元运算(operator)将输入序列规约为一个值。例如,需要获得一数列的和,可以通过“加运算”规约得到;数列的最大值,可以通过两个参数的“最大值运算”规约得到。

  数列求和的规约操作可以由thrust::reduce实现:

int sum = thrust::reduce(D.begin(), D.end(), (int) 0, thrust::plus());

  开始的两个参数定义了需要规约数组的范围,第三和第四个参数分别提供了归约的初始值和相关的operator。事实上,归约求和是很常见的,我们可以选择没有初始值和operator的版本。

int sum = thrust::reduce(D.begin(), D.end(), (int) 0, thrust::plus());
int sum = thrust::reduce(D.begin(), D.end(), (int) 0);
int sum = thrust::reduce(D.begin(), D.end());

  虽然thrust::reduce能够有效地满足大部分的规约操作,但是Thrust库提供了另外的一些函数以便使用(类似于STL)。例如,thrust::count能够返回给定序列中特定值的数量。

#include 
#include 
...
// put three 1s in a device_vector
thrust::device_vector vec(5,0); //vec包含5个值为0的元素
vec[1] = 1;
vec[3] = 1;
vec[4] = 1;

// count the 1s // 计算1的数量
int result = thrust::count(vec.begin(), vec.end(), 1);
// result is three // result = 3

  其他规约操作,包括thrust::count_ifthrust::min_elementthrust::max_elementthrust::is_sortedthrust::inner_product等,详细请参考文档。

  前面SAXPY例子使用的transformation kernel展示了如何通过融合来减少内存交换。我们也可以使用thrust::transform_reduce实现与归约算法的融合。下面的例子用来计算向量的模:

#include 
#include 
#include 
#include 
#include 

// square computes the square of a number f(x) -> x*x
template <typename T>
struct square
{
  __host__ __device__
  T operator()(const T& x) const
  { 
    return x * x;
  }
};

int main(void)
{
  // initialize host array
  float x[4] = {1.0, 2.0, 3.0, 4.0};

  // transfer to device
  thrust::device_vector<float> d_x(x, x + 4); //利用x来初始化d_x

  // setup arguments
  square<float>        unary_op;   //一元运算
  thrust::plus<float> binary_op;  //二元运算
  float init = 0;

  // compute norm
  float norm = std::sqrt( thrust::transform_reduce(d_x.begin(), d_x.end(), unary_op, init, binary_op) );

  std::cout << norm << std::endl;

  return 0;
}

  这里平方是一元运算,将输入序列的每个元素平方。接着,平方和使用标准的加法规约得到。
  当然我们也可以像慢版本的SAXPY transformation实现这个功能:首先用自己定义的平方运算(或者直接使用仿函数multiplies)计算出平方后的值,最后使用归约求和。但是显然这样做会带来不必要的浪费,速度降低。通过融合规约和平方操作,我们就可以获得与自己编写内核一样的高性能。


Prefix-Sums

  前缀和,也叫scan操作,在stream compaction、基数排序中具有重要的作用。下面的源码将举例说明使用默认加法的inclusive scan:

#include 

int data[6] = {1, 0, 2, 2, 1, 3};
thrust::inclusive_scan(data, data + 6, data);  // in-place scan ,in-place指的是输入输出的位置一样的
// data is now {1, 1, 3, 5, 6, 9}

  Inclusive scan的每个输出元素为输入数列的相应部分和。例如,data[2] = data[0] + data[1] + data[2]。Exclusive scan类似,但是右移一个位置:

#include 

int data[6] = {1, 0, 2, 2, 1, 3};
thrust::exclusive_scan(data, data + 6, data); // in-place scan
// data is now {0, 1, 1, 3, 5, 6}

  现在为data[2] = data[0] + data[1]。由例子可见,inclusive_sacn与exclusive_scan允许原址操作。

  Thrust也提供了函数transform_inclusive_scantransform_exclusive_scan可以实现在scan操作前对输入数列进行一元运算。完整的scan说明请参见文档。
  


Reordering

  Thrust通过下面的算法为partitioning和stream compaction提供支持:

  • copy_if:保留Predicate的结果中为真的元素。
  • partition:根据Predicate的结果重新排列元素(真值对应的元素在前,其余元素在后)。
  • remove/remove_if:删除Predicate的结果中为真的元素。
  • unique:移除连续重复的元素。

注:Predicate是一个一元函数,用来判断真或假。例如,函数is_positive()用来判断是否是正数。

#include 
...
struct is_even
{
  __host__ __device__
  bool operator()(const int &x)
  {
    return (x % 2) == 0;
  }
};
...
int A[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
const int N = sizeof(A)/sizeof(int);
thrust::partition(A, A + N,is_even()); //偶数在前,奇数在后
// A is now {2, 4, 6, 8, 10, 1, 3, 5, 7, 9}

  完整的函数和例子参见文档。


Sorting

  Thrust提供一系列函数,可以根据既定的准则来分类数据和重排数据。thrust::sortthrust::stable_sort函数对应STL中的sortstable_sort

#include 
...
const int N = 6;
int A[N] = {1, 4, 2, 8, 5, 7};
thrust::sort(A, A + N);
// A is now {1, 2, 4, 5, 7, 8}

  另外,Thrust提供thrust::sort_by_keythrust::stable_sort_by_key

#include 
...
const int N = 6;
int    keys[N] = {  1,   4,   2,   8,   5,   7};
char values[N] = {'a', 'b', 'c', 'd', 'e', 'f'};
thrust::sort_by_key(keys, keys + N, values);
// keys is now   {  1,   2,   4,   5,   7,   8}
// values is now {'a', 'c', 'b', 'e', 'f', 'd'}

  跟STL类似,sorting函数同样接受用户自定义对比操作。

#include 
#include 
...
const int N = 6;
int A[N] = {1, 4, 2, 8, 5, 7};
thrust::stable_sort(A, A + N, thrust::greater<int>());
// A is now {8, 7, 5, 4, 2, 1}

参考:

  1. Thrust快速入门教程(三)
  2. thrust: Modules
  3. 【C++ STL】深入解析神秘的 — 仿函数 - 小田的专栏 - 博客频道 - CSDN.NET
  4. GPU Gems 3 - Chapter 39. Parallel Prefix Sum (Scan) with CUDA

你可能感兴趣的:(cuda)