情况:
我正在优化 LZF 压缩算法的纯 java 实现,它涉及大量 byte[] 访问和基本的 int 数学,用于散列和比较。性能确实很重要,因为压缩的目标是减少 I/O 需求。我没有发布代码,因为它还没有被清理,并且可能会被大量重组。
问题:
- 如何编写代码以使用更快的 SSE 操作将其 JIT 编译为表单?
- 如何构造它以便编译器可以轻松消除数组边界检查?
- 是否有关于特定数学运算的相对速度的广泛参考(等于正常加/减需要多少增量/减量,移位或与数组访问相比有多快)?
- 我该如何优化分支——拥有大量带有短主体的条件语句,或者一些长的,还是带有嵌套条件的短的条件语句更好?
- 使用当前的 1.6 JVM,在 System.arraycopy 击败复制循环之前必须复制多少元素?
我已经做了什么:
在我因过早优化而受到攻击之前:基本算法已经很出色了,但 Java 实现的速度不到等效 C 的 2/3。我已经用 System.arraycopy 替换了复制循环,致力于优化循环并消除了 un - 需要的操作。
我大量使用位旋转并将字节打包到整数中以提高性能,以及移位和屏蔽。
出于法律原因,我无法查看类似库中的实现,并且现有库的使用许可条款过于严格。
良好(已接受)答案的要求:
- 不可接受的答案: “这更快”没有解释多少和为什么,或者没有用 JIT 编译器测试。
- 边界答案:在 Hotspot 1.4 之前没有经过任何测试
- 基本答案:将提供一般规则和解释,说明为什么它在编译器级别更快,以及大概快多少
- 好的答案:包括几个代码示例来演示
- 很好的答案:有 JRE 1.5 和 1.6 的基准
- 完美答案:由从事 HotSpot 编译器工作的人提供,并且可以完全解释或参考要使用的优化的条件,以及它通常的速度有多快。可能包括由 HotSpot 生成的 java 代码和示例汇编代码。
另外:如果有人有详细说明热点优化和分支性能的链接,欢迎提供。我对字节码有足够的了解,因此在字节码而不是源代码级别分析性能的网站会有所帮助。
(编辑)部分答案:边界检查消除:
这取自提供的 HotSpot 内部 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 数据库项目的未发布版本代码。最终版本将是开源的。这是对此处代码的优化: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;
}
最终编辑:
到目前为止,我已将最佳答案标记为已接受,因为截止日期快到了。由于我在决定发布代码之前花了很长时间,因此我将继续投票并在可能的情况下回复评论。 如果代码混乱,请道歉:这代表开发中的代码,没有为提交而完善。