Skip to content

Commit 8a84bec

Browse files
committed
Add unsafe inflater impl
1 parent 5600a7f commit 8a84bec

File tree

4 files changed

+177
-2
lines changed

4 files changed

+177
-2
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package software.coley.llzip.format.compression;
2+
3+
import software.coley.llzip.format.model.LocalFileHeader;
4+
import software.coley.llzip.util.BufferData;
5+
import software.coley.llzip.util.ByteData;
6+
import software.coley.llzip.util.UnsafeInflater;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.IOException;
10+
import java.util.ArrayDeque;
11+
import java.util.Deque;
12+
import java.util.zip.DataFormatException;
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+
public class UnsafeDeflateDecompressor implements Decompressor {
21+
private static final Deque<UnsafeInflater> INFLATERS = new ArrayDeque<>();
22+
23+
@Override
24+
public ByteData decompress(LocalFileHeader header, ByteData data) throws IOException {
25+
if (header.getCompressionMethod() != ZipCompressions.DEFLATED)
26+
throw new IOException("LocalFileHeader contents not using 'Deflated'!");
27+
ByteArrayOutputStream out = new ByteArrayOutputStream();
28+
UnsafeInflater inflater;
29+
Deque<UnsafeInflater> inflaters = INFLATERS;
30+
synchronized (inflaters) {
31+
inflater = inflaters.poll();
32+
if (inflater == null) {
33+
inflater = new UnsafeInflater(true);
34+
}
35+
}
36+
try {
37+
byte[] output = new byte[1024];
38+
byte[] buffer = new byte[1024];
39+
long position = 0L;
40+
long length = data.length();
41+
do {
42+
if (inflater.needsInput()) {
43+
int remaining = (int) Math.min(buffer.length, length);
44+
if (remaining == 0) {
45+
break;
46+
}
47+
data.get(position, buffer, 0, remaining);
48+
length -= remaining;
49+
position += remaining;
50+
inflater.setInput(buffer, 0, remaining);
51+
}
52+
int count = inflater.inflate(output);
53+
if (count != 0) {
54+
out.write(output, 0, count);
55+
}
56+
} while (!inflater.finished() && !inflater.needsDictionary());
57+
} catch (DataFormatException e) {
58+
String s = e.getMessage();
59+
throw (ZipException) new ZipException(s != null ? null : "Invalid ZLIB data format").initCause(e);
60+
} finally {
61+
end:
62+
{
63+
if (inflaters.size() < 32) {
64+
synchronized (inflaters) {
65+
if (inflaters.size() < 32) {
66+
inflater.fastReset();
67+
inflaters.push(inflater);
68+
break end;
69+
}
70+
}
71+
}
72+
inflater.end();
73+
}
74+
}
75+
return BufferData.wrap(out.toByteArray());
76+
}
77+
}

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);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
/**
58+
* {@link #reset()} but without synchronization.
59+
*/
60+
public void fastReset() {
61+
try {
62+
Object zsRefValue = UNSAFE.getObject(this, off_zsRef);
63+
if (mh_address == null) {
64+
MethodHandles.Lookup lookup = UnsafeUtil.lookup();
65+
mh_address = lookup.findGetter(zsRefValue.getClass(), "address", long.class);
66+
}
67+
long addressRet = (long) mh_address.invoke(zsRefValue);
68+
mh_reset.invoke(addressRet);
69+
UNSAFE.putObject(this, off_input, DEFAULT_BUF);
70+
UNSAFE.putObject(this, off_inputArray, null);
71+
UNSAFE.putBoolean(this, off_finished, false);
72+
UNSAFE.putBoolean(this, off_needDict, false);
73+
UNSAFE.putLong(this, off_bytesRead, 0);
74+
UNSAFE.putLong(this, off_bytesWritten, 0);
75+
} catch (Throwable ex) {
76+
initFail = true;
77+
throw new IllegalStateException("Failed to reset unsafely. " + ERR_SUFFIX, ex);
78+
}
79+
}
80+
}

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)