【问题标题】:How can i optimize more my OpenCl kernel如何优化我的 OpenCl 内核
【发布时间】:2014-05-14 20:42:42
【问题描述】:

我已经分离出一些彼此不同但需要在不同工作项中并行运行的功能。因此,当内核被调用时,它需要决定必须执行哪个函数..

void call_calc0() {
    // code
}

void call_calc1() {
    // code
}

void call_calc2() {
    // code
}

void call_calc3() {
    // code 
}

__kernel void perform (__global double* A, __global double* B) {
    int idx = get_global_id(0);
    if (idx == 0) {
        call_calc0();
    } else if (idx == 1) {
        call_calc1();
    } else if (idx == 2) {
        call_calc2();
    } else if (idx == 3) {
        call_calc3();
    }
}

如果有 256/512 个工作项,此代码示例将不是正确的方法。我该如何优化呢?

【问题讨论】:

  • 我可以想到函数指针和预处理器的方式.. 但是,是否可以在 OpenCl 中使用函数指针?
  • OpenCL 中不允许使用函数指针。你是说每个工作项都必须执行完全不同的代码?
  • 是的。但在同一个缓冲区.. 单独的单个缓冲区块。有可能.. 如果我用单个“if”条件替换 if-else,我做了一些测试@jprice.. 然后编译变得非常快......代码就像 if (idx == 1) {calc1}; if (idx == 2) {calc2}...
  • 当然,您可以使用switch 语句实现相同的目的。然而,OpenCL NDRange 执行模型是为数据并行执行而设计的,而这正是 GPU 和加速器等设备所擅长的。听起来您的问题根本不是数据并行的(您正在对相同的数据执行完全不同的代码),这对于围绕 SIMD 执行设计的设备可能具有非常低的性能。在您执行的每个不同计算中真的没有共同的组件吗?
  • 如果逻辑真的像你说的那么简单,本地大小为1,为什么不直接调用4个单独的内核,完全去掉条件呢?我同意@jprice,这听起来确实不像是一个很好地映射到 OpenCL 的问题。也许更多的上下文会很有用。

标签: opencl


【解决方案1】:

如果可能的话,最好的优化是使用四个不同的内核。如果您调用这个内核的组大小超过 1,那么在并行执行时就会出现问题。

如果可能,请尝试分离您的全局内存或以非常小心、不冲突的方式使用它。这应该允许您创建四个单独的内核,并摆脱条件代码执行。

当遇到第一个 if/case 时,该组的一些工作项将运行代码,但其他 75% 的工作项将等待。大多数 opencl 设备,尤其是 GPU,都以这种方式运行。当前 25% 的工作项完成后,它们将等待下一个 if/case 代码执行。

这适用于 opencl 中的所有分支,例如 if/else、switch、for 和 while/do。每当组中的某些工作项不满足条件时,它们就会等待其他满足条件的工作项。然后在 'if' 组等待时执行 'else' 组工作项。

查看它的另一种方法是比较 CPU 和 GPU 硬件。 CPU 有很多专门用于分支预测和高速缓存的晶体管。 GPU 在本质上更加基于矢量,并且直到最近才开始支持 CPU 的一些更高级的流控制功能。

【讨论】:

  • 这是 GPU 中并行化的第一条规则,也是 OO 编程的第一条规则。将所有内容放在一个内核/类中,特别是当进程完全不相关时,是一种非常糟糕的方法。有时,将工作拆分为多个内核/类的懒惰会在未来造成令人头疼的问题。
  • 是的,而且在不知道被调用的函数对全局内存的实际作用的情况下,无法确定它是否可以分解为单独的内核。
【解决方案2】:

由于 OpenCL 不支持函数指针,因此您只能使用 if/elseswitch。这两者的性能应该是相同的,只是编码偏好不同。

您可以使用预处理器宏让事情变得更简单/更简洁。例如,您可以这样做:

#define CALL_CASE(i)    \
    case i:             \
        call_calc##i(); \
        break;          \

__kernel void perform (__global double* A, __global double* B) {
    int idx = get_global_id(0);
    switch (idx) {
        CALL_CASE(0);
        CALL_CASE(1);
        CALL_CASE(2);
        CALL_CASE(3);
        ... // etc
    }
}

如果您要自动生成call_calcX() 函数,那么同时生成这个switch 块也很容易。如果您手动编写这些call_calcX() 函数,那么只需多行代码即可将每个函数添加到块中。不理想,但也不可怕。

根据上面的 cmets,这个问题似乎根本不是数据并行的,这将限制利用大多数 OpenCL 设备中可用的 SIMD 执行的能力。

【讨论】:

  • 我完全同意您对 SIMD 的评论。我需要考虑同样的问题。在我的情况下,数据是 MISD 。让我为更好的算法考虑更多。但是作为我的问题..你给了我一个解决方案..谢谢你的关心.. :)
猜你喜欢
  • 2016-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-05
  • 2013-04-02
相关资源
最近更新 更多