Chapter 4 Programming with OpenCL C
Qualifiers - 修饰符
Function Qualifiers - 函数修饰符
kernel void
parallel_add(global float *a, global float *b, global float *result)
{
...
// The following example is an example of an illegal kernel
// declaration and will result in a compile-time error.
// The kernel function has a return type of int instead of void.
kernel int
parallel_add(global float *a, global float *b, global float *result)
{
...
}
- 返回值必须是void,否则会产生编译错误。
- host可以通过将在队列中添加一个执行该kernel函数的指令,从而在一个设备上执行这个kernel。
- 如果该函数被一个kernel函数调用,那么它的表现和一般的函数是一样的。唯一的限制是:内部用local修饰符来申明的变量的kernel函数不能被其他kernel函数调用。
kernel void
my_func_a(global float *src, global float *dst)
{
local float l_var[32];
...
}
kernel voidmy_func_b(global float * src, global float *dst){
my_func_a(src, dst); // implementation-defined behavior
}
为了适用于所有实现,代码进行了修改,将local变量作为参数传给kernel函数:
kernel void
my_func_a(global float *src, global float *dst, local float *l_var)
{
...
}
kernel voidmy_func_b(global float * src, global float *dst, local float *l_var){
my_func_a(src, dst, l_var);
}
Kernel Attribute Qualifiers
修饰符kernel可以和关键字__attribute__一起使用,用来说明关于kernel的一些附加信息:- __attribute__((work_group_size_hint(X, Y, Z))):提示编译器,说明最有可能的work-group的大小,也就是在local_work_size作为参数给clEnqueueNDRangeKernel的值。
- __attribute__((reqd_work_group_size(XYZ))):说明要使用的work-group的大小,也就是在local_work_size作为参数给clEnqueueNDRangeKernel的值。这样编译器就可以根据已知的work-group大小进行优化。
- __atrribute__((vec_type_hint(<type>))):告诉编译器kernel的宽度,也就是kernel所操作的数据类型的大小。这个是针对自动向量化的编译器的提示。默认的<type>是int,指明kernel本来是标量的,自动向量化程序就能通过向量单元的SIMD通道,为多个work-item把代码向量化。
Address Space Qualifiers - 地址空间修饰符
// declares a pointer p in the private address space that points to
// a float object in address space global
global float *p;
// declares an array of integers in the private address space
int f[4];
// for my_func_a function we have the following arguments:
//
// src - declares a pointer in the private address space that
// points to a float object in address space constant
//
// v - allocate in the private address space
//
int
my_func_a(constant float *src, int4 v)
{
float temp; // temp is allocated in the private address space.
}
如果kernel函数的参数是一个指针,那么这个指针必须指向以下的地址空间:global,local,或者constant。如果它没有指明,会发生编译错误。这个限制不适用于非kernel函数。
kernel void my_func(int *p) // illegal because generic address space
// name for p is private.
kernel void
my_func(private int *p) // illegal because memory pointed to by
// p is allocated in private.
void
my_func(int *p) // generic address space name for p is private.
// legal as my_func is not a kernel function
void
my_func(private int *p) // legal as my_func is not a kernel function
Globle Address Space - 全局地址空间
这个地址空间名用来说明分配在全局存储区域(global memory region)的存储对象(memory object,包括缓冲区buffer和图像image)。
kernel的所有work-group的所有work-item可以在这个存储区域进行读写。这个地址空间通过修饰符
global来指明。
global float4 *color; // an array of float4 elements
typedef struct {
float3 a;
int2 b[2];
} foo_t;
global foo_t *my_info; // an array of foo_t elements
指向全局地址空间的指针可以作为函数包括kernel函数)的参数,也可以在函数内部申明变量。在函数内部的变量不能分配在全局的地址空间中。
void
my_func(global float4 *vA, global float4 *vB)
{
global float4 *p; // legal
global float4 a; // illegal
}
Constant Address Space - 常量地址空间
这个地址空间名用来描述在全局存储中的、可以在kernel中访问的、只读变量。这个存储区在所有执行的kernel中的所有work-group中的所有work-item的只读访问。
图像类型不能分配在常量地址空间中。下面的例子将图像定义在常量地址空间中,这是非法的,将会导致编译错误。
kernel void
my_func(constant image2d_t imgA)
{
...
}
指向常量地址空间的指针可以作为函数的参数,也可以在函数内部定义变量。
在kernel函数内部(函数作用域的最外层)的变量可以分配到常量地址空间。在program范围的变量只能被分配到常量地址空间。所有这些变量都需要被初始化,所有用于初始化的值都必须是编译是确定的常量。对这些变量进行写操作将会产生编译错误。
在program中申明的字符串也会存储在常量地址空间。
下面是一些例子。
// legal - program scope variables can be allocated only
// in the constant address space
constant float wtsA[] = { 0, 1, 2, . . . }; // program scope
// illegal - program scope variables can be allocated only
// in the constant address space
global float wtsB[] = { 0, 1, 2, . . . };
kernel void
my_func(constant float4 *vA, constant float4 *vB)
{
constant float4 *p = vA; // legal
constant float a; // illegal – not initialized
constant float b = 2.0f; // legal – initialized with a compile-time constant
p[0] = (float4)(1.0f);
// illegal – p cannot be modified
// the string "opencl version" is allocated in the
// constant address space
char *c = "opencl version";
}
注意:一个kernel使用的、常量地址空间中申明的变量的数量是受设备的CL_DEVICE_MAX_CONSTANT_ARGS参数限制的。OpenCL 1.1 要求所有实现必须支持的最小值为8。也就是说,一个kernel中使用在常量地址空间中的变量不大于8个时,程序在所有实现上都会正常的运行。这8个参数的大小由CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE给出,并被设置为64KB。这样就可以把多个常量声明混合到一个常量buffer中,只要所有的大小不超过CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE即可。这中将多个变量聚集到一起声明到常量地址空间的做法不是必须的实现,并不是所有的OpenCL实现都支持它。为了程序的通用性,开发者应该假定变量不能聚集到一个单独的常量缓冲区中。
Local Address Space - 局部地址空间
这个地址空间的名字描述的是分配在local memory中的变量,它再同一个work-group中的所有work-item共享,但不能一个kernel中的多个work-group之间共享。一个work-group的所有work-item可以对其进行读写操作。
local memory非常类似于用户管理缓冲区。如果一个work-item或者一个work-group中的多个work-item从global memory中的同一个地方读取数据,使用local memory会极大地改善性能。比如对一个图像进行高斯滤波,多个work-item读取图像的重叠区域。重叠区域的大小是有滤波宽度决定的。比起多次从global memory读取多次的方法,一次从global memory中读入到local memory,然后再由多个work-item从local memory分别读取,这样会极大的改善性能。
指向局部地址空间的指针可以作为函数的参数,可以作为函数内部的变量。
在一个kernel中的申明的变量能够分配在局部地址空间中,但是有一些限制:
- 这些变量申明必须在kernel函数的范围
- 这些变量不能被初始化。
需要注意的是:在局部地址空间中的变量作为指针变量传给kernel函数时,或者是在kernel函数内部时,这些变量的生命周期和work-group一样。
一些例子
kernel void
my_func(global float4 *vA, local float4 *l)
{
local float4 *p;// legal
local float4 a; // legal
a = 1;
local float4 b = (float4)(0); // illegal – b cannot be initialized
if (...)
{
local float c; // illegal – must be allocated at kernel function scope
...
}
}
Private Address Space - 私有地址空间
这个地址空间中的变量只在单个的work-item中使用,不能再work-item之间共享。在一个kernel函数中、没有被地址空间修饰词说明的变量,所有在非kernel函数中申明的变量,和所有函数参数均是在私有的地址空间中。
Casting between Address Space - 地址空间之间的转换
一个地址空间的指针只能被同一个地址空间的指针赋值。将一个地址空间中的指针转换为另一个地址空间的指针是不合法的。比如:
kernel void
my_func(global float4 *particles)
{
// legal – particle_ptr & particles are in the
// same address space
global float *particle_ptr = (global float *)particles;
// illegal – private_ptr and particle_ptr are in different
// address spaces
float *private_ptr = (float *)particle_ptr;
}
Access Qualifiers - 访问修饰符
访问修饰符可以用来指明图像参数,是只读的(read_only)或者是只写(write_only)的。这是因为当前GPUs的一个限制:不允许一个kernel中对同一图像的读和写。因为读取的图像是被缓冲到一个纹理缓冲区中,而对图像的写操作并不更新这个纹理缓冲区。
下面的例子中imageA是只读的2D图像对象,而imageB是只写的图像对象。
kernel void
my_func(read_only image2d_t imageA, write_only image2d_t imageB)
{
...
}
用read_only修饰符修饰的图像可以被内建的读取图像的函数使用,但是不能被内建的写图像的函数使用。类似的,用write_only修饰的图像只能写而不能读。可以参见下面的例子:
kernel void
my_func(read_only image2d_t imageA, write_only image2d_t imageB, sampler_t sampler)
{
float4 clr;
float2 coords;
clr = read_imagef(imageA, sampler, coords); // legal
clr = read_imagef(imageB, sampler, coords); // illegal
write_imagef(imageA, coords, &clr); // illegal
write_imagef(imageB, coords, &clr); // legal
}
imageA是只读的独享,它不能传给write_imagef。类似的,imageB被申明为只写的图像,不能作为参数穿给read_imagef。
读写修饰符read_write是被保留的。使用这个操作符会导致编译时错误。