《C++ AMP:用Visual C++加速大规模并行计算》——3.5 array_view< T,N >

简介:

本节书摘来自异步社区出版社《C++ AMP:用Visual C++加速大规模并行计算》一书中的第3章,第3.5节,作者: 【美】Kate Gregory , Ade Miller,更多章节内容可以访问云栖社区“异步社区”公众号查看。

3.5 array_view< T,N >

C++ AMP:用Visual C++加速大规模并行计算
array表示的是加速器上的数据。我们既可以在构造array的同时就加载数据,也可以延迟加载数据。无论哪一种做法,在执行一些计算以后,都几乎肯定要把结果从数组传回CPU,这样我们才能在应用程序的其他部分中使用这些结果。

注意事项:
一些应用,例如在前一章里看到的NBody案例,并没有选择把数据复制回来。它们把数据留在GPU上,以此来渲染移动粒子云。但更常规的做法是先返回值,然后在应用程序的另一个部分使用或显示给用户看。
当然,我们只使用数组也能写出来有用的程序,但C++ AMP还提供了array_view,它支持的功能能让我们更方便地直接使用数组。对加速器来说,array_view就像是一个数组,但它免去了把数据复制到加速器或从加速器复制数据的麻烦。

array_viewarray之间的关系有点儿(尽管这么说有点儿不准确)像是引用和引用指向的对象之间的关系。就像引用那样,数组视图在创建的时候就要初始化。同样和引用一样,改变array_view也会(最终)改变其来源的数据。但是,反过来就不正确了:改变array_view来源处的数据并不会自动地改变array_view,因此我们要小心地处理这种情况。

array_view的创建方式有两种:从加速器的array创建,或者从CPU上的一系列数据(如`std::
vector)中创建。array_view`构造完成后,数据可以按需复制。例如,加速器上的parallel_
for_each开始使用array_view中的值时,这些值就会复制到加速器上。并行处理结束后,
array_view用完地,加速器array_view中的新值会被同步回向量中。

下述代码演示了这种方法。向量v在CPU内存中创建,初始化为{0,1,2,3,4}。接下来,array_view使用v中存储的数据初始化。直到parallel_for_each开始在GPU上执行时,数据才会复制到GPU内存中。lambda表达式会在GPU上执行,作用是将数组中的每个元素值翻倍。在访问CPU上改变的值之前,程序必须首先调用array_view上的synchronize()方法。

std::vector<int> v(5);
std::iota(v.begin(), v.end(), 0);
array_view<int, 1> av(5,v);
parallel_for_each(av.extent, [=](index<1> idx) restrict(amp)
{
   av(idx) = av(idx) * 2;
});
av.synchronize();```
在未来或者在某些加速器上将会有针对性的优化,例如只把那些发生改变的元素值复制回去,但我们不能对此抱有过多期望。如果运行时代码知道array_view没有发生改变,`synchronize()`方法就什么都不会做。

我们可以调用array的view_as()方法,把array_view中的整个数组包装起来,view_as()返回的即是array_view。或者,也可以使用array类的section()方法,传入原点值和范围值,
section()包装的只是array_view的一部分内容。有时候,我们会想换一种方式看数据;例如,把三维数组看成是一维数组。数组的reinterpret_as()方法会返回此类视图。就像标准C++的
reinterpret_cast,名字本身就能告诉阅读代码的人数据访问方式将会发生重大改变。这个方法会把数组的秩降为一,换言之,这个方法并不能以二维数组的形式来看待三维数组。元素布局与内存布局一样,最低有效索引值差值为1的元素彼此之间是相邻的。

一旦把std::vector这样的CPU受限数据包在array_view里,就最好不要改变CPU上的原始数据。因为缓存的存在,加速器所使用的array_view并不会发生改变,如果array_view复制回源向量,它们可能会被重写。如果必须要直接改变源数据,可以选择在array_view上调用refresh()刷新array_view改变。同步工作也是必须的,这样才能保证在源向量发生改变的同时,array_view不会改变。

C++11中的lambda表达式

最新的C++标准是C++11,引入了一些新的语言功能,其中就有lambda表达式。起先,lambda表达式好像只能解决一些小问题:它们把我们解放出来,免去我们在小函数传入std::sort()等标准算法之前先行命名的痛苦,也不需要我们构造仿函数(functor,也称作函子)或者函数对象把“做事”的想法从代码某处传入另一处。表面上,lambda表达式提供的只是“语法糖”,但是lambda表达式的方便性、可读性和简单性是很显然的,它们的存在使许多编程习语用起来更加趁手。C++ AMP的“入口”,即parallel_for_each,只有lambda表达式一个参数。下面我们快速回顾一下lambda表达式的语法,以方便先前对lambda表达式没有了解的读者。

任何可以使用表达式的地方都能使用lambda表达式,通常lambda表达式会出现在赋值语句的右侧,或者作为函数调用参数出现。例如,下述代码可以把某向量v的所有内部元素都返回到标准输出上:

void print(int i)
{
   std::wcout << i << " ";
}
// . . .
std::vector v(5, 0);
// . . .
std::for_each(v.begin(), v.end(), print);`
尽管在上面的代码里,print()函数和使用print()函数的代码离得很近,但多数情况下它们的距离肯定不会那么近,这样可读性就会受到影响。在大型代码库中,保持小型辅助函数命名唯一性往往不会那么容易。更糟的是,函数实体会随时发生变化,但函数并不会重新命名。诸如此类的限制会使许多开发者只想使用for,但这种做法并不好,因为for_each算法实际上比for的表达能力要强,for_each可以做更多复杂的事情。

使用lambda表达式作为for_each调用的最后一个参数,我们就能把代码放到使用它的地方,这样我们就能直接了解代码的功能。这样也免去了对单行函数命名的烦扰,可以保持函数名以及函数体的准确性。下述代码是for_each调用的lambda表达:

std::for_each(v.begin(), v.end(), [](int i) { std::wcout << i << " ";});
所有的lambda表达式都会以[]作为前缀,但是方括号可能不会像本例一样为空。这被称为“捕获子句(capture clause)”。接下来,圆括号里是lambda的参数。针对每个向量元素,`for_
each算法都会调用一次lambda表达式,并将元素值传入lambda`表达式。最后,大括号里面是lambda表达式的内容。lambda表达式不需要写在单独的一行当中,它的写法并没有限制。它甚至还可以包含一个或多个return语句,从自身返回。当lambda没有return语句,甚至整个lambda只有一条return语句时,编译器能够自动推断出返回类型。其他情况都需要指定返回值类型,如下所示:

std::vector<int> v;
// . . .
std::vector<double> dv;
transform(v.begin(), v.end(), back_inserter(dv), [](int n) -> double
{
   if (n % 2 == 0) {
     return n * n * n;
   } else {
     return n / 2.0;
   }
});```
C++ AMP的parallel_for_each使用的lambda没有返回值,因此没有上述注解,在这里指定完全是为了完整性的需要。

lambda的参数是通过调用for_each()这样的代码传入的。lambda创建后作用域内的值都可以访问。lambda创建的同时,编译器会生成匿名函数对象,lambda作用域的变量取值可以保存在函数对象成员变量中。我们可以在捕获子句中指定哪些值是按值传递的:

int x, y;
// . . .
std::for_each(v.begin(), v.end(), x, y
{
  if (n >= x && n <= y)
    std::wcout << n << " ";
});`
也可以指定哪些值是按引用传递的:

int x, y;
// . . . std::for_each(v.begin(), v.end(), [&x, &y](int& r)
{
  const int old = r;
  r *= 2;
  x = y;
  y = old;
});```
可以使用捕获子句[=]指示编译器按值传入lambda作用体,也可以使用捕获子句[&]指示编译器按引用传入。甚至还可以将这两个子句组合在一起使用:

process(0, numItems, =, &y
{
   //use various values from calling scope
   // any changes to y in the lambda will be reflected in calling scope
});`

我们无需掌握关于C++ AMP代码如何使用lambda表达式的全部信息。现在只需要了解{}就足够了。只要了解捕获子句、lambda参数和lambda表达式作用体就行了。

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
21天前
|
存储 C++
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
【C++】Visual Studio C++ 配置并使用gtest(不好用你捶我)
|
29天前
|
算法 IDE Java
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
【软件设计师备考 专题 】面向对象程序设计语言:C++、Java、Visual Basic和Visual C++
41 0
|
29天前
|
Java API 开发工具
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(三)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
30 0
|
29天前
|
Java 数据处理 数据库
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(二)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
34 0
|
29天前
|
存储 算法 Java
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用(一)
【软件设计师备考 专题 】C、C++、Java、Visual Basic、Visual C++等语言的基础知识和应用
34 0
|
1月前
|
JSON 数据格式 C++
C++ JSON库 nlohmann::basic_json::array 的用法
C++ JSON库 nlohmann::basic_json::array 的用法
30 1
|
1月前
|
存储 缓存 安全
【C/C++ 基础 数组容器比较】深入探究C++容器:数组、vector与array之间的异同
【C/C++ 基础 数组容器比较】深入探究C++容器:数组、vector与array之间的异同
15 0
|
1月前
|
存储 C++ Python
C++版本netCDF在Visual Studio中的部署
【2月更文挑战第20天】本文介绍在Windows电脑的Visual Studio软件中,配置C++语言最新版netCDF库的方法~
C++版本netCDF在Visual Studio中的部署
|
1天前
|
C++
c++的学习之路:7、类和对象(3)
c++的学习之路:7、类和对象(3)
16 0
|
1天前
|
存储 编译器 C语言
c++的学习之路:5、类和对象(1)
c++的学习之路:5、类和对象(1)
12 0

热门文章

最新文章