65.9K
CodeProject 正在变化。 阅读更多。
Home

第七部分:OpenCL插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (5投票s)

2012年2月13日

CPOL

13分钟阅读

viewsIcon

37472

本文将演示如何创建 C/C++ 插件,这些插件可以在运行时动态加载,从而为正在运行的应用程序添加大规模并行 OpenCL 功能。

本系列关于可移植并行计算的第 6 部分(使用 OpenCL™)介绍了如何在单个应用程序中混合 OpenCL™ 计算和 OpenGL 渲染。 本示例源代码中使用了 OpenGL 3.1 标准的一个新增功能——基元重置,通过在 GPU 上进行计算和渲染数据,从而极大地提高了性能。这避免了通过 PCIe 总线进行数据传输,并突出了 GPU 的性能。

本文将演示如何创建 C/C++ 插件,这些插件可以在运行时动态加载,从而为正在运行的应用程序添加大规模并行 OpenCL 功能。通过共享对象或 DLL(动态链接库)动态加载模块是许多应用程序(尤其是在商业市场中)的一种流行设计模式。了解如何在动态加载的运行时环境中使 OpenCL 的开发人员能够创建插件,通过编写利用 OpenCL 的新插件,可以将现有应用程序的性能提高一个数量级或更多。

正如本系列第一部分中所讨论的,OpenCL 应用程序内核是用 ISO C99 C 语言规范的一个变体编写的。这些内核通过运行时 OpenCL 编译器在运行时为目标设备进行编译。从本系列之前的文章中我们知道,OpenCL 已经创建并动态加载了依赖于设备的用于正在运行的应用程序的代码。这种动态编译功能非常适合用作插件环境,但当需要某些顺序操作来支持大规模并行 OpenCL 内核、在遗留插件框架内工作,或者开发人员希望使用多个 OpenCL 设备时,情况并非如此。出于这些原因,本教程将演示如何创建动态加载到应用程序中的 C/C++ 插件。然后,这些插件可以创建和加载大规模并行 OpenCL 内核。

展望未来,本系列的下一篇文章将把这种插件功能扩展到通过一个通用的“即插即用工具”框架将 OpenCL 整合到异构工作流中,该框架可以在单个工作站、机器网络或云计算框架内流式传输任意消息(向量、数组和任意复杂的嵌套结构)。创建可伸缩工作流的能力很重要,因为对于许多问题而言,数据处理和转换可能与用于产生期望结果的计算问题一样复杂。

读者应该注意到,动态编译的 OpenCL 插件和内核也为基于问题参数的高度优化内核生成提供了可能性。可以在互联网上找到许多相关的论文和示例。其中有两个示例是演示文稿“面向局部性和并行性管理的自动 OpenCL 优化”和论文“从高级表示自动生成和调整稀疏矩阵-向量乘法的 GPU 代码”。

库和插件的 OpenCL

大多数程序员都熟悉使用库。 库是包含在单个文件中的方法和函数的集合。按照惯例,库通常表示为库名后跟 Linux 下的 .a 或 Windows 下的 .lib。库被大多数程序员定期使用,因为它们允许以模块化的方式共享和更改代码。在构建可执行文件时的编译阶段,编译器必须注意对任何外部库方法或函数的调用。在后续步骤中,链接器负责完成对任何未解析引用的解析,以创建可以在计算机上运行的可执行文件。

链接到外部方法可以发生在

在创建可执行文件时进行静态链接:静态链接意味着在构建可执行文件时解析所有引用。此外,可执行文件包含运行程序使用的所有库函数的显式机器代码。

在加载时动态链接:加载时动态链接发生在可执行文件加载到内存时。与静态链接一样,可执行文件中的所有符号都在程序启动时通过链接一个或多个 .dll(Windows)或 .so 文件(Linux)来解析。这种链接形式提供了固定的功能(可以认为是 C 运行时库和其他常用库)。加载时链接的一个主要优点是,所有在加载时链接库的应用程序都将受益于错误修复和性能改进,只需在共享位置安装修订的库文件即可。无需重新编译或重新链接任何应用程序即可使用改进的库代码。此外,共享库可以保持单个可执行文件大小的小巧——对于常用库而言,这种成本节省会成倍增加。

在运行时动态链接:运行时链接用于提供加载插件的功能,这使得在不重新编译应用程序的情况下添加通用功能成为可能。因此,应用程序可以调用一个通用的外部函数 func(),其功能完全取决于应用程序加载的插件。如前所述,应用程序可以(当编译器可用时)通过生成特定于问题的源代码来编写和编译插件,然后将该插件编译并链接到正在运行的应用程序中。这种技巧使应用程序开发人员能够在提供特定问题参数的情况下,针对通用问题领域创建高度优化的函数。许多科学应用程序利用此功能来显著提高性能。

有关使用库、DLL(动态链接库)和共享对象文件的优势的更多信息,请参阅“Linux 静态、共享动态和可加载库”或Wikipedia 关于 DLL 的一般讨论

以下是一个简单的 C 语言程序,它调用一个通用的外部函数 func(),并打印由该通用函数创建的 x 的值。此简单源代码中演示了 init(), func(), fini() 框架(类似于 C++ 对象构造函数、计算方法和析构函数),以提供额外的通用性。这是插件编程中的常见设计模式,因为 init() 方法允许程序员执行任何初始化,而 fini() 方法允许程序员执行最终处理和清理。此外,还调用了 C 库 printf() 函数。

#include <stdio.h>
extern int func(int *);

int main()
{
   int x;
   init();
 
   func(&x);
   printf("Example of static linking\n");
   printf("Valx=%d\n",x);
 
   fini();
   return 0;
}
示例 1:prog.c

dynCompile.cc 的源代码扩展了这种通用行为,并增加了在运行时动态编译 .so(共享对象)的能力。然后,已编译的方法被加载并链接到正在运行的可执行文件中。源文件名由用户在命令行中指定。不难看出,此应用程序可以扩展为生成源代码,然后将该源代码编译以创建共享对象插件。

dynCompile.cc 的代码演练从构建 dynCompile.cc 所需的包含文件的规范开始。

#include <cstdlib>
#include <sys/types.h>
#include <dlfcn.h>
#include <string>
#include <iostream>

using namespace std;
示例 2:dynCompile.cc 的第 1 部分

定义了一些全局句柄和函数指针类型。

void *lib_handle;

typedef int (*initFini_t)();
typedef int (*func_t)(int*);
示例 3:dynCompile.cc 的第 2 部分

main() 方法首先解析命令行参数,该参数包含要构建的源文件的文件名。通过 system() 调用创建并执行用于构建 .so 的命令。在 Linux 环境中,使用 g++。Windows 用户可以调用 Visual Studio 的 cl.exe 编译器。

int main(int argc, char **argv) 
{
  if(argc < 2) {
    cerr << "Use: sourcefilename" << endl;
    return -1;
  }
  string base_filename(argv[1]);
  base_filename = base_filename.substr(0,base_filename.find_last_of("."));
  
  // build the shared object or dll
  string buildCommand("g++ -fPIC –shared ");
  buildCommand += string(argv[1])
    + string(" -o ") + base_filename + string(".so ");
  
  cerr << buildCommand << endl; 
  if(system(buildCommand.c_str())) {
    cerr << "compile command failed!" << endl;
    cerr << "Build command " << buildCommand << endl;
    return -1;
  }
示例 4:dynCompile.cc 的第 3 部分

假设在编译阶段没有发生错误,下一步是加载上一步创建的库。如果发生错误,程序将退出。

  // load the library -------------------------------------------------
  string nameOfLibToLoad("./");
  nameOfLibToLoad += base_filename;
  
  nameOfLibToLoad += ".so";
  lib_handle = dlopen(nameOfLibToLoad.c_str(), RTLD_LAZY);
  if (!lib_handle) {
    cerr << "Cannot load library: " << dlerror() << endl;
    return -1;
  }
示例 5:dynCompile.cc 的第 4 部分

最后,加载符号并解析 init()func()fini() 方法的指针。

  // load the symbols -------------------------------------------------
  initFini_t dynamicInit= NULL;
  func_t dynamicFunc= NULL;
  initFini_t dynamicFini= NULL;

  // reset errors
  dlerror();
  
  // load the function pointers
  dynamicFunc= (func_t) dlsym(lib_handle, "func");
  const char* dlsym_error = dlerror();
  if (dlsym_error) { cerr << "sym load: " << dlsym_error << endl; return -1;}
  dynamicInit= (initFini_t) dlsym(lib_handle, "init");
  dlsym_error = dlerror();
  if (dlsym_error) { cerr << "sym load: " << dlsym_error << endl; return -1;}
  dynamicFini= (initFini_t) dlsym(lib_handle, "fini");
  dlsym_error = dlerror();
  if (dlsym_error) { cerr << "sym load: " << dlsym_error << endl; return -1;}
示例 6:dynCompile.cc 的第 5 部分

检查每个函数指针以查看符号是否已解析。如果已解析,则调用该函数。为了方便插件作者,任何调用都可以是可选的——这意味着该方法不需要包含在编译后的源文件中。唯一的要求是修改上一步中的逻辑,以便解析引用失败不会导致应用程序退出。

  if( (*dynamicInit)() < 0) return -1;

  int x;
  (*dynamicFunc)(&x);
  cout << "Valx " << x << endl;
  
  if( (*dynamicFini)() < 0) return -1;
示例 7:dynCompile.cc 的第 6 部分

最后,卸载库,应用程序退出。

 // unload the library -----------------------------------------------
 dlclose(lib_handle);
}
示例 8:dynCompile.cc 的第 7 部分

以下是一个简单的 C++ 插件源文件 cctest1.cc。此源代码非常直观。

#include <iostream>
using namespace std;
extern "C" int init() {
  cerr << "Hello from Init" << endl;
  return(0); 
}
 
extern "C" int func(int *i)
{
  cerr << "Hello from Func" << endl;
   *i=100;
   return(1);
}
 
extern "C" int fini()
{
  cerr << "Hello from Fini" << endl;
  return(0); 
}
示例 9:cctest1.cc

以下脚本演示了如何构建 dynCompile.cc 并运行 cctest1.cc 源代码

echo "------ building dynCompile -----"
g++ -o dynCompile dynCompile.cc -ldl
echo "------ dynamic version of cctest1.cc -----"
./dynCompile cctest1.cc
示例 10:在 Linux 中构建和运行 dynCompile 的命令

它产生以下输出

$ ./dynCompile cctest1.cc
g++ -fPIC -shared cctest1.cc -o cctest1.so 
Hello from Init
Hello from Func
Valx 100
Hello from Fini
示例 11:dynCompile 的输出

dynCompile.cc 应用程序演示了如何构建顺序 C/C++ 插件并将其加载到正在运行的应用程序中。此外,它还为高度优化的自动插件生成提供了可能性,这些生成基于问题参数。

在插件中使用 OpenCL

以下源代码 testStatic.cc 调用共享对象函数 myOCLfunction(),该函数创建并运行 OpenCL 内核。通过代码的演练,我们看到设备上下文和队列的设置与本文系列第 5 部分中所述的相同。用户可以指定插件在 CPU 或 GPU 上运行。

#define PROFILING // Define to see the time the kernel takes
#define __NO_STD_VECTOR // Use cl::vector instead of STL version
#define __CL_ENABLE_EXCEPTIONS // needed for exceptions
#include <CL/cl.hpp>
#include <fstream>
#include <iostream>
using namespace std;
 
extern "C" int myOCLfunction(cl::CommandQueue&, const char*, int, char **);
 
int main(int argc, char* argv[])
{
  if( argc < 2) {
    cerr << "Use: {cpu|gpu} kernelFile" << endl;
    exit(EXIT_FAILURE);
  }
 
  // handle command-line arguments
  const string platformName(argv[1]);
  const char* kernelFile = argv[2];
  int ret= -1;
 
  cl::vector<int> deviceType;
  cl::vector< cl::CommandQueue > contextQueues;
 
  // crudely parse the command line arguments. 
  if(platformName.compare("cpu")==0)
    deviceType.push_back(CL_DEVICE_TYPE_CPU);
  else if(platformName.compare("gpu")==0) 
    deviceType.push_back(CL_DEVICE_TYPE_GPU);
  else { cerr << "Invalid device type!" << endl; return(1); }
 
  // create the context and queues
  try {
    cl::vector< cl::Platform > platformList;
    cl::Platform::get(&platformList);
 
    // Get all the appropriate devices for the platform the
    // implementation thinks we should be using.
    // find the user-specified devices
    cl::vector<cl::Device> devices;
    for(int i=0; i < deviceType.size(); i++) {
      cl::vector<cl::Device> dev;
      platformList[0].getDevices(deviceType[i], &dev);
      for(int j=0; j < dev.size(); j++) devices.push_back(dev[j]);
    }
 
    // set a single context
    cl_context_properties cprops[] = {CL_CONTEXT_PLATFORM, NULL, 0};
    cl::Context context(devices, cprops);
    cout << "Using the following device(s) in one context" << endl;
    for(int i=0; i < devices.size(); i++)  {
      cout << "  " << devices[i].getInfo<CL_DEVICE_NAME>() << endl;
    }
 
    // Create the separate command queues to perform work
    for(int i=0; i < devices.size(); i++)  {
#ifdef PROFILING
      cl::CommandQueue queue(context, devices[i],CL_QUEUE_PROFILING_ENABLE);
#else
      cl::CommandQueue queue(context, devices[i],0);
#endif
      contextQueues.push_back( queue );
    }
 
    // Call our OpenCL function using the first device. If desired,
    // the reader can add a command-line argument to specify the
    // device number.
    ret = myOCLfunction(contextQueues[0], kernelFile, argc-3, argv+3);
  } catch (cl::Error error) {
    cerr << "caught exception: " << error.what() 
        << '(' << error.err() << ')' << endl;
  }
  return ret;
}
示例 12:testStatic.cc 的源代码

正如注释(用绿色高亮显示)中所指出的,使用第一个设备调用函数 myOCLfunction()。如果需要,读者可以添加一个额外的命令行参数来指定设备编号,或者像本教程系列第 5 部分中所示那样更改代码以在多个设备上运行。通用的函数被传递设备队列以及 OpenCL 内核源文件的名称。

以下是一个用于构建和运行 OpenCL 内核的简单 C++ 插件文件。通过代码的演练,我们看到提供了适当的预处理器定义和包含。此外,oclBuildProgram() 方法已从第 5 部分改编,用于为设备构建 OpenCL 内核。请注意,可以通过 OpenCL 队列检索设备上下文和设备信息(用绿色高亮显示)。

#define PROFILING // Define to see the time the kernel takes
#define __NO_STD_VECTOR // Use cl::vector instead of STL version
#define __CL_ENABLE_EXCEPTIONS // needed for exceptions
#include <CL/cl.hpp>
#include <fstream>
#include <iostream>
using namespace std;
 
#ifndef _OCL_BUILD
#define _OCL_BUILD
cl::Program oclBuildProgram( cl::CommandQueue& queue,
                          const char *kernelFile, 
                          const char* myType)
{
  cl::Context context = queue.getInfo<CL_QUEUE_CONTEXT>();
  cl::Device device = queue.getInfo<CL_QUEUE_DEVICE>();
 
  // Demonstrate using defines in the ocl build
  string buildOptions;
  { // create preprocessor defines for the kernel
    char buf[256]; 
    sprintf(buf,"-D TYPE1=%s ", myType);
    buildOptions += string(buf);
    }
  
  // build the program from the source in the file
  ifstream file(kernelFile);
  string prog(istreambuf_iterator<char>(file),
             (istreambuf_iterator<char>()));
  cl::Program::Sources source( 1, make_pair(prog.c_str(),
                                      prog.length()+1));
  cl::Program oclProg(context, source);
  file.close();
  
  try {
    cerr << "   buildOptions " << buildOptions << endl;
    cl::vector<cl::Device> foo;
    foo.push_back(device);
    oclProg.build(foo, buildOptions.c_str() );
  } catch(cl::Error& err) {
    // Get the build log
    cerr << "Build failed! " << err.what() 
        << '(' << err.err() << ')' << endl;
    cerr << "retrieving  log ... " << endl;
    cerr << oclProg.getBuildInfo<CL_PROGRAM_BUILD_LOG>(device)
        << endl;
    exit(-1);
  }
  return(oclProg);
}
#endif
示例 13:myOCLfunction.cc 的第 1 部分

另请注意,函数 myOCLfunction() 被声明为外部“C”方法,以防止 C++ 名称修饰,这允许从 C 语言程序调用此方法。此插件解析从正在运行的应用程序传递的附加命令行参数。然后,它调用 oclBuildProgram() 方法来为设备编译 OpenCL 内核代码。在此示例中,内核操作的向量类型被定义为 unsigned int。通过使用预处理器定义,可以将此 OpenCL 内核编译以支持任何类型(intfloatdouble),正如本系列第 4 部分中所讨论的。

extern "C" int myOCLfunction( cl::CommandQueue& queue, const char* kernelFile, 
                           int argc, char *argv[])
{
  if(argc < 1) {
    cerr << "myOCLfunction requires a vector size on the command-line" << endl;
    return -1;
  }
  int vecsize = atoi(argv[0]);
  unsigned int* vec = new uint[vecsize];
  int vecBytes = vecsize*sizeof(uint);
 
  cl::Context context = queue.getInfo<CL_QUEUE_CONTEXT>();
 
  // Build the OCL program to use uints
  cl::Program oclProg = oclBuildProgram(queue, kernelFile, "uint");
  cl::Kernel funcKernel = cl::Kernel(oclProg, "func");
示例 14:myOCLfunction.cc 的第 2 部分

聪明的读者会认识到,这个插件是从第 5 部分的 testSum.hpp 代码改编而来的。一个向量被填充有随机数并传递给 OpenCL 内核。主机上的简单双重检查验证了 OpenCL 代码是否正确地将向量中的每个随机数与自身相加。如果 OpenCL 和主机结果一致,则打印消息“test passed”。如果发现结果不正确,则打印消息“TEST FAILED!”。

  // Fill the host memory with random data for the sums
  srand(0);
  for(int i=0; i < vecsize; i++) vec[i] = (rand()&0xffffff);
  
  // Create a separate buffer for each device in the context
  cl::Buffer d_vec;
  d_vec = cl::Buffer(context, CL_MEM_READ_WRITE, vecBytes);
  
  funcKernel.setArg(0,vecsize); // define the size of the vector
  funcKernel.setArg(1,d_vec); // set the pointer to the device vector
  funcKernel.setArg(2,0); // set the offset for the kernel
 
  queue.enqueueWriteBuffer(d_vec, CL_TRUE,0, vecBytes, &vec[0]);
  
  cl::Event event;
  queue.enqueueNDRangeKernel(funcKernel, 
                          cl::NullRange, // offset starts at 0,0
                          cl::NDRange( vecsize ), // parallelize by vecsize
                          cl::NDRange(1, 1),//one work-item per work-group
                          NULL, &event);
  // manually transfer the data from the device
  queue.enqueueReadBuffer(d_vec, CL_TRUE, 0, vecBytes, &vec[0]);
 
  queue.finish(); // wait for everything to finish
 
  // perform the golden test
  {
    int i;
    srand(0);
    for(i=0; i < vecsize; i++) {
      unsigned int r = (rand()&0xffffff);
      r += r;
      if(r != vec[i]) break;
    }
    if(i == vecsize) {
      cout << "test passed" << endl;
    } else {
      cout << "TEST FAILED!" << endl;
    }
  }
  delete [] vec;
  
  return EXIT_SUCCESS;
}
示例 15:myOCLfunction.cc 的第 3 部分

这两个示例代码可以在 Linux 下使用以下命令进行编译

echo "---------------"
g++ -c -I $AMDAPPSDKROOT/include  myOCLfunction.cc
g++ -I $AMDAPPSDKROOT/include -fopenmp testStatic.cc myOCLfunction.o -L $AMDAPPSDKROOT/lib/x86_64 -lOpenCL -o testStatic

此程序可以改编为动态加载插件并调用 myOCLfunction(),如下面的 testDynamic.cc 的源代码所示。用于执行动态加载和链接的修改代码用绿色高亮显示。请注意,共享对象文件的名称通过命令行参数传递。

#define PROFILING // Define to see the time the kernel takes
#define __NO_STD_VECTOR // Use cl::vector instead of STL version
#define __CL_ENABLE_EXCEPTIONS // needed for exceptions
#include <CL/cl.hpp>
#include <fstream>
#include <iostream>
#include <dlfcn.h>
using namespace std;
 
typedef int (*func_t)(cl::CommandQueue&, const char*, int, char **);
 
int main(int argc, char* argv[])
{
  if( argc < 3) {
    cerr << "Use: {cpu|gpu} kernelFile sharedObjectFile" << endl;
    exit(EXIT_FAILURE);
  }
 
  // handle command-line arguments
  const string platformName(argv[1]);
  const char* kernelFile = argv[2];
  const char* soFile = argv[3];
  int ret= -1;
 
  cl::vector<int> deviceType;
  cl::vector< cl::CommandQueue > contextQueues;
 
  // crudely parse the command line arguments. 
  if(platformName.compare("cpu")==0)
    deviceType.push_back(CL_DEVICE_TYPE_CPU);
  else if(platformName.compare("gpu")==0) 
    deviceType.push_back(CL_DEVICE_TYPE_GPU);
  else { cerr << "Invalid device type!" << endl; return(1); }
 
  // create the context and queues
  try {
    cl::vector< cl::Platform > platformList;
    cl::Platform::get(&platformList);
 
    // Get all the appropriate devices for the platform the
    // implementation thinks we should be using.
    // find the user-specified devices
    cl::vector<cl::Device> devices;
    for(int i=0; i < deviceType.size(); i++) {
      cl::vector<cl::Device> dev;
      platformList[0].getDevices(deviceType[i], &dev);
      for(int j=0; j < dev.size(); j++) devices.push_back(dev[j]);
    }
 
    // set a single context
    cl_context_properties cprops[] = {CL_CONTEXT_PLATFORM, NULL, 0};
    cl::Context context(devices, cprops);
    cout << "Using the following device(s) in one context" << endl;
    for(int i=0; i < devices.size(); i++)  {
      cout << "  " << devices[i].getInfo<CL_DEVICE_NAME>() << endl;
    }
 
    // Create the separate command queues to perform work
    for(int i=0; i < devices.size(); i++)  {
#ifdef PROFILING
      cl::CommandQueue queue(context, devices[i],CL_QUEUE_PROFILING_ENABLE);
#else
      cl::CommandQueue queue(context, devices[i],0);
#endif
      contextQueues.push_back( queue );
    }
 
    // ----- Perform the dynamic load ---------------
    string nameOfLibToLoad = string("./") + soFile;
    void* lib_handle = dlopen(nameOfLibToLoad.c_str(), RTLD_LAZY);
    if (!lib_handle) {
      cerr << "Cannot load library: " << dlerror() << endl;
      return -1;
    }
    // load the function pointers
    func_t dynamicFunc = (func_t) dlsym(lib_handle, "myOCLfunction" );
    const char* dlsym_error = dlerror();
    if (dlsym_error) { cerr << "sym load: " << dlsym_error << endl; return -1;}
    
    // Call our OpenCL function using the first device. If desired,
    // the reader can add a command-line argument to specify the
    // device number.
    ret = (*dynamicFunc)(contextQueues[0], kernelFile, argc-4, argv+4);
 
  } catch (cl::Error error) {
    cerr << "caught exception: " << error.what() 
        << '(' << error.err() << ')' << endl;
  }
  return ret;
}
示例 16:testDynamic.cc 的源代码

在 Linux 下,使用以下命令创建 testDynamic 可执行文件和 myOCLfunction.so 文件

echo "---------------"
g++ -fPIC -shared -I $AMDAPPSDKROOT/include  myOCLfunction.cc -o myOCLfunction.so
g++ -I $AMDAPPSDKROOT/include testDynamic.cc -L $AMDAPPSDKROOT/lib/x86_64 -lOpenCL -o testDynamic
示例 17:在 Linux 下构建 testDynamic.cc 的命令

运行 testStatic testDynamic 表明该插件在两个应用程序中都能在 CPU 和 GPU 上正确运行。显示设备和主机结果一致的消息用绿色高亮显示。

$ sh RUN
 ------- static test CPU ------ 
Using the following device(s) in one context
  AMD Phenom(tm) II X6 1055T Processor
   buildOptions -D TYPE1=uint 
test passed
 ------- static test GPU ------ 
Using the following device(s) in one context
  Cypress
   buildOptions -D TYPE1=uint 
test passed
 ------- dynamic test CPU ------ 
Using the following device(s) in one context
  AMD Phenom(tm) II X6 1055T Processor
   buildOptions -D TYPE1=uint 
test passed
 ------- dynamic test GPU ------ 
Using the following device(s) in one context
  AMD Phenom(tm) II X6 1055T Processor
   buildOptions -D TYPE1=uint 
test passed
示例 18:testStatic 和 testDynamic 的输出

包含和保护 OpenCL 源

在许多情况下,开发人员可能不想将 OpenCL 源代码包含在单独的文件中。使用多个文件会使安装复杂化,并可能导致难以诊断的安装和升级错误。使用单个文件可简化安装,并使 OpenCL 插件部署更加健壮。特别是,商业开发人员不希望以任何人都可以轻易阅读的形式发布其 OpenCL 源代码。

将 OpenCL 源包含在插件文件中的最简单方法是使用 Linux 的 xxd 十六进制转储工具创建一个包含 OpenCL 源代码的字符串。然后可以使用本文系列第 1 部分中使用的 OpenCL 方法 clCreateProgramWithSource() 来从字符串编译源代码。

以下命令演示了如何从包含以下内容的 foo.txt 文件创建 C 语言字符串:This is a test file. It spans multiple lines。

示例 19:一个要使用 xxd 转换的示例文件 foo.txt

运行 "xxd –I foo.txt" 会产生以下输出

unsigned char foo_txt[] = {
  0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0x65,
  0x73, 0x74, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x0a, 0x49, 0x74, 0x20,
  0x73, 0x70, 0x61, 0x6e, 0x73, 0x20, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70,
  0x6c, 0x65, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x2e, 0x0a
};
unsigned int foo_txt_len = 46;
示例 20:"xxd –I foo.txt" 的输出

字符串 foo_txt 可以在插件中编译并传递给 clCreateProgramWithSource()。使用 xxd 的缺点是 OpenCL 源字符串不再可读。这并不能保护 OpenCL 内核源代码,因为它可以通过简单地运行 UNIX strings 命令在 .so 或可执行文件中轻松找到。

商业开发人员可以使获取其 OpenCL 内核源代码更加困难,方法是使用 Google Code 上的 Keyczar 等软件包对源文本进行加密和解密。加密的源字符串仍然可以使用 xxd 创建。尽管如此,OpenCL 源代码将在解密后以及在调用 clCreateProgramWithSource() 时保留在缓冲区中。虽然加密使其更难,但心怀不轨的黑客仍然可以从该缓冲区中找到并打印源代码。

第 1 部分所述,离线编译可以为特定设备创建 OpenCL 设备二进制文件。就像应用程序二进制文件可以防止逆向工程一样,OpenCL 二进制文件也可以混淆 OpenCL 内核代码。同样,可以使用 xxd 的输出来将此二进制文件包含在源代码中。AMD 提供了一篇知识库文章,解释了如何执行离线内核编译。此方法的一个技术限制是,插件只能支持预编译的设备。根据业务模式,这可能是优势或劣势,因为交付给客户的插件只能支持其拥有预编译 OpenCL 二进制文件的特定设备。

摘要

通过创建 OpenCL 插件的能力,应用程序程序员能够编写和支持通用应用程序,这些应用程序可以在存在 GPU 时提供加速性能,在没有 GPU 时提供基于 CPU 的性能。这些插件架构是成熟的,并且是利用现有应用程序和代码库的便捷方式。它们也有助于保护现有的软件投资。

将 OpenCL 源代码动态编译并链接到正在运行的应用程序中的能力为代码生成器优化打开了一系列机会。这种能力是 OpenCL 可移植并行计算基础的一部分。正如本文所述并在科学计算中得到利用,为通用问题域中的特定参数集动态生成优化代码可以实现非常高的性能——远远超过任何“一刀切”的通用代码所能提供的。

本系列的下一篇文章将把这种能力扩展到利用异构工作流中的混合 CPU/GPU 计算,以便开发人员可以在其生产工作流中利用 GPU 加速和 CPU 功能。

© . All rights reserved.