代码之家  ›  专栏  ›  技术社区  ›  BobMcGee

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

  •  39
  • BobMcGee  · 技术社区  · 15 年前

    我正在优化LZF压缩算法的纯java实现,它涉及大量字节[]访问和用于哈希和比较的基本int数学。性能确实很重要,因为压缩的目标是减少I/O需求。我不会发布代码,因为它还没有被清理,可能会被重组。

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

    我已经做了:

    为了提高性能,我大量使用位旋转和将字节打包到int中,以及移位&掩蔽。

    良好(可接受)答案的要求:

    • 不可接受的答案: “这是更快的”,没有解释多少和为什么,或者没有用JIT编译器进行测试。
    • 基本答案:
    • 好答案: 包括两个代码示例以演示
    • 有JRE 1.5和1.6的基准测试
    • 完美答案: 是由在HotSpot编译器上工作的人编写的,他可以完全解释或引用要使用的优化的条件,以及它通常有多快。可能包括由HotSpot生成的java代码和示例汇编代码。

    (编辑)部分答案:边界检查省略:

    这是从热点内部wiki提供的链接中获取的,位于: 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 CompressLZF code

    从逻辑上讲,这与开发版本相同,但使用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; 
        }
    

    最终编辑:

    如果代码凌乱,则表示歉意:这表示代码正在开发中,而不是在提交时经过润色。

    4 回复  |  直到 4 年前
        1
  •  19
  •   Quonux    11 年前

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

    了解你的敌人

    如果您的目标市场/应用程序将主要运行在特定的体系结构上,那么您应该考虑投资特定于它的工具。 *如果您在x86上的性能是关键因素,那么intel的 VTune 非常适合深入研究 jit output analysis enregistering 机会是非常不同的。

    获得正确的工具

    您可能需要一个采样分析器。对于您的特定需求,插装的开销(以及相关的连锁反应,如内联、缓存污染和代码大小膨胀)太大了。“英特尔VTune分析器”实际上可以用于Java,尽管集成没有其他分析器那么紧密。
    investigate the output of the JIT 如果你懂一点装配知识,这是相当可观的。 这 article details some interesting analysis using this functionality

    从其他实现中学习

    The Change history 更改历史记录表明,以前的内联程序集实际上适得其反,并且允许编译器完全控制输出(在 而不是汇编中的指令)产生了更好的结果。

    一些细节


    因此,任何不能转换为常量的静态字段都应该放在同一个类中,因为这些值通常放在与类关联的vtables和元数据专用的内存区域中。

    需要避免使用转义分析无法捕获的对象分配(仅在1.6以后的版本中)。

    c code 积极使用循环展开。然而,在较旧(1.4纪元)的虚拟机上的性能是 heavily dependant on the mode the JVM is in . 显然,后面的sun jvm版本在内联和展开方面更具攻击性,特别是在服务器模式下。

    你的目标正在移动,并将继续移动。再次重申马克·莱曼以前的经历:

    即使是对jvm的微小更新也可能涉及 significant compiler changes

    6544668不要对运行时无法对齐的数组操作进行矢量化。 6536652实现一些超字(SIMD)优化 6468290基于每个cpu从eden中分配和分配

    显然是队长

    量,量,量。如果你能让你的库包含(在一个单独的dll中)一个简单且易于执行的基准,记录相关信息(vm版本、cpu、操作系统、命令行开关等),并将其简单地发送回你,你将增加你的覆盖范围,最重要的是你将覆盖那些使用它的人。

        2
  •  6
  •   João Silva    15 年前

    远至 就这点而言,我相信新的JDK已经包含了一个改进的算法,只要有可能就可以消除它。以下是关于这一主题的两篇主要论文:

    • V.米哈耶夫,S.费多塞耶夫,V.苏哈列夫,N.利普斯基。2002 Java中循环版本控制的有效增强 Link Jet JVM。
    • 沃辛格、托马斯、克里斯蒂安·维默和汉斯彼得·姆森贝克。2007 . PPPJ。 Link . 稍微基于上述文件,我相信这是将在下一篇文章中包括的实现 JDK 加速

    this 这篇博文对其中一篇论文进行了肤浅的讨论,并给出了一些基准测试结果,不仅针对新JDK中的数组,还针对算法。博客的评论也很有趣,因为上述论文的作者提出了一些非常有趣的评论和讨论论点。此外,还有一些关于这一主题的类似博客文章。

    希望能有帮助。

        3
  •  3
  •   Michael Borgwardt    15 年前

    在优化像LZW这样简单的数字运算算法方面,您不太可能需要帮助JIT编译器。ShuggyCoUk提到了这一点,但我认为值得特别注意:

    您必须尽可能减少工作集的大小并改进数据访问位置。您提到“将字节打包成整数以提高性能”。这听起来像是使用int来保存字节值,以便将它们与单词对齐。不要那样做!增加的数据集大小将超过任何收益(我曾经将一些ECC数字处理代码从int[]转换为byte[],并获得2倍的速度提升)。

    很可能您不知道这一点:如果您需要将某些数据同时作为字节和整数处理,则不必移位和|-屏蔽-使用 ByteBuffer.asIntBuffer()

    在当前的1.6 JVM中,有多少 必须先复制元素,然后再复制 System.arraycopy胜过复制循环?

    最好自己做基准测试。当我在Java1.3时代这么做的时候,大约有2000个元素。

        4
  •  2
  •   StaxMan    15 年前

    到目前为止,有很多答案,但还有几个问题:

    • 紧循环对Java和C都很重要(变量分配也一样——虽然您不能直接控制它,但HotSpot最终将不得不这样做)。我将紧循环重新安排为紧(er)内环处理单字节大小写(7位ascii),从而将UTF-8解码速度提高了一倍,而将其他情况排除在外。

    H2实现也进行了相当多的优化(例如:不再清除哈希数组,这通常是有意义的);实际上,我帮助修改了它,以便在另一个Java项目中使用。我的贡献主要是改变它,使其在非流的情况下更加优化,但这并没有触及紧密的编码/解码循环。

    推荐文章