让我们说我的Java程序的瓶颈确实是计算一堆矢量点积的一些紧密循环。是的我已经描述过,是的,它是瓶颈,是的,它是重要的,是的,这就是算法的结果,是的,我已经运行Proguard来优化字节码等。
这项工作本质上是点产品。因为,我有两个float[50]
,我需要计算成对产品的总和。我知道处理器指令集可以快速和批量地执行这些操作,如SSE或MMX。
是的我可以通过在JNI中编写一些本机代码来访问它们。 JNI电话非常昂贵。
我知道你无法保证JIT编译或编译的内容。有没有人曾听说过使用这些指令的JIT生成代码?如果有的话,有什么关于Java代码可以帮助它以这种方式编译吗?
可能是“不”;值得一提。
答案 0 :(得分:39)
因此,基本上,您希望代码运行得更快。 JNI就是答案。我知道你说它不适合你,但让我告诉你你错了。
这是Dot.java
:
import java.nio.FloatBuffer;
import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;
@Platform(include="Dot.h", compiler="fastfpu")
public class Dot {
static { Loader.load(); }
static float[] a = new float[50], b = new float[50];
static float dot() {
float sum = 0;
for (int i = 0; i < 50; i++) {
sum += a[i]*b[i];
}
return sum;
}
static native @MemberGetter FloatPointer ac();
static native @MemberGetter FloatPointer bc();
static native float dotc();
public static void main(String[] args) {
FloatBuffer ab = ac().capacity(50).asBuffer();
FloatBuffer bb = bc().capacity(50).asBuffer();
for (int i = 0; i < 10000000; i++) {
a[i%50] = b[i%50] = dot();
float sum = dotc();
ab.put(i%50, sum);
bb.put(i%50, sum);
}
long t1 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
a[i%50] = b[i%50] = dot();
}
long t2 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
float sum = dotc();
ab.put(i%50, sum);
bb.put(i%50, sum);
}
long t3 = System.nanoTime();
System.out.println("dot(): " + (t2 - t1)/10000000 + " ns");
System.out.println("dotc(): " + (t3 - t2)/10000000 + " ns");
}
}
和Dot.h
:
float ac[50], bc[50];
inline float dotc() {
float sum = 0;
for (int i = 0; i < 50; i++) {
sum += ac[i]*bc[i];
}
return sum;
}
我们可以使用命令行编译并运行JavaCPP:
$ javac -cp javacpp.jar Dot.java
$ java -jar javacpp.jar Dot
$ java -cp javacpp.jar:. Dot
使用英特尔酷睿i7-3632QM CPU @ 2.20GHz,Fedora 20,GCC 4.8.3和OpenJDK 7或8,我得到了这样的输出:
dot(): 37 ns
dotc(): 23 ns
或大约快1.6倍。我们需要使用直接NIO缓冲区而不是数组,但HotSpot can access direct NIO buffers as fast as arrays。另一方面,在这种情况下,手动展开循环并不能提供可测量的性能提升。
答案 1 :(得分:36)
为了解决其他人在此表达的一些怀疑态度,我建议任何想要证明自己或其他人使用以下方法的人:
示例:
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE) //makes looking at assembly easier
public void inc() {
for (int i=0;i<a.length;i++)
a[i]++;// a is an int[], I benchmarked with size 32K
}
带有和不带标志的结果(最近的Haswell笔记本电脑,Oracle JDK 8u60): -XX:+ UseSuperWord:475.073±44.579 ns / op(每个操作纳秒数) -XX:-UseSuperWord:3376.364±233.211 ns / op
热循环的程序集有点格式化并坚持在这里,但这里是一个片段(hsdis.so无法格式化一些AVX2向量指令,所以我使用-XX运行:UseAVX = 1): - XX:+ UseSuperWord('-prof perfasm:intelSyntax = true')
9.15% 10.90% │││ │↗ 0x00007fc09d1ece60: vmovdqu xmm1,XMMWORD PTR [r10+r9*4+0x18]
10.63% 9.78% │││ ││ 0x00007fc09d1ece67: vpaddd xmm1,xmm1,xmm0
12.47% 12.67% │││ ││ 0x00007fc09d1ece6b: movsxd r11,r9d
8.54% 7.82% │││ ││ 0x00007fc09d1ece6e: vmovdqu xmm2,XMMWORD PTR [r10+r11*4+0x28]
│││ ││ ;*iaload
│││ ││ ; - psy.lob.saw.VectorMath::inc@17 (line 45)
10.68% 10.36% │││ ││ 0x00007fc09d1ece75: vmovdqu XMMWORD PTR [r10+r9*4+0x18],xmm1
10.65% 10.44% │││ ││ 0x00007fc09d1ece7c: vpaddd xmm1,xmm2,xmm0
10.11% 11.94% │││ ││ 0x00007fc09d1ece80: vmovdqu XMMWORD PTR [r10+r11*4+0x28],xmm1
│││ ││ ;*iastore
│││ ││ ; - psy.lob.saw.VectorMath::inc@20 (line 45)
11.19% 12.65% │││ ││ 0x00007fc09d1ece87: add r9d,0x8 ;*iinc
│││ ││ ; - psy.lob.saw.VectorMath::inc@21 (line 44)
8.38% 9.50% │││ ││ 0x00007fc09d1ece8b: cmp r9d,ecx
│││ │╰ 0x00007fc09d1ece8e: jl 0x00007fc09d1ece60 ;*if_icmpge
有趣地冲进城堡!
答案 2 :(得分:26)
在以Java 7u40开头的HotSpot版本中,服务器编译器提供对自动矢量化的支持。根据{{3}}
然而,这似乎只适用于“简单循环” - 至少暂时如此。例如,累积数组无法进行矢量化JDK-6340864
答案 3 :(得分:5)
以下是关于试验我的朋友写的Java和SIMD指令的好文章: http://prestodb.rocks/code/simd/
它的一般结果是你可以期望JIT在1.8中使用一些SSE操作(以及1.9中的更多操作)。虽然你不应该期待太多,但你需要小心。
答案 4 :(得分:4)
您可以编写OpenCl内核来进行计算并从java http://www.jocl.org/运行它。
代码可以在CPU和/或GPU上运行,OpenCL语言也支持矢量类型,因此您应该能够明确地利用例如SSE3 / 4指令。
答案 5 :(得分:3)
我猜你在发现netlib-java之前写了这个问题;-)它提供了你需要的本机API,机器优化的实现,并且在原生边界没有任何成本,谢谢记忆钉扎。
答案 6 :(得分:3)
看看Performance comparison between Java and JNI for optimal implementation of computational micro-kernels。他们表明Java HotSpot VM服务器编译器支持使用超级字级并行的自动矢量化,这仅限于循环并行内部的简单情况。本文还将为您提供一些指导,说明您的数据大小是否足以证明JNI路由的合理性。
答案 7 :(得分:-3)
我不相信大多数虚拟机是否足够智能以进行这种优化。公平地说,大多数优化都要简单得多,例如在2的幂时移位而不是乘法。单声道项目引入了他们自己的矢量和其他方法与原生支持,以帮助提高性能。