我正在尝试测试Aparapi的表现。 我见过一些blogs,结果表明Aparapi在进行数据并行操作时确实提高了性能。
但我在测试中无法看到这一点。这是我做的,我写了两个程序,一个使用Aparapi,另一个使用普通循环。
计划1:在Aparapi
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
答案 0 :(得分:5)
你没有做错任何事 - 执行基准测试本身。
基准测试总是很棘手,特别是对于涉及JIT的情况(对于Java),以及对用户隐藏许多细节的图书馆(如Aparapi)。在这两种情况下,您至少应该多次执行要进行基准测试的代码部分。
对于Java版本,人们可能期望循环的单次执行的计算时间在循环本身被执行多次时减少,这是由于JIT的开始。还有许多额外的注意事项需要考虑 - 详情,你应该参考this answer。在这个简单的测试中,JIT的效果可能并不真实,但在更现实或更复杂的情况下,这将产生影响。无论如何:当重复循环10次时,在我的机器上单次执行循环的时间大约是 70毫秒。
对于Aparapi版本,评论中已经提到了可能的GPU初始化点。在这里,这确实是主要问题:当运行内核10次时,我的机器上的时间是
1248
72
72
72
73
71
72
73
72
72
您会看到初始调用会导致所有开销。这样做的原因是,在第一次调用Kernel#execute()
时,它必须进行所有初始化(基本上将字节码转换为OpenCL,编译OpenCL代码等)。 KernelRunner
类的文档中也提到了这一点:
KernelRunner
因调用Kernel.execute()
而被 lazily 创建。
这种影响 - 即第一次执行的相对较大的延迟 - 导致了Aparapi邮件列表上的这个问题:A way to eagerly create KernelRunners。唯一的解决方法是建议创建一个“初始化调用”,如
kernel.execute(Range.create(1));
没有实际工作负载,只能触发整个设置,以便后续调用快速。 (这也适用于您的示例)。
您可能已经注意到,即使在初始化之后,Aparapi版本仍然不比普通Java版本快。这样做的原因是像这样的简单向量添加的任务是内存绑定 - 有关详细信息,您可以参考this answer,这解释了这个术语以及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毫秒 - 这是通过阿帕拉皮的速度接近90!
另请注意,即使在 CPU 模式下,与普通Java相比,Aparapi也可能具有优势。在我的机器上,在CPU模式下,Aparapi只需要 2300毫秒,因为它仍然使用Java线程池并行执行。
答案 1 :(得分:0)
只需在执行主循环内核之前添加
kernel.setExplicit(true);
kernel.put(a);
kernel.put(b);
和
kernel.get(sum);
之后。
尽管Aparapi会分析
Kernel.run()
的字节码 方法(以及从Kernel.run()
可以访问的任何方法)Aparapi没有 呼叫站点的可见性。在上面的代码中没有办法 Aparapi检测到for中未修改hugeArray 循环体。不幸的是,Aparapi必须默认为“安全”, 向后复制hugeArray的内容并转发到GPU 设备。
https://github.com/aparapi/aparapi/blob/master/doc/ExplicitBufferHandling.md