R 是一个统计学经常用到的软件,提供了非常多的统计学函数。 但是它是一个单线程解释语言,面对大数据量的时候,往往性能跟不上,可以利用 Rcpp 编写 C++ 包提供给 R 使用,可以大大提高性能。 而对于大规模数据的处理,使用 CUDA 则是一个非常好的解决方案。 在 Linux 和 macOS 下, CUDA 程序和 C++ 程序都使用 gcc 编译器, 但是在 Windows 下,Rcpp 的包必须用 MinGW 编译器, CUDA 的包必须用 MSVC 编译器,需要一定的技巧才能让 R 用上 CUDA。
本文介绍 MSVC 包和 MinGW 包的混合编译,不仅适用于 R 语言,也不仅适用于 CUDA 程序, 也适用于其他需要通过 MinGW 的程序调用 MSVC 程序的情况。
在 R 中,一定会大量用到矩阵,一有矩阵那必然涉及到指针,而且一定是在 MinGW 函数中创建的指针。那么该怎么将矩阵传到 MSVC 的函数中呢? 这其实非常类似于 CUDA 编程中的内存问题,我们只需要在两边分别开辟内存,然后将内存中的数据复制一下,相当于写一个 cudaMalloc 和 cudaMemcpy 。 这样相当于在全局创建了很多的变量,如果使用一些方法将这些全局变量统一管理会更好。 可以将这些全局变量保存在一个结构体中,工厂函数返回一个指向这个结构体的指针,并为这个结构体成员创建初始值。
也可以利用 C++ 类的封装我们要在 R 中调用的函数,将这个类继承自一个抽象类(所有函数都是纯虚函数),将接口类导出, 同时导出一个 C 的工厂函数,返回指向这个接口类的指针, 利用 MSVC 和 MinGW 虚表结构一致的特点,就可以在 MinGW 的 C++ 代码中使用这个接口类中的函数了(类没有虚析构函数)。 将矩阵数据作为类的成员变量,利用 C++ 成员函数进行创建、赋值、销毁。
非类的写法
首先演示一种纯 C 非类的写法。首先需要建立一个 VS 的 CUDA 工程,并设置该项目配置类型为“动态链接库”。 项目目录如下:
AddCUDA
add.cpp
add.h
kernel.cu
kernel.h
AddCUDA.vcxproj
文件 kernel.cu 使用的 CUDA 函数是 VS 中 CUDA 工程自带的模板,做了一些修改, 主要去掉了 goto 语句,并设置了核函数启动配置。
__global__ voidaddKernel(int *c, constint *a, constint *b) { int i = threadIdx.x; c[i] = a[i] + b[i]; }
// Helper function for using CUDA to add vectors in parallel. booladdWithCuda(int *c, constint *a, constint *b, unsignedint size) { int *dev_a = 0; int *dev_b = 0; int *dev_c = 0; cudaError_t cudaStatus;
// Choose which GPU to run on, change this on a multi-GPU system. cudaStatus = cudaSetDevice(0); if (cudaStatus != cudaSuccess) { fprintf(stderr, "cudaSetDevice failed! Do you have a CUDA-capable GPU installed?"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); returnfalse; }
// Allocate GPU buffers for three vectors (two input, one output) . cudaStatus = cudaMalloc((void**)&dev_c, size * sizeof(int)); if (cudaStatus != cudaSuccess) { fprintf(stderr, "cudaMalloc failed!"); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); returnfalse; }
// Launch a kernel on the GPU with one thread for each element. dim3 blockSize(256), gridSize((size + blockSize.x - 1) / blockSize.x); addKernel<<<gridSize, blockSize >>>(dev_c, dev_a, dev_b);
// Check for any errors launching the kernel cudaStatus = cudaGetLastError(); if (cudaStatus != cudaSuccess) { fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus)); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); returnfalse; } // cudaDeviceSynchronize waits for the kernel to finish, and returns // any errors encountered during the launch. cudaStatus = cudaDeviceSynchronize(); if (cudaStatus != cudaSuccess) { fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus); cudaFree(dev_c); cudaFree(dev_a); cudaFree(dev_b); returnfalse; }
ADDCUDA_API int* createVector(int n); ADDCUDA_API voidsetVector(int* ptr, int i, int value); ADDCUDA_API intgetVector(int* ptr, int i); ADDCUDA_API voiddeleteVector(int* ptr); ADDCUDA_API booladdVector(int* a, int* b, int n, int *c);
当然, __declspec(dllimport) 可要可不要,要也可以,这样这个 Dll 也可以给 Windows 程序使用。
intmain(int argc, charconst *argv[]) { int n = 100000; int *a = createVector(n); int *b = createVector(n); int *c = createVector(n); for (size_t i = 0; i < n; i++) { setVector(a, i, 10); setVector(b, i, 100); setVector(c, i, 0); } addVector(a, b, n, c); int *result = newint[n]; for (size_t i = 0; i < n; i++) { result[i] = getVector(c, i); } printf("result:\n"); for (size_t i = 0; i < 10; i++) { printf("%5d", result[i]); } printf("\n"); deleteVector(a); deleteVector(b); deleteVector(c); return0; }
classADDCUDA_APIIAdd { public: virtualvoidSetA(int i, int value)= 0; virtualvoidSetB(int i, int value)= 0; virtualintGetC(int i)= 0; virtualboolAdd()= 0; };