Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8321620: Optimize JImage decompressors #16556

Closed
wants to merge 7 commits into from

Conversation

Glavo
Copy link
Contributor

@Glavo Glavo commented Nov 8, 2023

This PR significantly speeds up decompressing resources in Jimage while significantly reducing temporary memory allocations in the process.

This will improve startup speed for runtime images generated using jlink --compress 1 and jlink --compress 2 .

I generated a runtime image containing javac using jlink --compress 1 --add-modules jdk.compiler and tested the time it took to compile a simple HelloWorld program 20 times using perf stat -r20 javac /dev/shm/HelloWorld.java, this PR reduces the total time taken from 17830ms to 13598ms (31.12% faster).


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8321620: Optimize JImage decompressors (Enhancement - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/16556/head:pull/16556
$ git checkout pull/16556

Update a local copy of the PR:
$ git checkout pull/16556
$ git pull https://git.openjdk.org/jdk.git pull/16556/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 16556

View PR using the GUI difftool:
$ git pr show -t 16556

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/16556.diff

Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Nov 8, 2023

👋 Welcome back Glavo! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Nov 8, 2023

@Glavo The following label will be automatically applied to this pull request:

  • core-libs

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the core-libs core-libs-dev@openjdk.org label Nov 8, 2023
@Glavo
Copy link
Contributor Author

Glavo commented Nov 8, 2023

I generated runtime images using jlink --compress (1|2) --add-modules java.se,jdk.unsupported,jdk.management and then ran the following JMH benchmark:

@Warmup(iterations = 10, time = 2)
@Measurement(iterations = 5, time = 3)
@Fork(value = 1, jvmArgsAppend = {"-XX:+UseG1GC", "-Xms8g", "-Xmx8g", "--add-exports=java.base/jdk.internal.jimage=ALL-UNNAMED"})
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class Decompress {

    private static final ImageReader READER = ImageReaderFactory.getImageReader();
    private static final ImageLocation LOC = READER.findLocation("java.base", "java/lang/String.class");

    @Benchmark
    public ByteBuffer test() {
        return READER.getResourceBuffer(LOC);
    }

}

This is the result:

String Share
                                                            (Baseline)                        (Current)
Benchmark                           Mode  Cnt       Score       Error   Units        Score       Error   Units
Decompress.test                     avgt    5  184243.403 ± 3643.983   ns/op     35176.514 ±   507.618   ns/op   (-80.91%)
Decompress.test:gc.alloc.rate       avgt    5    3263.730 ±   64.431  MB/sec      3143.058 ±    45.330  MB/sec
Decompress.test:gc.alloc.rate.norm  avgt    5  630544.422 ±    0.008    B/op    115936.081 ±     0.001    B/op   (-81.61%)
Decompress.test:gc.count            avgt    5      10.000             counts         9.000              counts
Decompress.test:gc.time             avgt    5      14.000                 ms        13.000                  ms


Zip

Benchmark                           Mode  Cnt       Score       Error   Units        Score       Error   Units
Decompress.test                     avgt    5  194237.534 ±  1026.180   ns/op   152855.728 ± 16058.780   ns/op   (-21.30%)
Decompress.test:gc.alloc.rate       avgt    5    1197.700 ±     6.306  MB/sec      464.278 ±    47.465  MB/sec
Decompress.test:gc.alloc.rate.norm  avgt    5  243953.338 ±     5.810    B/op    74376.291 ±     2.175    B/op   (-69.51%)
Decompress.test:gc.count            avgt    5       2.000              counts        1.000              counts
Decompress.test:gc.time             avgt    5       4.000                  ms        3.000                  ms

The results show that memory allocation is reduced by more than 70% while decompression speed is significantly improved.

@Glavo
Copy link
Contributor Author

Glavo commented Nov 8, 2023

I ran the tier1 and tier2 tests and there were no new errors.

Comment on lines +110 to +111
bytesOut[bytesOutOffset++] = (byte) ((count >> 8) & 0xff);
bytesOut[bytesOutOffset++] = (byte) (count & 0xff);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably use ByteArray.setUnsignedShort:

Suggested change
bytesOut[bytesOutOffset++] = (byte) ((count >> 8) & 0xff);
bytesOut[bytesOutOffset++] = (byte) (count & 0xff);
ByteArray.setUnsignedShort(bytesOut, bytesOutOffset, count);
bytesOutOffset += 2;

Same remark elsewhere.

Copy link
Contributor Author

@Glavo Glavo Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These classes need to be compiled with --release 8, so I can't use the ByteArray.

@Glavo
Copy link
Contributor Author

Glavo commented Nov 9, 2023

I generated a runtime image containing javac using jlink --compress (0|1) --add-modules jdk.compiler and tested the time it took to compile a simple HelloWorld program 20 times using perf stat -r20 javac /dev/shm/HelloWorld.java. This is the result:

Baseline, No Compress: 10829ms

String Share:

  • Baseline: 17830ms
  • This PR: 13598ms

Zip:

  • Baseline: 12350ms
  • This PR: 12279ms

You can see that in this test, this PR made the runtime image compressed using string share 30% faster.

@@ -344,6 +345,23 @@ public String getString(int offset) {
return ImageStringsReader.stringFromByteBuffer(strings, offset);
}

int getStringMUTF8(int offset, byte[] bytesOut, int bytesOutOffset) {
if (offset < 0 || offset >= strings.limit()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use Objects::checkIndex here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use Objects::checkIndex here instead?

No, because these classes need to be compiled with --release 8.

Javadoc:

/**
 * @implNote This class needs to maintain JDK 8 source compatibility.
 *
 * It is used internally in the JDK to implement jimage/jrtfs access,
 * but also compiled and delivered as part of the jrtfs.jar to support access
 * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 */

out.writeShort(count);
int offset, long originalSize) throws IOException {
if (originalSize > Integer.MAX_VALUE) {
throw new OutOfMemoryError("Required array size too large");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct exception type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the correct exception type?

I'm not sure, but I think it shouldn't matter, since the exception is never really thrown here for a valid jimage file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's common to throw OutOfMemoryError when a requested array size would be too large, either due overflow or because some other limitation (see StringUTF16::newBytesFor or String::replace for some examples).

@cl4es
Copy link
Member

cl4es commented Nov 20, 2023

I'll need some time to review this thoroughly but skimming through it it seems like a very nice enhancement, making --compress more appealing - great work!

One gotcha in this area is that jlink by default does not recreate the CDS archive. Startup might gain from generating out the default CDS archive (java -Xshare:dump), but not doing so is a reasonable thing to do if you're aiming for minimal static footprint. But to make comparisons fair you need to take care to compare apples to apples: is the uncompressed baseline a similar jlink image generated without --compress? Or the default JDK image (uncompressed, with CDS)?

}
}

private void check(byte[] flow, List<byte[]> arrays) {
List<Integer> d = CompressIndexes.decompressFlow(flow);
List<Integer> d = Arrays.stream(CompressIndexes.decompressFlow(flow)).boxed().toList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rewrite this as

int[] d = CompressIndexes.decompressFlow(flow);
int[] dd = new int[arrays.length];
for (int j = 0; j < arrays.length; j++) {
  byte[] b = arrays[j];
  dd[j] = CompressIndexes.decompress(b, 0);
}
if (!Arrays.equals(d, dd) {
//...
}

@Glavo
Copy link
Contributor Author

Glavo commented Nov 23, 2023

But to make comparisons fair you need to take care to compare apples to apples: is the uncompressed baseline a similar jlink image generated without --compress? Or the default JDK image (uncompressed, with CDS)?

The uncompressed runtime image used for comparison was also generated using jlink, without using CDS, so the comparison should be fair.

@Glavo
Copy link
Contributor Author

Glavo commented Dec 8, 2023

Does anyone want to take a look at this PR?

@cl4es
Copy link
Member

cl4es commented Dec 8, 2023

Does anyone want to take a look at this PR?

RFE: https://bugs.openjdk.org/browse/JDK-8321620 - update the bug ID and this should PR should reach a wider audience. I'll have some time next week to help review.

@Glavo Glavo changed the title Optimize JImage decompressors 8321620: Optimize JImage decompressors Dec 8, 2023
@openjdk openjdk bot added the rfr Pull request is ready for review label Dec 8, 2023
@mlbridge
Copy link

mlbridge bot commented Dec 8, 2023

Webrevs

@bridgekeeper
Copy link

bridgekeeper bot commented Jan 6, 2024

@Glavo This pull request has been inactive for more than 4 weeks and will be automatically closed if another 4 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@Glavo
Copy link
Contributor Author

Glavo commented Jan 6, 2024

/open

@openjdk
Copy link

openjdk bot commented Jan 6, 2024

@Glavo This pull request is already open

@Glavo
Copy link
Contributor Author

Glavo commented Jan 8, 2024

Can anyone review this PR?

Copy link
Member

@cl4es cl4es left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to merge with master to get jlink to work, perhaps some local issue since Classfile API has been moving around.

While the improvement to --compress 1 are impressive, this compression mode is effectively deprecated. I'm not sure your improvements to it here will be enough to reverse that decision (it's still a bit behind on all measures, no?). Perhaps it would be better to split out those changes and move forward with and focus this on the zip decompressor enhancements?

@Glavo
Copy link
Contributor Author

Glavo commented Jan 12, 2024

@cl4es

While the improvement to --compress 1 are impressive, this compression mode is effectively deprecated.

I noticed the deprecation warning before creating this PR, so I did some research.

This warning was introduced here: #11617. In the discussion of that PR I learned that the option --compress 1 was deprecated because it was not compression but string sharing. The string sharing plugin is not actually deprecated, it's just that the command line option may be renamed in the future.

@mlchung
Copy link
Member

mlchung commented Jan 12, 2024

The plan [1] is to remove the old compression values 0, 1, 2 in the future and only support the zip compression --compress zip-[0-9], i.e. the string sharing plugin will be removed as there isn't any known customer usage of this plugin. If you are aware of any customer using it, we can reevaluate.

[1] https://bugs.openjdk.org/browse/JDK-8301124?focusedId=14562770&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-14562770

@Glavo
Copy link
Contributor Author

Glavo commented Jan 12, 2024

The plan [1] is to remove the old compression values 0, 1, 2 in the future and only support the zip compression --compress zip-[0-9], i.e. the string sharing plugin will be removed as there isn't any known customer usage of this plugin. If you are aware of any customer using it, we can reevaluate.

[1] https://bugs.openjdk.org/browse/JDK-8301124?focusedId=14562770&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-14562770

I expected that few people use this option, because compared to zip, it is not superior in file size or decompression speed. However, I did some experiments in the JApp project and found that the file size using a mixture of string sharing + zstd compression was significantly smaller than using only zstd, so I thought it might be meaningful to study it further.

Anyway, these are things for the future and I want this PR to stay simple, so I'll just drop these modifications.

@Glavo
Copy link
Contributor Author

Glavo commented Jan 12, 2024

I created a new PR: #17405

I want to leave my changes to the string sharing plugin here so I can pick them up in the future.

@Glavo Glavo closed this Jan 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core-libs core-libs-dev@openjdk.org rfr Pull request is ready for review
Development

Successfully merging this pull request may close these issues.

6 participants