2

Is there any reason that:

public void test(Object object) {
    for (Object other : otherObjects) {
        object.equals(other);
    }
}

could be faster than:

public void test(Object object) {
    for (Object other : otherObjects) {
        other.equals(object);
    }
}

( as equals() is called on the same object in the first example?)

Related to Is CONSTANT.equals(VARIABLE) faster than VARIABLE.equals(CONSTANT)? and Interview : Java Equals.

Community
  • 1
  • 1
Michel Jung
  • 2,966
  • 6
  • 31
  • 51
  • 1
    @DiabolicWords: that's not what is asked. The op means that if you call, `x.equals(y)` and `x.equals(z)`, will that be faster than `y.equals(x)` and `z.equals(x)`. Since in the first case the address of the callee can remain pushed on the call stack for instance. – Willem Van Onsem Apr 17 '17 at 15:03
  • Thanks for the timely accept! – GhostCat Apr 20 '17 at 13:29

3 Answers3

3

There should be no difference for objects that implement equals in a way that it is useful (say, java.lang.String or java.lang.Integer).

However, you can't generalize this, because corner cases do exist. For example, if object passed in implements equality through identity, i.e.

@Override
public boolean equals(Object other) {
    return other == this; // identity equality
}

while objects in your collection perform a type check before returning a value, the first approach will require fewer CPU cycles, translating into faster execution.

A quick performance benchmark of this corner case shows an improvement of about 25% (45 ms to 34 ms).

Note that this is only a corner case, not a general rule. You should not rely on it for performance optimizations.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • I expect it could make a difference as I commented on OP: *one possible reason is that you're calling the same override over and over.. this might trigger optimisations in the jit and be more (instruction-) cache friendly depending on your implementation* - any chance of that in current jvms? – BeyelerStudios Apr 17 '17 at 15:15
  • @BeyelerStudios JIT has to be out before we begin performance comparison, otherwise we'll get into another corner case (e.g. because we are dealing with methods that are not called vs. calling these methods for the first time). Instruction cache shouldn't be an issue, because in order for the difference to be measurable the number of objects needs to be in the hundreds, and having that many classes in a heterogeneous collection is not practical. – Sergey Kalinichenko Apr 17 '17 at 15:21
  • This confuses me when talking about performance in java: not talking about actual performance. I agree within those limits, but believe that the first version is enabling for an optimisation to happen (i.e. inlining the function call for a specific object type) while the second one is preventing it. – BeyelerStudios Apr 17 '17 at 15:28
  • @dasblinkenlight Good evening Mr. Licht ... like your answer. As I know that you are a good observer; feel free to have a quick look at my take and give some feedback ;-) – GhostCat Apr 17 '17 at 18:25
  • @GhostCat Good evening! I like your observation about `Set` a lot, because it demonstrates how algorithm optimizations beat code optimizations. Your observation about JIT/bytecode brings a relevant perspective to the discussion of micro-benchmarks, which are very easy to get wrong. – Sergey Kalinichenko Apr 17 '17 at 19:09
3

This question has a lot of dimensions; let's address them one by one:

Yes, polymorphism could translate into performance gains. Option 1 calls the same method repeatedly. Option 2 could theoretically invoke a different equals implementation during each call. Thus Option 1 becomes subject to "JIT" compilation much earlier.

But let's look into the "practical" relevance of the whole thing:

Looking at compiled bytecode is interesting; but not really that helpful in the real world; as there are two cases:

  • bytecode is just exected a few times - JIT doesn't kick. Does it make a difference then, if that method needs 500 or 600 ns to finish?
  • bytecode is executed often enough for the JIT to work on it. Then you have no idea what will come out of that anyway.

Beyond that:

  • if you would be concerned about performance in the real world ... then you use neither of those two options; because in the real world, you would make sure that this incoming collection would be a set. If contains() is such a common operation in your code that you start worrying about option 1 vs option 2, then you would for sure go for O(1) set operation instead!
  • and as we have seen, option 1 might occasionally perform better. And: it is easier to read (much less surprising than option 2). And it has a slightly different probabilities regarding NPEs.

Long story short: all the arguments are in favor for option 1. Clear winner here!

Finally: of course, benchmarking this is an interesting exercise; but keep in mind: correct benchmarking is hard!

Community
  • 1
  • 1
GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • Different `equals()` implementation is actually pretty obvious, but I didn't think of it when I posted the question :-) I thought I learned to use Option 1 instead of 2 a long time ago and today, as I was reviewing code, I wondered why. Unfortunately, I couldn't find any answer. I wondered if could also be linked to CPU caching or something? – Michel Jung Apr 17 '17 at 21:13
0

No there is no reason. This is the equals() method from class Object

public boolean equals(Object obj) {
    return (this == obj);
}

Take this code

public class TestClass {
    private Object[] otherObjects;

    public void test1(Object object) {
        for (Object other : otherObjects) {
            object.equals(other);
        }
    }

    public void test2(Object object) {
        for (Object other : otherObjects) {
            other.equals(object);
        }
    }

}

Equivalent bytecode is

Compiled from "TestClass.java"
public class rs.djm.TestClass {
  public rs.djm.TestClass();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

  public void test1(java.lang.Object);
    Code:
       0: aload_0
       1: getfield      #18                 // Field otherObjects:[Ljava/lang/Object;
       4: dup
       5: astore        5
       7: arraylength
       8: istore        4
      10: iconst_0
      11: istore_3
      12: goto          29
      15: aload         5
      17: iload_3
      18: aaload
      19: astore_2
      20: aload_1
      21: aload_2
      22: invokevirtual #20                 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
      25: pop
      26: iinc          3, 1
      29: iload_3
      30: iload         4
      32: if_icmplt     15
      35: return

  public void test2(java.lang.Object);
    Code:
       0: aload_0
       1: getfield      #18                 // Field otherObjects:[Ljava/lang/Object;
       4: dup
       5: astore        5
       7: arraylength
       8: istore        4
      10: iconst_0
      11: istore_3
      12: goto          29
      15: aload         5
      17: iload_3
      18: aaload
      19: astore_2
      20: aload_2
      21: aload_1
      22: invokevirtual #20                 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
      25: pop
      26: iinc          3, 1
      29: iload_3
      30: iload         4
      32: if_icmplt     15
      35: return
}

The difference in bytecode are on lines 19, 20 and 21.


And those links you have referenced they are not about speed but about the NullPointerException and Strings in Java.

djm.im
  • 3,295
  • 4
  • 30
  • 45
  • 1
    Ok, but just because my example code accepts `Object` doesn't mean you can't pass a subclass of it (so, any type) that overrides `equals()` – Michel Jung Apr 17 '17 at 19:48