Java – why is my null check so slow?
So my code looks like this right now
public boolean in(TransactionType... types) { if (types == null || types.length == 0) return false; for (int i = 0; i < types.length; ++i) if (types[i] != null && types[i] == this) return true; return false; }
I changed it to this
public boolean in(TransactionType... types) { if (types == null || types.length == 0) return false; for (int i = 0; i < types.length; ++i) if (types[i] == this) return true; return false; }
(transactiontype is an enumeration containing about 30 values)
The result shocked me In all my tests, the speed of the second test increased by an order of magnitude I expect it to be twice as fast, but not an order of magnitude Why is it different? This is a very slow nullcheck, or what strange things will happen to additional array access?
My benchmark code looks like this
public class App { public enum TransactionType { A(1,"A","A"),B(3,"B","B"),C(5,"C","C"),D(6,"D","D"),E(7,"E","E"),F(8,"F","F"),G(9,"G","G"),H(10,"H","H"),I(11,"I","I"),J(12,"J","J"),K(13,"K","K"),L(14,"L","L"),M(15,"M","M"),N(16,"N","N"),O(17,"O","O"),P(18,"P","P"),Q(19,"Q","Q"),R(20,"R","R"),S(21,"S","S"),T(22,"T","T"),U(25,"U","U"),V(26,"V","V"),W(27,"W","W"),X(28,"X","X"),Y(29,"Y","Y"),Z(30,"Z","Z"),AA(31,"AA","AA"),AB(32,"AB","AB"),AC(33,"AC","AC"),AD(35,"AD","AD"),AE(36,"AE","AE"),AF(37,"AF","AF"),AG(38,"AG","AG"),AH(39,"AH","AH"),AI(40,"AI","AI"),AJ(41,"AJ","AJ"),AK(42,"AK","AK"),AL(43,"AL","AL"),AM(44,"AM","AM"),AN(45,"AN","AN"),AO(46,"AO","AO"),AP(47,"AP","AP"); public final static TransactionType[] aArray = { O,Z,N,Y,AB }; public final static TransactionType[] bArray = { J,P,AA,L,Q,M,K,AE,AK,AF,AD,AG,AH }; public final static TransactionType[] cArray = { S,U,V }; public final static TransactionType[] dArray = { A,B,D,G,C,E,T,R,I,F,H,AC,AI,AJ,AL,AM,AN,AO }; private int id; private String abbrev; private String name; private TransactionType(int id,String abbrev,String name) { this.id = id; this.abbrev = abbrev; this.name = name; } public boolean in(TransactionType... types) { if (types == null || types.length == 0) return false; for (int i = 0; i < types.length; ++i) if (types[i] == this) return true; return false; } public boolean inOld(TransactionType... types) { if (types == null || types.length == 0) return false; for (int i = 0; i < types.length; ++i) { if (types[i] != null && types[i] == this) return true; } return false; } } public static void main(String[] args) { for (int i = 0; i < 10; ++i) bench2(); for (int i = 0; i < 10; ++i) bench1(); } private static void bench1() { final TransactionType[] values = TransactionType.values(); long runs = 0; long currTime = System.currentTimeMillis(); while (System.currentTimeMillis() - currTime < 1000) { for (TransactionType value : values) { value.inOld(TransactionType.dArray); } ++runs; } System.out.println("old " + runs); } private static void bench2() { final TransactionType[] values = TransactionType.values(); long runs = 0; long currTime = System.currentTimeMillis(); while (System.currentTimeMillis() - currTime < 1000) { for (TransactionType value : values) { value.in(TransactionType.dArray); } ++runs; } System.out.println("new " + runs); } }
Here are the results of the benchmark run
new 20164901 new 20084651 new 45739657 new 45735251 new 45757756 new 45726575 new 45413016 new 45649661 new 45325360 new 45380665 old 2021652 old 2022286 old 2246888 old 2237484 old 2246172 old 2268073 old 2271554 old 2259544 old 2272642 old 2268579
This is using Oracle JDK 1.7 zero point six seven
Solution
Empty checking doesn't accomplish anything, and I'm surprised that it makes such a difference But I believe your comments basically answered your own questions
@Cogman wrote:
If you compile your class and print out the disassembled byte code of these two methods using javap, you will see:
public boolean in(App$TransactionType...); Code: 0: aload_1 1: ifnull 9 4: aload_1 5: arraylength 6: ifne 11 9: iconst_0 10: ireturn 11: iconst_0 12: istore_2 13: iload_2 14: aload_1 15: arraylength 16: if_icmpge 34 19: aload_1 20: iload_2 21: aaload 22: aload_0 23: if_acmpne 28 26: iconst_1 27: ireturn 28: iinc 2,1 31: goto 13 34: iconst_0 35: ireturn
And:
public boolean inOld(App$TransactionType...); Code: 0: aload_1 1: ifnull 9 4: aload_1 5: arraylength 6: ifne 11 9: iconst_0 10: ireturn 11: iconst_0 12: istore_2 13: iload_2 14: aload_1 15: arraylength 16: if_icmpge 40 19: aload_1 20: iload_2 21: aaload 22: ifnull 34 25: aload_1 26: iload_2 27: aaload 28: aload_0 29: if_acmpne 34 32: iconst_1 33: ireturn 34: iinc 2,1 37: goto 13 40: iconst_0 41: ireturn
Your new method removes six operations and a potential branch site
The cycle was tight before, but now it's very compact
I thought Java would use the two methods JIT, which are exactly the same Your time figures indicate otherwise
Some random numbers:
1.6. 33 32b:646100 vs 727173
1.6. 33 64b:1667665 vs 2668513
1.7. 67 32b:661003 vs 716417
1.7. 07 64b:1663926 vs 32493989
1.7. 60 64b:1700574 vs 32368506
1.8. 20 64b:1648382 vs 32222823
All 64 - bit JVMs perform both implementations faster than 32 - bit versions