On Thu, 6 Jul 2023 14:46:59 GMT, Pavel Rappo <pra...@openjdk.org> wrote:

>> I'd suggest replacing the calls to `valuesMatch` with `Objects.equals` and 
>> remove the `valuesMatch` method as unnecessary.
>
>> I'd suggest replacing the calls to `valuesMatch` with `Objects.equals` and 
>> remove the `valuesMatch` method as unnecessary.
> 
> I'll do something about them soon, Roger. But first I need to understand 
> JDK-8015417 better, as it also affects other similar PRs already in review or 
> in the works.

> I wrote a little case study on `Objects::equals` that talks about how it 
> should optimize, when it does, why it doesn’t, and how (maybe) to fix that.
> 
> https://cr.openjdk.org/~jrose/jvm/equals-profile.html 
> https://cr.openjdk.org/~jrose/jvm/equals-profile.md
> 
> This is also attached to the JBS bug.
> 
> The work on [JDK-8026251](https://bugs.openjdk.org/browse/JDK-8026251) with 
> the `TypeProfileLevel` switch bring us closer to correctly optimizing 
> `Objects::equals` in more cases. Sadly, JDK refactoring by itself will not 
> get all the way to where we want to go. The JVM’s profiling logic needs 
> tweaking.

This PR has turned into an impromptu discussion on JDK-8015417. I think it's 
reasonable to continue it here, rather than to fork a separate discussion, 
because of (i) the relevance, (ii) context, and (iii) the fact that I have 
already pointed someone to this thread elsewhere.

Below is my mental model which I built based on your excellent write-up, John; 
is this model correct?

1. Inside a static method of a utility class, such as `java.util.Objects`,  
`java.util.Arrays`, and `jdk.internal.util.ArraysSupport`, that have one or 
more parameters of `java.lang.Object` or arrays thereof, "observation of a 
static type" fails because `java.lang.Object` is the most inexact type possible.

2. Generics that might be available in the context of a call site of the said 
method do not _currently_ contribute to "observation of a static type" inside 
the said method:

        T obj = ...
        return Objects.hashCode(obj);

   It would still be true inside the method, if it used someone else's or its 
own generic parameters, as for example, `java.util.Objects.compare` does:

        public static <T> int compare(T a, T b, Comparator<? super T> c) {
            return (a == b) ? 0 :  c.compare(a, b);
        }

   In the sense of being opposite to "specific", generics are as "generic" as 
`java.lang.Object` and are not reifiable even for optimization purposes.

3. The above two points conclude the "observation of a static type" part of the 
issue. Our only hope is now (dynamic) profiling followed by successful type 
speculation.

4. Hand-inlining the said method is necessary but not sufficient to win the 
optimization. One way to avoid the need for hand-inlining is to annotate that 
method with `@ForceInline`.

5. Profiling/speculation leads to desired devirtualization of the call to an 
instance method, if the reference through which the call is made refers to an 
instance of an exact type (i.e. a `final` class or a primitive[^*]). Note: the 
reference itself does not need to be of an exact type. For example, this will 
do:

        Object s = "myString";
        ...
        java.util.Objects.hashCode(s);

4. Inside a general-purpose method, such as `Objects.hashCode(Object)`, 
profiling is noisy, and thus is unlikely to pan out a successful speculation. 
Outside such a general-purpose method, at its call site, profiling could be 
less noisy.

[^*]:  It cannot be primitive in this case, because we are only concerned with 
methods that accept `java.lang.Object`.
----

Let's further test that model by checking its predictions.

1. Consider mainline `java.util.AbstractMap.SimpleEntry`:

        public boolean equals(Object o) {
            return o instanceof Map.Entry<?, ?> e
                    && eq(key, e.getKey())
                    && eq(value, e.getValue());
        }

        /**
         * Utility method for SimpleEntry and SimpleImmutableEntry.
         * Test for equality, checking for nulls.
         *
         * NB: Do not replace with Object.equals until JDK-8015417 is resolved.
         */
        private static boolean eq(Object o1, Object o2) {
            return o1 == null ? o2 == null : o1.equals(o2);
        }

   Since everything here is either of type `java.lang.Object` or a wildcard, no 
information is contributed to "observation of a static type". That `eq` method 
is likely is as bad as `Objects.equals`, assuming our runtime operates on many 
maps of different parameterizations leading to a noisy profile. Which means 
that in this case, we can reasonably refactor that `equals` as follows:

        public boolean equals(Object o) {
            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }

   Similar reasoning could be applied to that same class' `hashCode`, to 
refactor it from this:

        public int hashCode() {
            return (key   == null ? 0 :   key.hashCode()) ^
                   (value == null ? 0 : value.hashCode());
        }

   to this:

        public int hashCode() {
            return (Objects.hashCode(key)) ^
                   (Objects.hashCode(value));
        }

   Unlike the previous example, however, where we trade one inlining for 
another, here we might lose the first layer of inlining. But assuming that 
`Objects.hashCode` is relatively small and additionally might get annotated 
with `@ForceInline` as a result of this discussion later on, it will be inlined.

2. Consider an opposite example:

        public class ECFieldF2m implements ECField {
            ...
            private BigInteger rp;
            ...
            public int hashCode() {
                int value = m << 5;
                value += (rp==null? 0:rp.hashCode());
                // no need to involve ks here since ks and rp
                // should be equivalent.
                return value;
            }
        }

   Would it be reasonable to change `hashCode` like this?

        public int hashCode() {
            int value = m << 5;
            value += Objects.hashCode(rp);
            // no need to involve ks here since ks and rp
            // should be equivalent.
            return value;
        }

   Firstly, we lose inlining. Secondly, the fact that `rp` is `BigInteger` is 
no longer immediately available to the actual hash code computation. Assuming 
that `java.util.Objects.hashCode` will be inlined at runtime, and given enough 
invocations, it will be profiled and subjected to class hierarchy analysis 
(CHA), which in the absence of `BigInteger.hashCode` overrides will hopefully 
determine that the call could be further inlined.

-------------

PR Review Comment: https://git.openjdk.org/jdk/pull/14752#discussion_r1254659913

Reply via email to