Monday, September 24, 2012

Better to call getters internally instead of direct instance variable references

I often see people using a coding standard that specifies to put some sort of hungarian-like prefix on instance variables, so if you write code that directly references instance variables, you can distinguish those references from local variables. I think that coding standards are generally a good thing, but I've never liked any variant of hungarian notation, as I think it's more important to focus on the interpretation of variables in a domain, not necessarily their implementation. That leads me to define variables that clearly reflect what they represent, in a readable fashion.

Although I've always felt this way, I've hesitated to argue strongly for this because I expected people would argue that calling getters would be slower than direct references, and I'd always thought they would likely be right. There used to be talk about the Java compiler "inlining" methods if they were marked as final, but I've since realized that all of these are misconceptions.

If we're talking about pure Java bytecode running in a VM, then getters will definitely be slower. However, the reality is that any bytecode that gets run a significant number of times in a JVM will get processed by the Hotspot compiler in a completely reasonable fashion.

For instance, don't you think it would be reasonable for the JVM to inline getter code, whether it's marked "final" or not?  In fact, that's exactly what the Hotspot compiler does, after it's executed a getter just a few times.

As a result, if you believe as I do that code is more understandable if instance variables are always accessed through getters, then you don't have to worry about this being slower than direct references, because the resulting code at runtime is virtually identical.

How about if I prove it to you?

Let's first start with a couple of trivial foundation classes that I use in a variety of timing tests.
package timings;

public class TimingContainer {
    private int         iterations;
    private String      label;
    private TimingTest  timingTest;

    public TimingContainer(int iterations, String label, TimingTest timingTest) {
        this.iterations = iterations;
        this.label      = label;
        this.timingTest = timingTest;
    }

    public void run() {
        long    totalns = 0;
        for (int ctr = 0; ctr < iterations; ++ ctr) {
            long startTime  = System.nanoTime();
            timingTest.run();
            totalns += (System.nanoTime() - startTime);
        }
        System.out.println(label + ":" + (totalns / iterations));
    }
}
Notice that I use nanoseconds, as opposed to milliseconds. The last I heard, the millisecond timer was unreliable on Windows, and nanoseconds are better for measuring smaller code blocks.


package timings;

public interface TimingTest {
    public abstract void run();
}
The "TimingTest" interface is pretty simple. You could just as easily use "Runnable", but I like defining an interface to reflect this. The name could be better.

Here's my timing test class specifically for testing getter vs. direct timings.
package timings;

public class GetterSetterTimings {

    public static void main(String[] args) {
        GetterSetterTimings timings = new GetterSetterTimings(args);
        timings.go();
    }
    
    public GetterSetterTimings(String[] args) { }
    
    private void go() {
        final IntContainer    intContainer    = new IntContainer();
        intContainer.int1(9).int2(33).int3(35).int4(11).int5(99).int6(104).int7(4064).int8(22).int9(44);

        final StringContainer   stringContainer = new StringContainer();
        stringContainer.string1("abc").string2("xxxxxxxxxxxxxx").string3("z").string4("asdf").string5("33333333333333333").
            string6("ljsdflkjsflksdfj").string7("fffffffffffffffff").string8("b").string9("vvvvvvvvvvvvvvvvvv");
        
        for (int ctr = 0; ctr < 10; ++ ctr) {
            testWithGS(intContainer);
            testWithVars(intContainer);
            testWithGS(stringContainer);
            testWithVars(stringContainer);
        }
        
        int     iters   = 100000000;

        new TimingContainer(iters, "intgetterssetters", new TimingTest() {
            public void run() {
                testWithGS(intContainer);
            }
        }).run();
        
        new TimingContainer(iters, "intvars", new TimingTest() {
            public void run() {
                testWithVars(intContainer);
            }
        }).run();

        new TimingContainer(iters, "strgetterssetters", new TimingTest() {
            public void run() {
                testWithGS(stringContainer);
            }
        }).run();
        
        new TimingContainer(iters, "strvars", new TimingTest() {
            public void run() {
                testWithVars(stringContainer);
            }
        }).run();

    }
    
    public int testWithGS(IntContainer intContainer) {
        return intContainer.computeValueWithGS();
    }
    
    public int testWithVars(IntContainer intContainer) {
        return intContainer.computeValueWithVars();
    }

    public String testWithGS(StringContainer stringContainer) {
        return stringContainer.computeValueWithGS();
    }
    
    public String testWithVars(StringContainer stringContainer) {
        return stringContainer.computeValueWithVars();
    }

    public static class IntContainer {
        private int int1;
        private int int2;
        private int int3;
        private int int4;
        private int int5;
        private int int6;
        private int int7;
        private int int8;
        private int int9;
        
        public int getInt1() { return int1; }
        public int getInt2() { return int2; }
        public int getInt3() { return int3; }
        public int getInt4() { return int4; }
        public int getInt5() { return int5; }
        public int getInt6() { return int6; }
        public int getInt7() { return int7; }
        public int getInt8() { return int8; }
        public int getInt9() { return int9; }
        
        public void setInt1(int int1) { this.int1 = int1; }
        public void setInt2(int int2) { this.int2 = int2; }
        public void setInt3(int int3) { this.int3 = int3; }
        public void setInt4(int int4) { this.int4 = int4; }
        public void setInt5(int int5) { this.int5 = int5; }
        public void setInt6(int int6) { this.int6 = int6; }
        public void setInt7(int int7) { this.int7 = int7; }
        public void setInt8(int int8) { this.int8 = int8; }
        public void setInt9(int int9) { this.int9 = int9; }

        public IntContainer int1(int int1) { this.int1 = int1; return this; }
        public IntContainer int2(int int2) { this.int2 = int2; return this; }
        public IntContainer int3(int int3) { this.int3 = int3; return this; }
        public IntContainer int4(int int4) { this.int4 = int4; return this; }
        public IntContainer int5(int int5) { this.int5 = int5; return this; }
        public IntContainer int6(int int6) { this.int6 = int6; return this; }
        public IntContainer int7(int int7) { this.int7 = int7; return this; }
        public IntContainer int8(int int8) { this.int8 = int8; return this; }
        public IntContainer int9(int int9) { this.int9 = int9; return this; }

        public int computeValueWithGS() {
            return getInt1() + getInt2() + getInt3() + getInt4() + getInt5() + getInt6() + getInt7() + getInt8() + getInt9();
        }
        
        public int computeValueWithVars() {
            return int1 + int2 + int3 + int4 + int5 + int6 + int7 + int8 + int9;
        }
    }
    
    public static class StringContainer {
        private String string1;
        private String string2;
        private String string3;
        private String string4;
        private String string5;
        private String string6;
        private String string7;
        private String string8;
        private String string9;
        
        public String getString1() { return string1; }
        public String getString2() { return string2; }
        public String getString3() { return string3; }
        public String getString4() { return string4; }
        public String getString5() { return string5; }
        public String getString6() { return string6; }
        public String getString7() { return string7; }
        public String getString8() { return string8; }
        public String getString9() { return string9; }
        
        public void setString1(String string1) { this.string1 = string1; }
        public void setString2(String string2) { this.string2 = string2; }
        public void setString3(String string3) { this.string3 = string3; }
        public void setString4(String string4) { this.string4 = string4; }
        public void setString5(String string5) { this.string5 = string5; }
        public void setString6(String string6) { this.string6 = string6; }
        public void setString7(String string7) { this.string7 = string7; }
        public void setString8(String string8) { this.string8 = string8; }
        public void setString9(String string9) { this.string9 = string9; }

        public StringContainer string1(String string1) { this.string1 = string1; return this; }
        public StringContainer string2(String string2) { this.string2 = string2; return this; }
        public StringContainer string3(String string3) { this.string3 = string3; return this; }
        public StringContainer string4(String string4) { this.string4 = string4; return this; }
        public StringContainer string5(String string5) { this.string5 = string5; return this; }
        public StringContainer string6(String string6) { this.string6 = string6; return this; }
        public StringContainer string7(String string7) { this.string7 = string7; return this; }
        public StringContainer string8(String string8) { this.string8 = string8; return this; }
        public StringContainer string9(String string9) { this.string9 = string9; return this; }

        public String computeValueWithGS() {
            return getString1() + getString2() + getString3() + getString4() + getString5() + getString6() + getString7() + getString8() + getString9();
        }
        
        public String computeValueWithVars() {
            return string1 + string2 + string3 + string4 + string5 + string6 + string7 + string8 + string9;
        }
    }
}
 Now that we have all of this code, what do we get when we run it normally?  I add "-server" to the command line to make it more similar to how it would be running in reality. I'm running this on a Dell Latitude laptop, with Windows Seven 32-bit.
intgetterssetters:32
intvars:32
strgetterssetters:272
strvars:293
 As you can see, the timing of the integer tests came out identical, even with 100,000,000 iterations. Ironically, the string test with direct references even came out slower than the getter test. I don't consider that significant, however.

Remember that this is happening because of the Hotspot compiler. Would it be helpful if you could run these tests with the Hotspot compiler disabled, or at least convince it to not process the two key methods, being "computeValueWithGS()" and "computeValueWithVars()"?  That is easily done by putting a file called ".hotspot_compiler" in your working directory, with the following contents:
exclude timings/GetterSetterTimings$IntContainer    computeValueWithGS
exclude timings/GetterSetterTimings$IntContainer    computeValueWithVars
exclude timings/GetterSetterTimings$StringContainer    computeValueWithGS
exclude timings/GetterSetterTimings$StringContainer    computeValueWithVars
You also have to add "-XX:CompileCommandFile=.hotspot_compiler" to the JVM command line. With this in place, the results are perhaps more consistent with what you might expect if we didn't have Hotspot around.
CompilerOracle: exclude timings/GetterSetterTimings$IntContainer.computeValueWithGS
CompilerOracle: exclude timings/GetterSetterTimings$IntContainer.computeValueWithVars
CompilerOracle: exclude timings/GetterSetterTimings$StringContainer.computeValueWithGS
CompilerOracle: exclude timings/GetterSetterTimings$StringContainer.computeValueWithVars
### Excluding compile: timings.GetterSetterTimings$IntContainer::computeValueWithGS
intgetterssetters:288
### Excluding compile: timings.GetterSetterTimings$IntContainer::computeValueWithVars
intvars:87
### Excluding compile: timings.GetterSetterTimings$StringContainer::computeValueWithGS
strgetterssetters:771
### Excluding compile: timings.GetterSetterTimings$StringContainer::computeValueWithVars
strvars:533
This definitely illustrates how much value the Hotspot compiler provides.

In conclusion, I think it should be obvious that you don't have to be concerned about the overhead of calling getter methods, as it clearly goes away in reality.

No comments: