爲 OpenCL 提供支模版支持

OpenCL本身(至少目前)並不支持模版。在許多場景下(例如移植CUDA代碼),這會帶來一些麻煩。
這裏,我們介紹一種解決方案。主要思路是,利用OpenCL在運行時編譯的特點,通過宏定義實現類似C++模型的功能。

  • 首先我們要解決動態獲取類型名稱的問題
    解決方法如下:
template<typename T>
struct TypeParseTraits {
    static const char *name;
};
template<typename T>
const char * TypeParseTraits<T>::name = "Unsupported Type!";

#define REGISTER_PARSE_TYPE(T) template<> struct TypeParseTraits<T> {\
    static const char* name; \
 };  \
 template<> const char * TypeParseTraits<T>::name = #T;

REGISTER_PARSE_TYPE(int);
REGISTER_PARSE_TYPE(float);
REGISTER_PARSE_TYPE(double);

對於註冊過的類似,我們可以通過 TypeParseTraits::name得到相應的字符串。對於未註冊的類型,默認返回“Unsupported”。(一個實例參見 boostCompute

另外一種類似的方法如下:

template<typename T> 
struct CLTypes {
    static const char * getName() { return "Unsupported"; }
};

template<>
struct CLTypes<int> {
    static const char * getName() { return "int"; }
};

template<> 
struct CLTypes<float> {
    static const char * getName() { return "float"; }
};

template<>
struct CLTypes<double> {
    static const char * getName() { return "double"; }
};

需要注意的是,無論哪各方法,我們可能會選擇使用C++提供的內置函數功能。

typeid(T).name()

但 C++ 標準並不保證能返回期望的結果(例如int並不保證能返回我們期望的字符串”int”)(參見這裏),因此,使用它這會存在潛在的移植性問題。

  • 動態獲得類型名稱的基礎上,我們可以通過宏替換 OpenCL kernel 定義中的”模型“類型。
static std::once_flag compiled;
std::call_once(compiled, []() {
        std::ostringstream options;
        options << "-D T=" << TypeParseTraits<T>::name;
        prg = cl::Program(addVectors_src, false);
        prg.build(options.str().c_str());
        std::cout << "vector addition kernel compiled for type: " << TypeParseTraits<T>::name << std::endl;
        kl = cl::Kernel(prg, "addVectors_kl");
    });

爲了保證每個kernel源碼只會編譯一次,這裏我們使用了C++11的call_once功能(參見這裏

實現這個目的的另一種方法是使用利用常規的靜態變量。

static bool compliled = []() {
        std::ostringstream options;
        options << "-D T=" << TypeParseTraits<T>::name;
        prg = cl::Program(addVectors_src, false);
        prg.build(options.str().c_str());
        std::cout << "vector addition kernel compiled for type: " << TypeParseTraits<T>::name << std::endl;
        kl = cl::Kernel(prg, "addVectors_kl");
        return true;
    };

這裏,兩種方法並無差別。但在其他情形下,call_once可能會更靈活一些,參見討論

完整的代碼如下:

#define STRINGFY(src) #src

template<typename T>
struct TypeParseTraits {
    static const char* name = "Unsupported";
};

#define REGISTER_PARSE_TYPE(X) template<> struct TypeParseTraits<X> { \
    static const char* name = #X; \
    }; 

REGISTER_PARSE_TYPE(int);
REGISTER_PARSE_TYPE(float);
REGISTER_PARSE_TYPE(double);

template<typename T>
void addVectors(vector<T> &out, vector<T> &in1, vector<T> &in2, size_t n) {
    static std::once_flag compiled;
    static cl::Program prg;
    static cl::Kernel kl;

    static const char * addVectors_src = STRINGFY(
        kernel
        void addVectors_kl(global const T * const a, global const T * const b, global T * restrict const c) {
        unsigned idx = get_global_id(0);
        c[idx] = a[idx] + b[idx];
        });

    std::call_once(compiled, []() {
        std::ostringstream options;
        options << "-D T=" << TypeParseTraits<T>::name;
        prg = cl::Program(addVectors_src, false);
        prg.build(options.str().c_str());
        std::cout << "vector addition kernel compiled for type: " << TypeParseTraits<T>::name << std::endl;
        kl = cl::Kernel(prg, "addVectors_kl");
    });

    Buffer a(begin(in1), end(in1), true, false);
    Buffer b(begin(in2), end(in2), true, false);
    Buffer c(CL_MEM_READ_WRITE, n * sizeof(T));

    auto addVectors_kl = cl::make_kernel<Buffer, Buffer, Buffer>(kl);

    addVectors_kl(EnqueueArgs(n), a, b, c);

    cl::copy(c, begin(out), end(out));
}

一個簡單的測試

void pseudotemplate_test() {
    const int n = 10;

    vector<int> iv1(n, 1);
    vector<int> iv2(n, 10);
    vector<int> iv3(n);

    vector<double> dv1(n, 2.0);
    vector<double> dv2(n, 3.0);
    vector<double> dv3(n);

    addVectors(iv3, iv1, iv2, iv1.size());
    addVectors(dv3, dv1, dv2, dv1.size());

    for (int i = 0; i < n; i++) {
        cout << iv3[i] << ":\t" << iv1[i] + iv2[i] << endl;
    }
    cout << endl;

    for (int i = 0; i < n; i++) {
        cout << dv3[i] << ":\t" << dv1[i] + dv2[i] << endl;
    }
    cout << endl;
}

輸出

11: 11
11: 11
11: 11
11: 11
11: 11
11: 11
11: 11
11: 11
11: 11
11: 11

5:  5
5:  5
5:  5
5:  5
5:  5
5:  5
5:  5
5:  5
5:  5
5:  5

關於STRINGFY,參見

1. stackoverflow上關於動態獲取類型名稱字符串的討論
2. Templating and Caching OpenCL Kernels

發佈了31 篇原創文章 · 獲贊 162 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章