Skip to content

Commit f85aabd

Browse files
authored
Merge pull request #13 from Col-E/optimize-decompress
Add unsafe inflater impl
2 parents 5600a7f + 6054e8a commit f85aabd

File tree

8 files changed

+235
-9
lines changed

8 files changed

+235
-9
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>software.coley</groupId>
88
<artifactId>lljzip</artifactId>
9-
<version>1.5.1</version>
9+
<version>1.6.0</version>
1010

1111
<name>LL Java ZIP</name>
1212
<description>Lower level ZIP support for Java</description>

src/main/java/software/coley/llzip/format/compression/DeflateDecompressor.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package software.coley.llzip.format.compression;
22

33
import software.coley.llzip.format.model.LocalFileHeader;
4-
import software.coley.llzip.util.BufferData;
54
import software.coley.llzip.util.ByteData;
5+
import software.coley.llzip.util.FastWrapOutputStream;
66

7-
import java.io.ByteArrayOutputStream;
87
import java.io.IOException;
98
import java.util.zip.DataFormatException;
109
import java.util.zip.Inflater;
@@ -21,7 +20,7 @@ public ByteData decompress(LocalFileHeader header, ByteData data) throws IOExcep
2120
if (header.getCompressionMethod() != ZipCompressions.DEFLATED)
2221
throw new IOException("LocalFileHeader contents not using 'Deflated'!");
2322
Inflater inflater = new Inflater(true);
24-
ByteArrayOutputStream out = new ByteArrayOutputStream();
23+
FastWrapOutputStream out = new FastWrapOutputStream();
2524
try {
2625
byte[] output = new byte[1024];
2726
byte[] buffer = new byte[1024];
@@ -42,13 +41,13 @@ public ByteData decompress(LocalFileHeader header, ByteData data) throws IOExcep
4241
if (count != 0) {
4342
out.write(output, 0, count);
4443
}
45-
} while (!inflater.finished() && !inflater.needsDictionary());
46-
}catch (DataFormatException e) {
44+
} while (!inflater.finished());
45+
} catch (DataFormatException e) {
4746
String s = e.getMessage();
4847
throw (ZipException) new ZipException(s != null ? null : "Invalid ZLIB data format").initCause(e);
4948
} finally {
5049
inflater.end();
5150
}
52-
return BufferData.wrap(out.toByteArray());
51+
return out.wrap();
5352
}
5453
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package software.coley.llzip.format.compression;
2+
3+
import software.coley.llzip.format.model.LocalFileHeader;
4+
import software.coley.llzip.util.ByteData;
5+
import software.coley.llzip.util.FastWrapOutputStream;
6+
import software.coley.llzip.util.UnsafeInflater;
7+
8+
import java.io.IOException;
9+
import java.util.ArrayDeque;
10+
import java.util.Deque;
11+
import java.util.zip.DataFormatException;
12+
import java.util.zip.Inflater;
13+
import java.util.zip.ZipException;
14+
15+
/**
16+
* Optimized implementation of {@link DeflateDecompressor} with unsafe resetting for more throughput.
17+
*
18+
* @author xDark
19+
*/
20+
@SuppressWarnings("UnnecessaryLocalVariable")
21+
public class UnsafeDeflateDecompressor implements Decompressor {
22+
private static final int DEFLATE_CACHE_LIMIT = 64;
23+
private static final Deque<DeflateEntry> DEFLATE_ENTRIES = new ArrayDeque<>();
24+
private static final byte[] emptyBuf = new byte[0];
25+
26+
static {
27+
Deque<DeflateEntry> entries = DEFLATE_ENTRIES;
28+
for (int i = 0; i < DEFLATE_CACHE_LIMIT; i++) {
29+
entries.push(new DeflateEntry());
30+
}
31+
}
32+
33+
@Override
34+
public ByteData decompress(LocalFileHeader header, ByteData data) throws IOException {
35+
if (header.getCompressionMethod() != ZipCompressions.DEFLATED)
36+
throw new IOException("LocalFileHeader contents not using 'Deflated'!");
37+
FastWrapOutputStream out = new FastWrapOutputStream();
38+
DeflateEntry entry;
39+
Deque<DeflateEntry> inflaters = DEFLATE_ENTRIES;
40+
synchronized (inflaters) {
41+
entry = inflaters.poll();
42+
}
43+
if (entry == null) {
44+
entry = new DeflateEntry();
45+
} else {
46+
// Normal 'Inflater' reset() is synchronized and bottlenecks us,
47+
// but we're using 'UnsafeInflater' which bypasses that.
48+
entry.inflater.reset();
49+
}
50+
try {
51+
byte[] output = entry.decompress;
52+
byte[] buffer = entry.buffer;
53+
Inflater inflater = entry.inflater;
54+
long position = 0L;
55+
long length = data.length();
56+
inflater.setInput(emptyBuf);
57+
do {
58+
if (inflater.needsInput()) {
59+
int remaining = (int) Math.min(buffer.length, length);
60+
if (remaining != 0) {
61+
data.get(position, buffer, 0, remaining);
62+
length -= remaining;
63+
position += remaining;
64+
inflater.setInput(buffer, 0, remaining);
65+
}
66+
}
67+
int count = inflater.inflate(output);
68+
if (count != 0) {
69+
out.write(output, 0, count);
70+
}
71+
} while (!inflater.finished());
72+
} catch (DataFormatException e) {
73+
String msg = e.getMessage();
74+
throw (ZipException) new ZipException(msg != null ? null : "Invalid ZLIB data format").initCause(e);
75+
} finally {
76+
end:
77+
{
78+
if (inflaters.size() < DEFLATE_CACHE_LIMIT) {
79+
synchronized (inflaters) {
80+
if (inflaters.size() < DEFLATE_CACHE_LIMIT) {
81+
inflaters.addFirst(entry);
82+
break end;
83+
}
84+
}
85+
}
86+
entry.inflater.end();
87+
}
88+
}
89+
return out.wrap();
90+
}
91+
92+
private static final class DeflateEntry {
93+
final Inflater inflater = new UnsafeInflater(true);
94+
final byte[] decompress = new byte[1024];
95+
final byte[] buffer = new byte[8192];
96+
}
97+
}

src/main/java/software/coley/llzip/format/compression/ZipCompressions.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import software.coley.llzip.format.model.LocalFileHeader;
44
import software.coley.llzip.util.ByteData;
5+
import software.coley.llzip.util.UnsafeInflater;
56

67
import java.io.IOException;
78

@@ -208,7 +209,12 @@ static ByteData decompress(LocalFileHeader header) throws IOException {
208209
case STORED:
209210
return header.getFileData();
210211
case DEFLATED:
211-
return header.decompress(new DeflateDecompressor());
212+
// Use unsafe decompressor if available since it is faster.
213+
if (UnsafeInflater.initFail) {
214+
return header.decompress(new DeflateDecompressor());
215+
} else {
216+
return header.decompress(new UnsafeDeflateDecompressor());
217+
}
212218
default:
213219
// TODO: Support other decompressing techniques
214220
String methodName = getName(method);

src/main/java/software/coley/llzip/util/BufferData.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,18 @@ public static BufferData wrap(ByteBuffer buffer) {
153153
public static BufferData wrap(byte[] array) {
154154
return new BufferData(ByteBuffer.wrap(array).order(ByteOrder.LITTLE_ENDIAN), new AtomicBoolean());
155155
}
156+
157+
/**
158+
* @param array
159+
* Byte array to wrap.
160+
* @param offset
161+
* Offset into the array to start at.
162+
* @param length
163+
* Length of content.
164+
*
165+
* @return Buffer data.
166+
*/
167+
public static BufferData wrap(byte[] array, int offset, int length) {
168+
return new BufferData(ByteBuffer.wrap(array, offset, length).order(ByteOrder.LITTLE_ENDIAN), new AtomicBoolean());
169+
}
156170
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package software.coley.llzip.util;
2+
3+
import java.io.ByteArrayOutputStream;
4+
5+
/**
6+
* Byte output stream with {@link BufferData} wrapping provided, without the array-copy of {@link #toByteArray()}.
7+
*
8+
* @author xDark
9+
*/
10+
public final class FastWrapOutputStream extends ByteArrayOutputStream {
11+
/**
12+
* @return Wrapper of the current buffer.
13+
*/
14+
public BufferData wrap() {
15+
return BufferData.wrap(buf, 0, count);
16+
}
17+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package software.coley.llzip.util;
2+
3+
import sun.misc.Unsafe;
4+
5+
import java.lang.invoke.MethodHandle;
6+
import java.lang.invoke.MethodHandles;
7+
import java.lang.invoke.MethodType;
8+
import java.nio.ByteBuffer;
9+
import java.util.zip.Inflater;
10+
11+
/**
12+
* Inflater extension providing non-synchronized resetting via unsafe.
13+
*
14+
* @author Matt Coley
15+
*/
16+
public class UnsafeInflater extends Inflater {
17+
private static final String ERR_SUFFIX = "If this is due to JDK restrictions, use DeflateDecompressor";
18+
private static final MethodHandles.Lookup LOOKUP = UnsafeUtil.lookup();
19+
private static final Unsafe UNSAFE = UnsafeUtil.get();
20+
private static final ByteBuffer DEFAULT_BUF = ByteBuffer.allocate(0);
21+
private static long off_zsRef;
22+
private static long off_input;
23+
private static long off_inputArray;
24+
private static long off_finished;
25+
private static long off_needDict;
26+
private static long off_bytesRead;
27+
private static long off_bytesWritten;
28+
private static MethodHandle mh_reset;
29+
private static MethodHandle mh_address;
30+
/** Indicates unsafe init failed */
31+
public static boolean initFail;
32+
33+
static {
34+
// All these fields are private, so we will use hacks to access them.
35+
try {
36+
off_zsRef = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("zsRef"));
37+
off_input = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("input"));
38+
off_inputArray = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("inputArray"));
39+
off_finished = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("finished"));
40+
off_needDict = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("needDict"));
41+
off_bytesRead = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("bytesRead"));
42+
off_bytesWritten = UNSAFE.objectFieldOffset(Inflater.class.getDeclaredField("bytesWritten"));
43+
mh_reset = LOOKUP.findStatic(Inflater.class, "reset", MethodType.methodType(void.class, long.class));
44+
} catch (Exception ex) {
45+
initFail = true;
46+
}
47+
}
48+
49+
/**
50+
* @param nowrap
51+
* if true then support GZIP compatible compression
52+
*/
53+
public UnsafeInflater(boolean nowrap) {
54+
super(nowrap);
55+
}
56+
57+
@Override
58+
public void reset() {
59+
try {
60+
Object zsRefValue = UNSAFE.getObject(this, off_zsRef);
61+
if (mh_address == null) {
62+
MethodHandles.Lookup lookup = UnsafeUtil.lookup();
63+
mh_address = lookup.findGetter(zsRefValue.getClass(), "address", long.class);
64+
}
65+
long addressRet = (long) mh_address.invoke(zsRefValue);
66+
mh_reset.invoke(addressRet);
67+
68+
UNSAFE.putObject(this, off_input, DEFAULT_BUF);
69+
UNSAFE.putObject(this, off_inputArray, null);
70+
// This is so fucking dumb, but resetting these causes a 2x slowdown. Yes, putBoolean is SLOW!
71+
// In testing, we don't need these values changes, so I guess we'll ignore this for now.
72+
// UNSAFE.putBoolean(this, off_finished, false);
73+
// UNSAFE.putBoolean(this, off_needDict, false);
74+
UNSAFE.putLong(this, off_bytesRead, 0);
75+
UNSAFE.putLong(this, off_bytesWritten, 0);
76+
} catch (Throwable ex) {
77+
initFail = true;
78+
throw new IllegalStateException("Failed to reset unsafely. " + ERR_SUFFIX, ex);
79+
}
80+
}
81+
}

src/main/java/software/coley/llzip/util/UnsafeUtil.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sun.misc.Unsafe;
44

5+
import java.lang.invoke.MethodHandles;
56
import java.lang.reflect.Field;
67

78
/**
@@ -11,6 +12,7 @@
1112
*/
1213
class UnsafeUtil {
1314
private static final Unsafe UNSAFE;
15+
private static final MethodHandles.Lookup LOOKUP;
1416

1517
/**
1618
* @return Unsafe instance.
@@ -19,11 +21,21 @@ public static Unsafe get() {
1921
return UNSAFE;
2022
}
2123

24+
/**
25+
* @return Unsafe instance.
26+
*/
27+
public static MethodHandles.Lookup lookup() {
28+
return LOOKUP;
29+
}
30+
2231
static {
2332
try {
2433
Field f = Unsafe.class.getDeclaredField("theUnsafe");
2534
f.setAccessible(true);
26-
UNSAFE = (Unsafe) f.get(null);
35+
Unsafe u = UNSAFE = (Unsafe) f.get(null);
36+
f = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
37+
MethodHandles.publicLookup();
38+
LOOKUP = (MethodHandles.Lookup) u.getObject(u.staticFieldBase(f), u.staticFieldOffset(f));
2739
} catch (NoSuchFieldException | IllegalAccessException ex) {
2840
throw new ExceptionInInitializerError(ex);
2941
}

0 commit comments

Comments
 (0)