From e9583ab3ea69c50aa79e7aa32e51d4b3bf48e2a2 Mon Sep 17 00:00:00 2001 From: cpovirk Date: Mon, 12 Jun 2023 12:00:42 -0700 Subject: [PATCH] Use Java's hardware-accelerated CRC32C implementation where available. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first use of a Java 9 API in Guava, but we use the API only when it's available, so we maintain compatibility with Java 8. Use of Java 9 APIs is relevant to https://github.com/google/guava/issues/6549 and https://github.com/google/guava/issues/3990 (and also https://github.com/mojohaus/animal-sniffer/issues/67). I didn't make the same change for `guava-android`, which [will add `java.util.zip.CRC32C` in API Level 34](https://developer.android.com/reference/java/util/zip/CRC32C). I don't know if Android is providing similar performance improvements, so it might not even matter. But even if I wanted to do it, I can't with my current approach, which relies on `MethodHandle`—unless I want to make even the usage of `MethodHandle` conditional on a reflective check :) RELNOTES=`hash`: Enhanced `crc32c()` to use Java's hardware-accelerated implementation where available. PiperOrigin-RevId: 539722059 --- .../google/common/hash/ChecksumBenchmark.java | 19 ++++ .../common/hash/IgnoreJRERequirement.java | 30 +++++++ android/pom.xml | 2 +- .../google/common/hash/ChecksumBenchmark.java | 19 ++++ guava/src/com/google/common/hash/Hashing.java | 86 ++++++++++++++++++- .../common/hash/IgnoreJRERequirement.java | 30 +++++++ pom.xml | 2 +- 7 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 android/guava/src/com/google/common/hash/IgnoreJRERequirement.java create mode 100644 guava/src/com/google/common/hash/IgnoreJRERequirement.java diff --git a/android/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java b/android/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java index 4bb0031dc2b0..26c0584137b8 100644 --- a/android/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java +++ b/android/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java @@ -22,6 +22,7 @@ import java.util.Random; import java.util.zip.Adler32; import java.util.zip.CRC32; +import java.util.zip.CRC32C; import java.util.zip.Checksum; /** @@ -69,6 +70,24 @@ byte crc32Checksum(int reps) throws Exception { return result; } + // CRC32C + + @Benchmark + byte crc32cHashFunction(int reps) { + return runHashFunction(reps, Hashing.crc32c()); + } + + @Benchmark + byte crc32cChecksum(int reps) throws Exception { + byte result = 0x01; + for (int i = 0; i < reps; i++) { + CRC32C checksum = new CRC32C(); + checksum.update(testBytes, 0, testBytes.length); + result = (byte) (result ^ checksum.getValue()); + } + return result; + } + // Adler32 @Benchmark diff --git a/android/guava/src/com/google/common/hash/IgnoreJRERequirement.java b/android/guava/src/com/google/common/hash/IgnoreJRERequirement.java new file mode 100644 index 000000000000..0de7c9a92439 --- /dev/null +++ b/android/guava/src/com/google/common/hash/IgnoreJRERequirement.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.common.hash; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Target; + +/** + * Disables Animal Sniffer's checking of compatibility with older versions of Java/Android. + * + *

Each package's copy of this annotation needs to be listed in our {@code pom.xml}. + */ +@Target({METHOD, CONSTRUCTOR, TYPE}) +@ElementTypesAreNonnullByDefault +@interface IgnoreJRERequirement {} diff --git a/android/pom.xml b/android/pom.xml index e14bb1967dc0..c57f5846c067 100644 --- a/android/pom.xml +++ b/android/pom.xml @@ -176,7 +176,7 @@ animal-sniffer-maven-plugin 1.23 - com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement + com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement true org.codehaus.mojo.signature diff --git a/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java b/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java index 4bb0031dc2b0..26c0584137b8 100644 --- a/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java +++ b/guava-tests/benchmark/com/google/common/hash/ChecksumBenchmark.java @@ -22,6 +22,7 @@ import java.util.Random; import java.util.zip.Adler32; import java.util.zip.CRC32; +import java.util.zip.CRC32C; import java.util.zip.Checksum; /** @@ -69,6 +70,24 @@ byte crc32Checksum(int reps) throws Exception { return result; } + // CRC32C + + @Benchmark + byte crc32cHashFunction(int reps) { + return runHashFunction(reps, Hashing.crc32c()); + } + + @Benchmark + byte crc32cChecksum(int reps) throws Exception { + byte result = 0x01; + for (int i = 0; i < reps; i++) { + CRC32C checksum = new CRC32C(); + checksum.update(testBytes, 0, testBytes.length); + result = (byte) (result ^ checksum.getValue()); + } + return result; + } + // Adler32 @Benchmark diff --git a/guava/src/com/google/common/hash/Hashing.java b/guava/src/com/google/common/hash/Hashing.java index c348e6c29fa0..f2ec72f389ab 100644 --- a/guava/src/com/google/common/hash/Hashing.java +++ b/guava/src/com/google/common/hash/Hashing.java @@ -16,8 +16,13 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.lang.invoke.MethodType.methodType; import com.google.errorprone.annotations.Immutable; +import com.google.j2objc.annotations.J2ObjCIncompatible; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.security.Key; import java.util.ArrayList; import java.util.Arrays; @@ -399,6 +404,13 @@ public static HashFunction crc32c() { @Immutable private enum Crc32CSupplier implements ImmutableSupplier { + @J2ObjCIncompatible + JAVA_UTIL_ZIP { + @Override + public HashFunction get() { + return ChecksumType.CRC_32C.hashFunction; + } + }, ABSTRACT_HASH_FUNCTION { @Override public HashFunction get() { @@ -406,7 +418,26 @@ public HashFunction get() { } }; - static final HashFunction HASH_FUNCTION = values()[0].get(); + static final HashFunction HASH_FUNCTION = pickFunction().get(); + + private static Crc32CSupplier pickFunction() { + Crc32CSupplier[] functions = values(); + + if (functions.length == 1) { + // We're running under J2ObjC. + return functions[0]; + } + + // We can't refer to JAVA_UTIL_ZIP directly at compile time because of J2ObjC. + Crc32CSupplier javaUtilZip = functions[0]; + + try { + Class.forName("java.util.zip.CRC32C"); + return javaUtilZip; + } catch (ClassNotFoundException runningUnderJava8) { + return ABSTRACT_HASH_FUNCTION; + } + } } /** @@ -449,6 +480,13 @@ public Checksum get() { return new CRC32(); } }, + @J2ObjCIncompatible + CRC_32C("Hashing.crc32c()") { + @Override + public Checksum get() { + return Crc32cMethodHandles.newCrc32c(); + } + }, ADLER_32("Hashing.adler32()") { @Override public Checksum get() { @@ -463,6 +501,52 @@ public Checksum get() { } } + @J2ObjCIncompatible + @SuppressWarnings("unused") + private static final class Crc32cMethodHandles { + private static final MethodHandle CONSTRUCTOR = crc32cConstructor(); + + @IgnoreJRERequirement // https://github.com/mojohaus/animal-sniffer/issues/67 + static Checksum newCrc32c() { + try { + return (Checksum) CONSTRUCTOR.invokeExact(); + } catch (Throwable e) { + throwIfUnchecked(e); + // That constructor has no `throws` clause. + throw newLinkageError(e); + } + } + + private static MethodHandle crc32cConstructor() { + try { + Class clazz = Class.forName("java.util.zip.CRC32C"); + /* + * We can't cast to CRC32C at the call site because we support building with Java 8 + * (https://github.com/google/guava/issues/6549). So we have to use asType() to change from + * CRC32C to Checksum. This may carry some performance cost + * (https://stackoverflow.com/a/22321671/28465), but I'd have to benchmark more carefully to + * even detect it. + */ + return MethodHandles.lookup() + .findConstructor(clazz, methodType(void.class)) + .asType(methodType(Checksum.class)); + } catch (ClassNotFoundException e) { + // We check that the class is available before calling this method. + throw new AssertionError(e); + } catch (IllegalAccessException e) { + // That API is public. + throw newLinkageError(e); + } catch (NoSuchMethodException e) { + // That constructor exists. + throw newLinkageError(e); + } + } + + private static LinkageError newLinkageError(Throwable cause) { + return new LinkageError(cause.toString(), cause); + } + } + /** * Returns a hash function implementing FarmHash's Fingerprint64, an open-source algorithm. * diff --git a/guava/src/com/google/common/hash/IgnoreJRERequirement.java b/guava/src/com/google/common/hash/IgnoreJRERequirement.java new file mode 100644 index 000000000000..0de7c9a92439 --- /dev/null +++ b/guava/src/com/google/common/hash/IgnoreJRERequirement.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.common.hash; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Target; + +/** + * Disables Animal Sniffer's checking of compatibility with older versions of Java/Android. + * + *

Each package's copy of this annotation needs to be listed in our {@code pom.xml}. + */ +@Target({METHOD, CONSTRUCTOR, TYPE}) +@ElementTypesAreNonnullByDefault +@interface IgnoreJRERequirement {} diff --git a/pom.xml b/pom.xml index 352fb25404e3..5ee5d050f768 100644 --- a/pom.xml +++ b/pom.xml @@ -177,7 +177,7 @@ animal-sniffer-maven-plugin 1.23 - com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement + com.google.common.hash.IgnoreJRERequirement,com.google.common.io.IgnoreJRERequirement,com.google.common.reflect.IgnoreJRERequirement,com.google.common.testing.IgnoreJRERequirement true org.codehaus.mojo.signature