如何编写Java以允许SSE使用和边界检查消除(或其他高级优化)?

Bob*_*Gee 40 java optimization performance jvm-hotspot bounds-check-elimination

情况:

我正在优化LZF压缩算法的纯java实现,它涉及大量的byte []访问和基本的int数学,用于散列和比较.性能确实很重要,因为压缩的目标是降低I/O要求.我没有发布代码,因为它尚未清理,并且可能会进行大量重组.

问题:

  • 如何编写我的代码以允许它使用更快的SSE操作进行JIT编译到表单?
  • 我如何构造它,以便编译器可以轻松地消除数组边界检查?
  • 是否有关于特定数学运算的相对速度的广泛参考(等于正常加/减需要多少递增/递减,移位或对阵列访问的速度有多快)?
  • 我如何才能优化分支 - 最好是使用短体,或者一些长的条件语句,或者是嵌套条件的短条件语句?
  • 使用当前的1.6 JVM,在System.arraycopy击败复制循环之前必须复制多少个元素?

我已经做了什么:

在我因为过早优化而受到攻击之前:基本算法已经非常出色,但Java实现的速度不到等效C的2/3.我已经用System.arraycopy替换了复制循环,致力于优化循环并消除了un - 需要的操作.

我大量使用bit twiddling并将字节打包为整数,以实现性能,以及移位和屏蔽.

出于法律原因,我无法查看类似库中的实现,并且现有库具有过于严格的许可条款.

GOOD(已接受)答案的要求:

  • 不可接受的答案: "这个更快",没有解释为什么多少,为什么,或者没有用JIT编译器测试过.
  • 边界答案:在Hotspot 1.4之前没有经过任何测试
  • 基本答案:将提供一般规则和解释为什么它在编译器级别更快,大致更快
  • 好的答案:包括几个代码示例来演示
  • 优秀答案:拥有JRE 1.5和1.6的基准测试
  • 完美的答案:是由参与HotSpot编译器的人员完成的,可以完全解释或参考使用优化的条件,以及通常更快的速度.可能包含由HotSpot生成的java代码和示例汇编代码.

另外:如果有人有详细说明Hotspot优化和分支性能的内容的链接,那么欢迎这些.我对字节码有足够的了解,网站分析字节码而不是源代码级别的性能会有所帮助.

(编辑)局部答案:界限 - 检查消除:

这是从提供的HotSpot内部维基链接获取的:https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination

HotSpot将消除所有for循环中的边界检查,具有以下条件:

  • 数组是循环不变的(不在循环中重新分配)
  • 索引变量有一个恒定的步幅(增量/减少恒定量,如果可能的话只在一个点)
  • 数组由变量的线性函数索引.

例: int val = array[index*2 + 5]

要么: int val = array[index+9]

不: int val = array[Math.min(var,index)+7]


早期版本的代码:

这是一个示例版本.不要窃取它,因为它是H2数据库项目的未发布版本的代码.最终版本将是开源的.这是对代码的优化:H2 CompressLZF代码

从逻辑上讲,这与开发版本相同,但是它使用for(...)循环来逐步执行输入,并使用if/else循环来实现文字和反向引用模式之间的不同逻辑.它减少了阵列访问和模式之间的检查.

public int compressNewer(final byte[] in, final int inLen, final byte[] out, int outPos){
        int inPos = 0;
        // initialize the hash table
        if (cachedHashTable == null) {
            cachedHashTable = new int[HASH_SIZE];
        } else {
            System.arraycopy(EMPTY, 0, cachedHashTable, 0, HASH_SIZE);
        }
        int[] hashTab = cachedHashTable;
        // number of literals in current run
        int literals = 0;
        int future = first(in, inPos);
        final int endPos = inLen-4;

        // Loop through data until all of it has been compressed
        while (inPos < endPos) {
                future = (future << 8) | in[inPos+2] & 255;
//                hash = next(hash,in,inPos);
                int off = hash(future);
                // ref = possible index of matching group in data
                int ref = hashTab[off];
                hashTab[off] = inPos;
                off = inPos - ref - 1; //dropped for speed

                // has match if bytes at ref match bytes in future, etc
                // note: using ref++ rather than ref+1, ref+2, etc is about 15% faster
                boolean hasMatch = (ref > 0 && off <= MAX_OFF && (in[ref++] == (byte) (future >> 16) && in[ref++] == (byte)(future >> 8) && in[ref] == (byte)future));

                ref -=2; // ...EVEN when I have to recover it
                // write out literals, if max literals reached, OR has a match
                if ((hasMatch && literals != 0) || (literals == MAX_LITERAL)) {
                    out[outPos++] = (byte) (literals - 1);
                    System.arraycopy(in, inPos - literals, out, outPos, literals);
                    outPos += literals;
                    literals = 0;
                }

                //literal copying split because this improved performance by 5%

                if (hasMatch) { // grow match as much as possible
                    int maxLen = inLen - inPos - 2;
                    maxLen = maxLen > MAX_REF ? MAX_REF : maxLen;
                    int len = 3;
                    // grow match length as possible...
                    while (len < maxLen && in[ref + len] == in[inPos + len]) {
                        len++;
                    }
                    len -= 2;

                    // short matches write length to first byte, longer write to 2nd too
                    if (len < 7) {
                        out[outPos++] = (byte) ((off >> 8) + (len << 5));
                    } else {
                        out[outPos++] = (byte) ((off >> 8) + (7 << 5));
                        out[outPos++] = (byte) (len - 7);
                    }
                    out[outPos++] = (byte) off;
                    inPos += len;

                    //OPTIMIZATION: don't store hashtable entry for last byte of match and next byte
                    // rebuild neighborhood for hashing, but don't store location for this 3-byte group
                    // improves compress performance by ~10% or more, sacrificing ~2% compression...
                    future = ((in[inPos+1] & 255) << 16) | ((in[inPos + 2] & 255) << 8) | (in[inPos + 3] & 255);
                    inPos += 2;
                } else { //grow literals
                    literals++;
                    inPos++;
                } 
        }

        // write out remaining literals
        literals += inLen-inPos;
        inPos = inLen-literals;
        if(literals >= MAX_LITERAL){
            out[outPos++] = (byte)(MAX_LITERAL-1);
            System.arraycopy(in, inPos, out, outPos, MAX_LITERAL);
            outPos += MAX_LITERAL;
            inPos += MAX_LITERAL;
            literals -= MAX_LITERAL;
        }
        if (literals != 0) {
            out[outPos++] = (byte) (literals - 1);
            System.arraycopy(in, inPos, out, outPos, literals);
            outPos += literals;
        }
        return outPos; 
    }
Run Code Online (Sandbox Code Playgroud)

最终编辑:

到目前为止,我已经标记了最佳答案,因为截止日期已接近尾声.由于我在决定发布代码之前花了这么长时间,因此我将继续提升并尽可能回复评论. 如果代码混乱,请道歉:这代表了开发中的代码,而不是用于提交.

Shu*_*oUk 18

不是一个完整的答案,我根本没有时间做你的问题需要的详细基准测试,但希望有用.

了解你的敌人

您的目标是JVM(实质上是JIT)和底层CPU /内存子系统的组合.因此,"在JVM X上运行速度更快"在所有情况下都不太可能有效,因为您会进行更积极的优化.

如果您的目标市场/应用程序将主要在特定架构上运行,您应该考虑投资特定于它的工具.*如果你在x86上的表现是关键因素,那么英特尔的VTune非常适合深入研究你描述的那种jit输出分析.*64位和32位JIT之间的差异可能相当大,尤其是在x86平台上,其中调用约定可以更改并且注册机会非常不同.

获得正确的工具

您可能希望获得一个采样分析器.根据您的特定需求,仪器的开销(以及与内联,缓存污染和代码大小膨胀相关的相关因素)将会非常大.英特尔VTune分析器实际上可以用于Java,尽管集成并不像其他人那么紧密.
如果您正在使用sun JVM并且很高兴只知道最新/最好的版本正在做什么,那么如果您知道一些程序集,那么可用于调查JIT输出的选项是相当可观的.该文章描述了使用此功能的一些有趣的分析

从其他实现中学习

更改历史记录更改历史记录表明,先前的内联汇编实际上是反效果的,并且允许编译器完全控制输出(在代码中调整而不是在汇编中指令)产生更好的结果.

一些细节

由于LZF在现代桌面CPUS上的高效非托管实现中,主要是内存带宽有限(因此它被编程为未经优化的memcpy的速度),因此您需要将代码完全保留在1级缓存中.
因此,您不能将任何静态字段放入同一个类中,因为这些值通常会放在专用于与类关联的vtable和元数据的内存区域中.

需要避免Escape Analysis无法捕获的对象分配(仅在1.6以后).

C代码,使积极利用循环展开的.然而,在较旧的(1.4时代)VM上的性能很大程度上取决于JVM所处的模式.显然,后来的sun jvm版本在内联和展开方面更具攻击性,尤其是在服务器模式下.

由JIT生成的预取指令可以在像这样接近内存绑定的代码上产生所有差异.

"这对我们来说很直接"

你的目标正在移动,并将继续.Marc Lehmann先前的经历:

默认的HLOG大小现在是15(cpu缓存增加了)

即使是对jvm的微小更新也可能涉及重大的编译器更改

6544668不执行无法在运行时对齐的数组操作.6536652实现一些超级字(SIMD)优化6531696不要立即将16位值存储到Intel cpus上的内存中6468290基于每个cpu划分和分配eden

明显上尉

测量,测量,测量.如果您可以让您的库包含(在一个单独的DLL中)一个简单易于执行的基准测试,记录相关信息(虚拟机版本,CPU,操作系统,命令行开关等),并使这个简单的发回给你,你将增加你的覆盖面,最重要的是你会覆盖那些使用它的人.


Joã*_*lva 7

边界检查消除而言,我相信新的JDK已经包含一个改进的算法,只要有可能就消除它.这是关于这个主题的两篇主要论文:

  • V. Mikheev,S.Fedoseev,V.Sukharev,N.Lipsky.2002年 在Java中有效增强循环版本控制.链接.本文来自Excelsior的人员,他们在Jet JVM中实现了这项技术.
  • Würthinger,Thomas,Christian Wimmer和HanspeterMössenböck.2007. 数组边界检查消除Java HotSpot客户端编译器.PPPJ.链接.稍微基于上面的论文,这是我认为将包含在下一个JDK中的实现.还提供了实现的加速.

还有这篇博客文章,它在表面上讨论了其中一篇论文,并且还提供了一些基准测试结果,不仅用于数组,还用于新JDK中的算术.博客条目的评论也非常有趣,因为上述论文的作者提出了一些非常有趣的评论和讨论论点.此外,还有一些关于此主题的其他类似博客文章的指示.

希望能帮助到你.