目次

9. OpenCL

9.1 OpenCLとは

NVIDIAまたはAMDのグラフィックスボード(GPU)が搭載されたコンピュータでは、 その高い演算能力を汎用的な科学技術計算に用いることができます。 そのためのプログラミング言語をOpenCL(Open Computing Language)と呼びます。
CPUがフロントエンドとして動作し、計算の主要部はGPUが行います。
OpenCLプログラムはCPUやCPU内蔵GPUでも動作しますが性能上の利点は少なく主に外付GPUで使用します。

9.2 OpenCLプログラミング

OpenCLプログラム
OpenCLプログラムは通常のC/C++で記述されるホストプログラムと、 OpenCL C言語で記述されるカーネルプログラム(拡張子.cl)から成ります。
ホストプログラムはCPUで実行され、カーネルプログラムはGPUで実行されます。 両者の関係はCUDAと同じです。

OpenCL C言語
カーネルを記述するOpenCL C言語のC/C++との違いは以下の通りです。

  1. 関数修飾子に"__kernel"が必要です。
  2. カーネル関数の引数にはアドレス空間修飾子 ("__global","__local","__constant","_private"のいずれか)が必要です。 "__"は省略できます。指定しないときは"__private"になります。
  3. "__local"は共有メモリーになります。(CUDAの"__shared__"に対応)
  4. "__constant"は読み取り専用になります。(CUDAの"__constant__"に対応)
  5. 多数の数学関数が用意されています。
  6. ベクタ型(float4,int4など)が使えます。
  7. 環境によってサイズの変わる型(size_tなど)は使えません。
  8. #define文が使えます。
  9. #include文は使えません。従って必要部分をコピーする必要があります。 (コピー元が変更されたときは忘れずに修正することが必要です)

オンラインコンパイルとオフラインコンパイル
OpenCLプログラムにはオンラインコンパイルとオフラインコンパイルの2種類のモードがあります。 それぞれの意味は以下の通りです。

  1. オンラインコンパイル
    実行プログラムと一緒にカーネルのソースコードを配布する必要があります。 実行時にカーネルがコンパイルされます。プログラムの移植性は上がります。
  2. オフラインコンパイル
    開発時にカーネルをコンパイルしてリンクします。 カーネルのソースコードを配布する必要はありませんが、 動作環境ごとに実行プロラムを作成しテストする必要があります。
ここではオンラインコンパイルを使用します。
この場合カーネルに文法エラーがあると実行時にエラーが発生しますが、 エラー発生場所がわからないので開発効率が悪くなります。 そこで、IntelのSDK[16]に付属する"Intel Kernel Builder"を使用します。 図9-1に使用例を示します。カーネルファイルを開いた後、 [Build]→[Compile File]メニューをクリックします。 文法エラーがないときは左のように緑色になり、 文法エラーがあるときは右のように赤色になりエラー発生場所が表示されます。

図9-1 Intel Kernel Builder (左:コンパイル正常、右:コンパイルエラー)

OpenCLプログラミングの指針
OpenCLプログラミングでは以下の点が最も重要です。これはCUDAと同じです。
(1)並列計算できるアルゴリズムを採用する。
(2)CPU/GPU間のデータ転送を最小限にする。
(3)カーネルコードではメモリアクセスをスレッド順とする。
これらを満たさないときは速度は数分の一以下になりGPUを使う意味がなくなります。

変数の命名ルール
CPUコードとGPUコードの両方から呼ばれる同じ意味をもつ変数については、 前者の頭には何もつけないか"h_"(host memoryの意味)を付け、 後者の頭に"d_"(device memoryの意味)を付ける方法がよく用いられます。
このようにすればその変数がCPUにあるかGPUにあるか一目でわかります。
またカーネル変数の名前はカーネル関数の名前と1対1に対応させるとわかりやすくなります。

データ並列とタスク並列
並列処理にはデータ並列とタスク並列の2種類がありますが、数値計算では通常前者を使用します。 OpenCLではそのための関数がclEnqueueNDRangeKernel関数です。

ワークアイテム
OpenCLではスレッドのことをワークアイテムと呼び、スレッドの集合をグループと呼びます。 ワークアイテムの大きさはclEnqueueNDRangeKernel関数の引数で指定します。
表9-1にOpenCLとCUDAの対応関係を示します。 カーネル関数でこれらを通して自分のスレッド番号を取得します。 OpenCLではNo.5,No.6も関数が用意されており便利です。

表9-1 OpenCLとCUDAのスレッド関係の比較
No.OpenCL
ワークアイテム
CUDA
execution configuration
1get_local_id(0)
get_local_id(1)
get_local_id(2)
threadIdx.x
threadIdx.y
threadIdx.z
2get_local_size(0)
get_local_size(1)
get_local_size(2)
blockDim.x
blockDim.y
blockDim.z
3get_group_id(0)
get_group_id(1)
get_group_id(2)
blockIdx.x
blockIdx.y
blockIdx.z
4get_num_groups(0)
get_num_groups(1)
get_num_groups(2)
gridDim.x
gridDim.y
gridDim.z
5get_global_id(0)
get_global_id(1)
get_global_id(2)
threadIdx.x+blockIdx.x*blockDim.x
threadIdx.y+blockIdx.y*blockDim.y
threadIdx.z+blockIdx.z*blockDim.z
6get_global_size(0)
get_global_size(1)
get_global_size(2)
blockDim.x*gridDim.x
blockDim.y*gridDim.y
blockDim.z*gridDim.z

9.3 OpenCLプログラミング例(1)

ホストプログラム
リスト9-1にベクトルの和をOpenCLで並列計算するプログラムを示します。

リスト9-1 OpenCLホストプログラム(ocl_vadd_v1.c)


     1	/*
     2	ocl_vadd_v1.c (OpenCL, version 1)
     3	
     4	Compile + Link:
     5	> cl.exe /O2 ocl_vadd_v1.c OpenCL.lib
     6	
     7	Usage:
     8	> ocl_vadd_v1 <n> <loop> <platform> <device>
     9	*/
    10	
    11	#include <stdio.h>
    12	#include <stdlib.h>
    13	#include <time.h>
    14	#include <CL/cl.h>
    15	
    16	#define MAX_PLATFORMS (10)
    17	#define MAX_DEVICES (10)
    18	#define MAX_SOURCE_SIZE (100000)
    19	
    20	int main(int argc, char **argv)
    21	{
    22		// OpenCL
    23		cl_context       context       = NULL;
    24		cl_command_queue command_queue = NULL;
    25		cl_program       program       = NULL;
    26		cl_kernel        kernel        = NULL;
    27		cl_platform_id   platform_id[MAX_PLATFORMS];
    28		cl_device_id     device_id[MAX_DEVICES];
    29	
    30		// memory object
    31		cl_mem d_a = NULL;
    32		cl_mem d_b = NULL;
    33		cl_mem d_c = NULL;
    34	
    35		FILE *fp;
    36		char *source_str;
    37		size_t source_size;
    38		size_t global_item_size, local_item_size;
    39		size_t ret_size;
    40		cl_uint num_platforms;
    41		cl_uint num_devices;
    42		cl_int ret;
    43		char str[BUFSIZ];
    44	
    45		cl_uint platform = 0;
    46		cl_uint device = 0;
    47		int nloop = 1000;
    48		int n = 1000;
    49	
    50		// arguments
    51		if (argc >= 5) {
    52			n        = atoi(argv[1]);
    53			nloop    = atoi(argv[2]);
    54			platform = atoi(argv[3]);
    55			device   = atoi(argv[4]);
    56		}
    57	
    58		// alloc
    59		source_str = (char *)malloc(MAX_SOURCE_SIZE * sizeof(char));
    60	
    61		// setup host arrays
    62		float *a = (float *)malloc(n * sizeof(float));
    63		float *b = (float *)malloc(n * sizeof(float));
    64		float *c = (float *)malloc(n * sizeof(float));
    65		for (int i = 0; i < n; i++) {
    66			a[i] = (float)(1 + i);
    67			b[i] = (float)(1 + i);
    68		}
    69	
    70		// platform
    71		clGetPlatformIDs(MAX_PLATFORMS, platform_id, &num_platforms);
    72		if (platform >= num_platforms) {
    73			printf("error : platform = %d (limit = %d)\n", platform, num_platforms - 1);
    74			exit(1);
    75		}
    76	
    77		// device
    78		clGetDeviceIDs(platform_id[platform], CL_DEVICE_TYPE_ALL, MAX_DEVICES, device_id, &num_devices);
    79		if (device >= num_devices) {
    80			printf("error : device = %d (limit = %d)\n", device, num_devices - 1);
    81			exit(1);
    82		}
    83	
    84		// device name (option)
    85		clGetDeviceInfo(device_id[device], CL_DEVICE_NAME, sizeof(str), str, &ret_size);
    86		printf("%s\n", str);
    87	
    88		// context
    89		context = clCreateContext(NULL, 1, &device_id[device], NULL, NULL, &ret);
    90	
    91		// command queue
    92		command_queue = clCreateCommandQueue(context, device_id[device], 0, &ret);
    93	
    94		// source
    95		if ((fp = fopen("vadd.cl", "r")) == NULL) {
    96			fprintf(stderr, "kernel source open error\n");
    97			exit(1);
    98		}
    99		source_size = fread(source_str, 1, MAX_SOURCE_SIZE, fp);
   100		fclose(fp);
   101	
   102		// program
   103		program = clCreateProgramWithSource(context, 1, (const char **)&source_str, (const size_t *)&source_size, &ret);
   104		if (ret != CL_SUCCESS) {
   105			fprintf(stderr, "clCreateProgramWithSource() error\n");
   106			exit(1);
   107		}
   108	
   109		// build
   110		if (clBuildProgram(program, 1, &device_id[device], NULL, NULL, NULL) != CL_SUCCESS) {
   111			fprintf(stderr, "clBuildProgram() error\n");
   112			exit(1);
   113		}
   114	
   115		// kernel
   116		kernel = clCreateKernel(program, "vadd", &ret);
   117		if (ret != CL_SUCCESS) {
   118			fprintf(stderr, "clCreateKernel() error\n");
   119			exit(1);
   120		}
   121	
   122		// memory object
   123		d_a = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret);
   124		d_b = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret);
   125		d_c = clCreateBuffer(context, CL_MEM_READ_WRITE, n * sizeof(float), NULL, &ret);
   126	
   127		// host to device
   128		clEnqueueWriteBuffer(command_queue, d_a, CL_TRUE, 0, n * sizeof(float), a, 0, NULL, NULL);
   129		clEnqueueWriteBuffer(command_queue, d_b, CL_TRUE, 0, n * sizeof(float), b, 0, NULL, NULL);
   130	
   131		// args
   132		clSetKernelArg(kernel, 0, sizeof(cl_mem), (void *)&d_a);
   133		clSetKernelArg(kernel, 1, sizeof(cl_mem), (void *)&d_b);
   134		clSetKernelArg(kernel, 2, sizeof(cl_mem), (void *)&d_c);
   135		clSetKernelArg(kernel, 3, sizeof(int),    (void *)&n);
   136	
   137		// timer
   138		clock_t t0 = clock();
   139	
   140		// work item
   141		local_item_size = 256;
   142		global_item_size = ((n + local_item_size - 1) / local_item_size) * local_item_size;
   143	
   144		// run
   145		for (int loop = 0; loop < nloop; loop++) {
   146			clEnqueueNDRangeKernel(command_queue, kernel, 1, NULL, &global_item_size, &local_item_size, 0, NULL, NULL);
   147		}
   148	
   149		// device to host
   150		clEnqueueReadBuffer(command_queue, d_c, CL_TRUE, 0, n * sizeof(float), c, 0, NULL, NULL);
   151	
   152		// timer
   153		clock_t t1 = clock();
   154		double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC;
   155	
   156		// output
   157		double sum = 0;
   158		for (int i = 0; i < n; i++) {
   159			sum += c[i];
   160		}
   161		double exact = n * (n + 1.0);
   162		printf("n=%d nloop=%d %e(%.6e) cpu[sec]=%.3f\n",
   163			n, nloop, sum, exact, cpu);
   164	
   165		// release
   166		clFlush(command_queue);
   167		clFinish(command_queue);
   168		clReleaseMemObject(d_a);
   169		clReleaseMemObject(d_b);
   170		clReleaseMemObject(d_c);
   171		clReleaseKernel(kernel);
   172		clReleaseProgram(program);
   173		clReleaseCommandQueue(command_queue);
   174		clReleaseContext(context);
   175	
   176		// free
   177		free(source_str);
   178		free(a);
   179		free(b);
   180		free(c);
   181	
   182		return 0;
   183	}

ソースコードの説明
14行目:OpenCLプログラムにはこのinclude文が必須です。
70-120行目:OpenCLに必須の前処理です。OpenCLはいろいろな環境で動くためにこのような処理が必要になります。 コードが煩雑になりますが定型的な処理です。
platform→device→context→command queue→カーネルソースコード→program→build→kernelの順に処理します。
78行目:"CL_DEVICE_TYPE_ALL"を"CL_DEVICE_TYPE_GPU"に変えるとGPUだけが対象デバイスになります。
123-125行目:GPUで使用する配列を確保します。(CUDAのcudaMallocに対応)
128-129行目:ホストからデバイスにメモリーを転送します。(CUDAのcudaMemcpy(...,cudaMemcpyHostToDevice)に対応)
132-135行目:カーネルの引数を代入します。(CUDAにない機能)
141-142行目:ワークアイテム(スレッドの構成)を指定します。global_item_sizeはlocal_item_sizeの整数倍であることが必要です。(CUDAのExecution configurationに対応)
150行目:デバイスからホストにメモリーを転送します。(CUDAのcudaMemcpy(...,cudaMemcpyDeviceToHost)に対応)
168-170行目:GPUで使用した配列を解放します。(CUDAのcudaFreeに対応)

カーネルプログラム
リスト9-2にカーネルプログラムを示します。
オンラインコンパイルではカーネルプログラムを実行プログラムと同じフォルダに置きます。

リスト9-2 OpenCLカーネルプログラム(vadd.cl)


     1	__kernel void vadd(global const float *a, global const float *b, global float *c, int n)
     2	{
     3		int i = get_global_id(0);
     4	
     5		if (i < n) {
     6			c[i] = a[i] + b[i];
     7		}
     8	}

ソースコードの説明
3行目:globalなスレッド番号を取得します。それがこの場合配列のインデックスになります。
5行目:配列の大きさはスレッド数の倍数とは限らないのでこの条件判定が必要です。

9.4 OpenCLプログラミング例(2)

プログラミングの改良
上で示したプログラムはOpenCLを用いた最小のプログラムですが、 大規模なアプリケーションを開発するための出発点としては不適切です。
すわなちOpenCL固有の処理とその他の処理が分離されておらず、 また計算アルゴリズムを検証するための逐次計算が実装されていないために開発効率が悪くなります。
その点を考慮したプログラムをリスト9-3に示します。 なお、カーネルプログラムはリスト9-2と共通です。

リスト9-3 OpenCLホストプログラム(ocl_vadd_v2.c)


     1	/*
     2	ocl_vadd_v2.c (OpenCL, version 2)
     3	
     4	Compile + Link:
     5	> cl.exe /O2 ocl_vadd_v2.c OpenCL.lib
     6	
     7	Usage:
     8	> ocl_vadd_v2 <n> <loop> <platform> <device>
     9	*/
    10	
    11	#include <stdio.h>
    12	#include <stdlib.h>
    13	#include <string.h>
    14	#include <time.h>
    15	
    16	#ifdef __APPLE__
    17	#include <OpenCL/opencl.h>
    18	#else
    19	#include <CL/cl.h>
    20	#endif
    21	
    22	#define MAX_PLATFORMS (10)
    23	#define MAX_DEVICES (10)
    24	#define MAX_SOURCE_SIZE (100000)
    25	
    26	// prototypes
    27	static int setup_ocl(cl_uint, cl_uint, char *);
    28	static void vadd_calc(void);
    29	static void vadd(void);
    30	
    31	// globals
    32	cl_command_queue Queue;
    33	cl_kernel k_vadd;
    34	int N;
    35	float *A, *B, *C;
    36	cl_mem d_A, d_B, d_C;
    37	int OCL;
    38	
    39	int main(int argc, char **argv)
    40	{
    41		int platform = 0;
    42		int device = 0;
    43		int nloop = 1000;
    44	
    45		// arguments
    46		N = 1000;
    47		if (argc >= 5) {
    48			N        = atoi(argv[1]);
    49			nloop    = atoi(argv[2]);
    50			platform = atoi(argv[3]);
    51			device   = atoi(argv[4]);
    52		}
    53		OCL = (platform >= 0);
    54	
    55		// alloc host arrays
    56		size_t size = N * sizeof(float);
    57		A = (float *)malloc(size);
    58		B = (float *)malloc(size);
    59		C = (float *)malloc(size);
    60	
    61		// setup problem
    62		for (int i = 0; i < N; i++) {
    63			A[i] = (float)(1 + i);
    64			B[i] = (float)(1 + i);
    65		}
    66	
    67		// setup OpenCL
    68		if (OCL) {
    69			char msg[BUFSIZ];
    70			int ret = setup_ocl((cl_uint)platform, (cl_uint)device, msg);
    71			printf("%s\n", msg);
    72			if (ret) {
    73				exit(1);
    74			}
    75		}
    76	
    77		// timer
    78		clock_t t0 = clock();
    79	
    80		// copy host to device
    81		if (OCL) {
    82			clEnqueueWriteBuffer(Queue, d_A, CL_TRUE, 0, size, A, 0, NULL, NULL);
    83			clEnqueueWriteBuffer(Queue, d_B, CL_TRUE, 0, size, B, 0, NULL, NULL);
    84		}
    85	
    86		// run
    87		for (int loop = 0; loop < nloop; loop++) {
    88			vadd_calc();
    89		}
    90	
    91		// copy device to host
    92		if (OCL) {
    93			clEnqueueReadBuffer(Queue, d_C, CL_TRUE, 0, size, C, 0, NULL, NULL);
    94		}
    95	
    96		// timer
    97		clock_t t1 = clock();
    98		double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC;
    99	
   100		// sum
   101		double sum = 0;
   102		for (int i = 0; i < N; i++) {
   103			sum += C[i];
   104		}
   105	
   106		// output
   107		double exact = N * (N + 1.0);
   108		printf("N=%d L=%d %.6e(%.6e) %.1e %.3f[sec]\n",
   109			N, nloop, sum, exact, fabs((sum - exact) / exact), cpu);
   110	
   111		// release
   112		if (OCL) {
   113			clReleaseMemObject(d_A);
   114			clReleaseMemObject(d_B);
   115			clReleaseMemObject(d_C);
   116			clReleaseKernel(k_vadd);
   117			clReleaseCommandQueue(Queue);
   118		}
   119	
   120		// free
   121		free(A);
   122		free(B);
   123		free(C);
   124	
   125		return 0;
   126	}
   127	
   128	// setup OpenCL
   129	static int setup_ocl(cl_uint platform, cl_uint device, char *msg)
   130	{
   131		cl_context     context = NULL;
   132		cl_program     program = NULL;
   133		cl_platform_id platform_id[MAX_PLATFORMS];
   134		cl_device_id   device_id[MAX_DEVICES];
   135	
   136		FILE *fp;
   137		char *source_str;
   138		char str[BUFSIZ];
   139		size_t source_size, ret_size, size;
   140		cl_uint num_platforms, num_devices;
   141		cl_int ret;
   142	
   143		// alloc
   144		source_str = (char *)malloc(MAX_SOURCE_SIZE * sizeof(char));
   145	
   146		// platform
   147		clGetPlatformIDs(MAX_PLATFORMS, platform_id, &num_platforms);
   148		if (platform >= num_platforms) {
   149			sprintf(msg, "error : platform = %d (limit = %d)", platform, num_platforms - 1);
   150			return 1;
   151		}
   152	
   153		// device
   154		clGetDeviceIDs(platform_id[platform], CL_DEVICE_TYPE_ALL, MAX_DEVICES, device_id, &num_devices);
   155		if (device >= num_devices) {
   156			sprintf(msg, "error : device = %d (limit = %d)", device, num_devices - 1);
   157			return 1;
   158		}
   159	
   160		// device name (option)
   161		clGetDeviceInfo(device_id[device], CL_DEVICE_NAME, sizeof(str), str, &ret_size);
   162		sprintf(msg, "%s (platform = %d, device = %d)", str, platform, device);
   163	
   164		// context
   165		context = clCreateContext(NULL, 1, &device_id[device], NULL, NULL, &ret);
   166	
   167		// command queue
   168		Queue = clCreateCommandQueue(context, device_id[device], 0, &ret);
   169	
   170		// source
   171		if ((fp = fopen("vadd.cl", "r")) == NULL) {
   172			sprintf(msg, "kernel source open error");
   173			return 1;
   174		}
   175		source_size = fread(source_str, 1, MAX_SOURCE_SIZE, fp);
   176		fclose(fp);
   177	
   178		// program
   179		program = clCreateProgramWithSource(context, 1, (const char **)&source_str, (const size_t *)&source_size, &ret);
   180		if (ret != CL_SUCCESS) {
   181			sprintf(msg, "clCreateProgramWithSource() error");
   182			return 1;
   183		}
   184	
   185		// build
   186		if (clBuildProgram(program, 1, &device_id[device], NULL, NULL, NULL) != CL_SUCCESS) {
   187			sprintf(msg, "clBuildProgram() error");
   188			return 1;
   189		}
   190	
   191		// kernel
   192		k_vadd = clCreateKernel(program, "vadd", &ret);
   193		if (ret != CL_SUCCESS) {
   194			sprintf(msg, "clCreateKernel() error");
   195			return 1;
   196		}
   197	
   198		// memory object
   199		size = N * sizeof(float);
   200		d_A = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
   201		d_B = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
   202		d_C = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &ret);
   203	
   204		// release
   205		clReleaseProgram(program);
   206		clReleaseContext(context);
   207	
   208		// free
   209		free(source_str);
   210	
   211		return 0;
   212	}
   213	
   214	// entry point
   215	static void vadd_calc(void)
   216	{
   217		if (OCL) {
   218			size_t global_item_size, local_item_size;
   219	
   220			// args
   221			clSetKernelArg(k_vadd, 0, sizeof(cl_mem), (void *)&d_A);
   222			clSetKernelArg(k_vadd, 1, sizeof(cl_mem), (void *)&d_B);
   223			clSetKernelArg(k_vadd, 2, sizeof(cl_mem), (void *)&d_C);
   224			clSetKernelArg(k_vadd, 3, sizeof(int),    (void *)&N);
   225	
   226			// work item
   227			local_item_size = 256;
   228			global_item_size = ((N + local_item_size - 1) / local_item_size)
   229			                 * local_item_size;
   230	
   231			// run
   232			clEnqueueNDRangeKernel(Queue, k_vadd, 1, NULL, &global_item_size, &local_item_size, 0, NULL, NULL);
   233		}
   234		else {
   235			// serial code
   236			vadd();
   237		}
   238	}
   239	
   240	// serial code
   241	static void vadd(void)
   242	{
   243		for (int i = 0; i < N; i++) {
   244			C[i] = A[i] + B[i];
   245		}
   246	}

ソースコードの説明
32-37行目:複数の関数から呼ばれる変数(プログラムの核をなす変数)をグローバル変数にします。
50行目:3番目の引数に負の値を代入するとOpenCLを使用しない逐次計算プログラムになります。
68-75行目:OpenCL固有の前処理は一か所に集めます。
215行目:この関数が計算の入り口になります。この中で並列処理と逐次処理を場合分けしています。
221-224行目:引数の内容は計算の途中で変わることがありますのでカーネルを呼び出す直前に設定します。
227-229行目:ここではワークアイテムをカーネルを呼び出す直前に設定していますが、 これは計算の途中で変わることはありませんので外部で設定しても構いません。
241-246行目:逐次コードを先に作成して十分テストし、計算式が正しく実装されていることを確認します。

逐次版と並列版の開発
プログラムの最終目的が並列計算であってもいきなり並列計算プログラムを実装してもうまく動くことはほとんどありません。
最初に並列化を想定したプログラム構造の逐次版を作成し、 計算式が正しく実装されていることを十分確認してから並列版を実装することが開発の近道です。
並列版固有の処理は以下の通りです。 これらはすべて定型的な作業なので注意深く実装すれば間違いは少なくなります。

  1. OpenCLの前処理、CommandQueueとカーネル変数の作成まで
  2. メモリーオブジェクトの作成
  3. ホスト→デバイス間とデバイス→ホスト間のメモリーコピー
  4. カーネル引数の代入
  5. ワークアイテムの作成(これは計算時間に影響するのでいろいろ変えてテストする必要があります)
  6. 逐次版関数を参考にカーネル関数を作成(オフラインコンパイラで文法チェック)

9.5 コンパイル・リンク・実行

コンパイル・リンク方法
コンパイル・リンク方法は以下の通りです(VC++の場合)。
> cl.exe /O2 ocl_vadd_v2.c OpenCL.lib
warningが出たときは以下のようにしてください。
> cl.exe /O2 /wd4996 /wd4201 ocl_vadd_v2.c OpenCL.lib
ここで、OpenCL.libはCUDAをインストールしたときにCUDAと同じ場所に保存されています。

プログラムの実行方法
プログラムの実行方法は以下の通りです。
> ocl_vadd_v2.exe 配列の大きさ 繰り返し回数 プラットフォーム番号 デバイス番号
例えば以下のようになります。
> ocl_vadd_v2.exe 1000000 1000 1 0 (プラットフォーム番号=1, デバイス番号=0のとき)
> ocl_vadd_v2.exe 1000000 1000 -1 0 (OpenCLを用いないで逐次計算するとき)
繰り返し回数は計算時間の測定誤差を小さくするためです。
プラットフォーム番号とデバイス番号については表9-3を参考にしてください。
プログラム実行時に C:\Windows\System32\OpenCL.dll を使用しています。

9.6 OpenCLの計算時間

OpenCLプログラムはCPU/CPU内蔵GPU/外付GPUで計算することができます。 (以下では、CPU内蔵GPUをiGPU、外付GPUをdGPUと呼びます)[21]
計算するハードウェアはOpenCLのプラットフォーム番号とデバイス番号によって選択することができます。
表9-2にベクトル和の計算時間を示します。
配列の大きさ(=N)と繰り返し回数(=L)の積は一定(=1010)です。 従って全体の演算量は同じです。
dGPUではNo.1-2のとき計算時間が大幅に短縮されますが、 No.3-4でカーネル起動回数が増えるために計算時間が増えます。 これはCUDAと同じです。
リスト9-3よりカーネル起動回数は繰り返し回数と同じです。 繰り返し回数が1,000,000回のときdGPUで約7秒余分に時間がかかっています。 これからdGPUのカーネル起動のオーバーヘッドは約7μsecと評価することができます。 (通常のアプリケーションではカーネルをこのように多数回呼ぶことは少ないです)
iGPUはCPU1コアより速いですが、 CPUは通常マルチコアで並列計算するのでiGPUでOpenCLプログラムを使用する理由は特にありません。 (iGPUはメモリー容量も小さい)
表9-2の外付GPUの計算時間は表8-3のGPUとほぼ同じです。 すなわちCUDAとOpenCLの性能はほぼ同じと言えます。

表9-2 ベクトル和の計算時間
(OpenCL、Windows、()内はCPU1コアとの速度比)
No.配列の大きさN繰り返し回数LCPU1コアCPU内蔵GPU
(iGPU)
外付GPU
(dGPU)
110,000,0001,0005.79秒 (1.0)2.85秒 (2.0)0.56秒 (10.3)
21,000,00010,0007.80秒 (1.0)2.85秒 (2.7)0.53秒 (14.7)
3100,000100,000 3.81秒 (1.0)2.62秒 (1.5)0.93秒 (4.1)
410,0001,000,0003.78秒 (1.0)2.09秒 (1.8)8.03秒 (0.5)

9.7 OpenCLのデバイスについて

表9-3に本環境のプラットフォーム番号とデバイス番号を示します。
プラットフォーム番号とデバイス番号は環境によって変わるので、 いろいろ変えて実行し表示されるデバイス名から番号を決定してください。
外付GPUが2個以上あるときは、デバイス番号が順に0,1,...となります。

表9-3 OpenCLのプラットフォーム番号とデバイス番号
No.ハードウェアプラットフォーム番号デバイス番号
0CPU--
1CPU内蔵GPU00
2外付GPU10