-
Enhancement
-
Resolution: Unresolved
-
P4
-
None
-
8, 11, 15, 17, 21
-
Fix Understood
ZipFile.getInputStream() tries to find a good size for sizing the internal buffer of the underlying InflaterInputStream. This buffer is used to read the compressed data from the associated InputStream. Unfortunately, ZipFile.getInputStream() uses CENLEN (i.e. the uncompressed size of a ZipEntry) instead of CENSIZ (i.e. the compressed size of a ZipEntry) to configure the input buffer and thus unnecessarily wastes memory, because the corresponding, compressed input data is at most CENSIZ bytes long.
After fixing this and doing some benchmarks, I realized that a much bigger problem is the continuous allocation of new, temporary input buffers for each new input stream. Assuming that a zip files usually has hundreds if not thousands of ZipEntries, I think it makes sense to cache these input buffers. Fortunately, ZipFile already has a built-in mechanism for such caching which it uses already for caching the Inflaters needed for each new input stream.
Adding such a cache for input stream buffers increases the speed of reading ZipEntries from an InputStream by roughly 5% (see benchmark results below). More importantly, it also decreases the memory consumption for each call to ZipFile.getInputStream() which can be quite significant if many ZipEntries are read from a ZipFile. One visible effect of caching the input buffers is that the manual JTreg test java/util/zip/ZipFile/TestZipFile.java, which regularly failed on my desktop with an OutOfMemoryError before, now reliably passes (this tests calls ZipFile.getInputStream() excessively).
The following JMH benchmark results show the time and memory used to read all bytes from a ZipEntry before and after the proposed change. The 'size' parameter denotes the uncompressed size of the corresponding ZipEntries.
In the "BEFORE" numbers yo ucan see the anomaly caused by using CENLEN instead of CENSIZ when looking at the "gc.alloc.rate.norm" values.
= AFTER =
Benchmark (size) Mode Cnt Score Error Units
ZipFileGetInputStream.readAllBytes 1024 avgt 3 13.031 ± 0.452 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1024 avgt 3 824.311 ± 0.027 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1024 avgt 3 27.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 1024 avgt 3 7.000 ms
ZipFileGetInputStream.readAllBytes 4096 avgt 3 20.018 ± 0.805 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 4096 avgt 3 824.289 ± 0.722 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 4096 avgt 3 15.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 4096 avgt 3 4.000 ms
ZipFileGetInputStream.readAllBytes 16384 avgt 3 48.916 ± 1.140 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 16384 avgt 3 824.263 ± 0.008 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 16384 avgt 3 6.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 16384 avgt 3 1.000 ms
ZipFileGetInputStream.readAllBytes 65536 avgt 3 192.815 ± 4.102 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 65536 avgt 3 824.012 ± 0.001 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 65536 avgt 3 ≈ 0 counts
ZipFileGetInputStream.readAllBytes 262144 avgt 3 755.713 ± 42.408 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 262144 avgt 3 824.047 ± 0.003 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 262144 avgt 3 ≈ 0 counts
ZipFileGetInputStream.readAllBytes 1048576 avgt 3 2989.236 ± 8.808 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1048576 avgt 3 824.184 ± 0.002 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1048576 avgt 3 ≈ 0 counts
= BEFORE =
Benchmark (size) Mode Cnt Score Error Units
ZipFileGetInputStream.readAllBytes 1024 avgt 3 13.577 ± 0.540 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1024 avgt 3 1872.673 ± 0.317 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1024 avgt 3 57.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 1024 avgt 3 15.000 ms
ZipFileGetInputStream.readAllBytes 4096 avgt 3 20.938 ± 0.577 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 4096 avgt 3 4945.793 ± 0.493 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 4096 avgt 3 102.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 4096 avgt 3 25.000 ms
ZipFileGetInputStream.readAllBytes 16384 avgt 3 51.348 ± 2.600 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 16384 avgt 3 17238.030 ± 3.183 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 16384 avgt 3 144.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 16384 avgt 3 33.000 ms
ZipFileGetInputStream.readAllBytes 65536 avgt 3 203.082 ± 7.046 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 65536 avgt 3 9035.475 ± 7.426 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 65536 avgt 3 18.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 65536 avgt 3 5.000 ms
ZipFileGetInputStream.readAllBytes 262144 avgt 3 801.928 ± 22.474 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 262144 avgt 3 9034.192 ± 0.047 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 262144 avgt 3 3.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 262144 avgt 3 1.000 ms
ZipFileGetInputStream.readAllBytes 1048576 avgt 3 3154.747 ± 57.588 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1048576 avgt 3 9032.194 ± 0.004 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1048576 avgt 3 ≈ 0 counts
After fixing this and doing some benchmarks, I realized that a much bigger problem is the continuous allocation of new, temporary input buffers for each new input stream. Assuming that a zip files usually has hundreds if not thousands of ZipEntries, I think it makes sense to cache these input buffers. Fortunately, ZipFile already has a built-in mechanism for such caching which it uses already for caching the Inflaters needed for each new input stream.
Adding such a cache for input stream buffers increases the speed of reading ZipEntries from an InputStream by roughly 5% (see benchmark results below). More importantly, it also decreases the memory consumption for each call to ZipFile.getInputStream() which can be quite significant if many ZipEntries are read from a ZipFile. One visible effect of caching the input buffers is that the manual JTreg test java/util/zip/ZipFile/TestZipFile.java, which regularly failed on my desktop with an OutOfMemoryError before, now reliably passes (this tests calls ZipFile.getInputStream() excessively).
The following JMH benchmark results show the time and memory used to read all bytes from a ZipEntry before and after the proposed change. The 'size' parameter denotes the uncompressed size of the corresponding ZipEntries.
In the "BEFORE" numbers yo ucan see the anomaly caused by using CENLEN instead of CENSIZ when looking at the "gc.alloc.rate.norm" values.
= AFTER =
Benchmark (size) Mode Cnt Score Error Units
ZipFileGetInputStream.readAllBytes 1024 avgt 3 13.031 ± 0.452 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1024 avgt 3 824.311 ± 0.027 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1024 avgt 3 27.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 1024 avgt 3 7.000 ms
ZipFileGetInputStream.readAllBytes 4096 avgt 3 20.018 ± 0.805 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 4096 avgt 3 824.289 ± 0.722 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 4096 avgt 3 15.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 4096 avgt 3 4.000 ms
ZipFileGetInputStream.readAllBytes 16384 avgt 3 48.916 ± 1.140 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 16384 avgt 3 824.263 ± 0.008 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 16384 avgt 3 6.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 16384 avgt 3 1.000 ms
ZipFileGetInputStream.readAllBytes 65536 avgt 3 192.815 ± 4.102 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 65536 avgt 3 824.012 ± 0.001 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 65536 avgt 3 ≈ 0 counts
ZipFileGetInputStream.readAllBytes 262144 avgt 3 755.713 ± 42.408 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 262144 avgt 3 824.047 ± 0.003 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 262144 avgt 3 ≈ 0 counts
ZipFileGetInputStream.readAllBytes 1048576 avgt 3 2989.236 ± 8.808 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1048576 avgt 3 824.184 ± 0.002 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1048576 avgt 3 ≈ 0 counts
= BEFORE =
Benchmark (size) Mode Cnt Score Error Units
ZipFileGetInputStream.readAllBytes 1024 avgt 3 13.577 ± 0.540 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1024 avgt 3 1872.673 ± 0.317 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1024 avgt 3 57.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 1024 avgt 3 15.000 ms
ZipFileGetInputStream.readAllBytes 4096 avgt 3 20.938 ± 0.577 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 4096 avgt 3 4945.793 ± 0.493 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 4096 avgt 3 102.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 4096 avgt 3 25.000 ms
ZipFileGetInputStream.readAllBytes 16384 avgt 3 51.348 ± 2.600 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 16384 avgt 3 17238.030 ± 3.183 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 16384 avgt 3 144.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 16384 avgt 3 33.000 ms
ZipFileGetInputStream.readAllBytes 65536 avgt 3 203.082 ± 7.046 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 65536 avgt 3 9035.475 ± 7.426 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 65536 avgt 3 18.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 65536 avgt 3 5.000 ms
ZipFileGetInputStream.readAllBytes 262144 avgt 3 801.928 ± 22.474 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 262144 avgt 3 9034.192 ± 0.047 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 262144 avgt 3 3.000 counts
ZipFileGetInputStream.readAllBytes:·gc.time 262144 avgt 3 1.000 ms
ZipFileGetInputStream.readAllBytes 1048576 avgt 3 3154.747 ± 57.588 us/op
ZipFileGetInputStream.readAllBytes:·gc.alloc.rate.norm 1048576 avgt 3 9032.194 ± 0.004 B/op
ZipFileGetInputStream.readAllBytes:·gc.count 1048576 avgt 3 ≈ 0 counts