4

我正在尝试测试 Aparapi 的性能。我看过一些博客,结果表明 Aparapi 在进行数据并行操作时确实提高了性能。

但我无法在我的测试中看到这一点。这就是我所做的,我写了两个程序,一个使用 Aparapi,另一个使用普通循环。

计划 1:在阿帕拉皮

import com.amd.aparapi.Kernel;
import com.amd.aparapi.Range;

public class App 
{
    public static void main( String[] args )
    {
        final int size = 50000000;

        final float[] a = new float[size];
        final float[] b = new float[size];

        for (int i = 0; i < size; i++) {
           a[i] = (float) (Math.random() * 100);
           b[i] = (float) (Math.random() * 100);
        }

        final float[] sum = new float[size];

        Kernel kernel = new Kernel(){
           @Override public void run() {
              int gid = getGlobalId();
              sum[gid] = a[gid] + b[gid];
           }
        };
        long t1 = System.currentTimeMillis();
        kernel.execute(Range.create(size));
        long t2 = System.currentTimeMillis();
        System.out.println("Execution mode = "+kernel.getExecutionMode());
        kernel.dispose();
        System.out.println(t2-t1);
    }
}

程序 2:使用循环

public class App2 {

    public static void main(String[] args) {

        final int size = 50000000;

        final float[] a = new float[size];
        final float[] b = new float[size];

        for (int i = 0; i < size; i++) {
           a[i] = (float) (Math.random() * 100);
           b[i] = (float) (Math.random() * 100);
        }

        final float[] sum = new float[size];
        long t1 = System.currentTimeMillis();
        for(int i=0;i<size;i++) {
            sum[i]=a[i]+b[i];
        }

        long t2 = System.currentTimeMillis();
        System.out.println(t2-t1);

    }
}

程序 1 大约需要 330 毫秒,而程序 2 只需要大约 55 毫秒。我在这里做错了吗?我确实打印出了Aparpai程序中的执行模式,它打印出执行模式是GPU

4

2 回答 2

7

您没有做错任何事情 - 执行基准测试本身。

基准测试总是很棘手,特别是在涉及 JIT 的情况下(如 Java),以及许多细节对用户隐藏的库(如 Aparapi)。在这两种情况下,您至少应该多次执行要进行基准测试的代码部分。

对于 Java 版本,由于 JIT 的作用,当循环本身被多次执行时,人们可能会期望循环执行一次的计算时间会减少。还有许多额外的注意事项需要考虑 - 有关详细信息,您应该参考这个答案。在这个简单的测试中,JIT 的效果可能并不明显,但在更现实或更复杂的场景中,这会有所作为。无论如何:重复循环 10 次时,在我的机器上单次执行循环的时间约为70 毫秒

对于 Aparapi 版本,可能的 GPU 初始化点已经在评论中提到。在这里,这确实是主要问题:当运行内核 10 次时,我机器上的时间是

1248
72
72
72
73
71
72
73
72
72

您会看到初始调用导致所有开销。这样做的原因是,在第一次调用 时Kernel#execute(),它必须进行所有初始化(基本上将字节码转换为 OpenCL,编译 OpenCL 代码等)。该KernelRunner课程的文档中也提到了这一点:

是由于调用而懒惰地KernelRunner创建的。Kernel.execute()

这样做的影响——即第一次执行的相对较大的延迟——导致了 Aparapi 邮件列表上的这个问题:一种急切地创建 KernelRunners 的方法。建议的唯一解决方法是创建一个“初始化调用”,例如

kernel.execute(Range.create(1));

没有真正的工作量,只触发整个设置,以便后续调用快速。(这也适用于您的示例)。


您可能已经注意到,即使初始化之后,Aparapi 版本仍然没有比普通 Java 版本快。原因是像这样的简单向量加法任务是受内存限制的- 有关详细信息,您可以参考这个答案,它解释了这个术语以及一般 GPU 编程的一些问题。

作为一个可能从 GPU 中受益的案例的过度暗示示例,您可能想要修改您的测试,以创建一个人工计算绑定任务:当您更改内核以涉及一些昂贵的三角函数时,像这样

Kernel kernel = new Kernel() {
    @Override
    public void run() {
        int gid = getGlobalId();
        sum[gid] = (float)(Math.cos(Math.sin(a[gid])) + Math.sin(Math.cos(b[gid])));
    }
};

和相应的普通Java循环版本,像这样

for (int i = 0; i < size; i++) {
    sum[i] = (float)(Math.cos(Math.sin(a[i])) + Math.sin(Math.cos(b[i])));;
}

然后你会看到不同。在我的机器(GeForce 970 GPU 与 AMD K10 CPU)上,Aparapi 版本的时间大约为140 毫秒,而普通 Java 版本的时间则高达12000 毫秒——通过 Aparapi 加速了近 90 毫秒!

另请注意,即使在CPU模式下,与纯 Java 相比,Aparapi 也可能具有优势。在我的机器上,在 CPU 模式下,Aparapi 只需要2300 毫秒,因为它仍然使用 Java 线程池并行执行。

于 2015-10-16T19:30:00.530 回答
1

只需在主循环内核执行之前添加

kernel.setExplicit(true);
kernel.put(a);
kernel.put(b);

kernel.get(sum);

在它之后。

尽管 Aparapi 确实分析了Kernel.run() 方法的字节码(以及任何可从 访问的方法Kernel.run()),但 Aparapi 对调用站点没有可见性。在上面的代码中,Aparapi 无法检测到该 hugeArray 没有在 for 循环体内被修改。不幸的是,Aparapi 必须默认为“安全”并将hugeArray 的内容向后和向前复制到GPU 设备。

https://github.com/aparapi/aparapi/blob/master/doc/ExplicitBufferHandling.md

于 2018-07-21T21:21:54.213 回答