2

This program converts an object's state into an HTML string.

public class Test {
    public static void main(String[] args) {
        Address addr = new Address();
        addr.setLine1("A straight line");
        addr.setLine2("A curve");
        addr.setCity("A Round City");
        addr.setState("A Triangular State");
        addr.setCountry("A Rectangle Country");
        addr.setZip("123456");

        @SuppressWarnings("unused")
        String str;
        int count = 1000;
        for (int j = 0; j < 5; j++) {

            double timeRich = System.nanoTime();
            for (int i = 0; i < count; i++) {
                str = AddressFormatter.formatRich(addr);
            }
            timeRich = System.nanoTime() - timeRich;

            double timeFine = System.nanoTime();
            for (int i = 0; i < count; i++) {
                str = AddressFormatter.formatFine(addr);
            }
            timeFine = System.nanoTime() - timeFine;


            double timePoor = System.nanoTime();
            for (int i = 0; i < count; i++) {
                str = AddressFormatter.formatPoor(addr);
            }
            timePoor = System.nanoTime() - timePoor;

            System.out.println("Test cases: " + count);
            System.out.println("Average time to format (SB Poor): " + (int)(timePoor/count) + " ns");
            System.out.println("Average time to format (SB Fine): " + (int)(timeFine/count) + " ns");
            System.out.println("Average time to format (String) : " + (int)(timeRich/count) + " ns");
            System.out.println();
            count *= 10;
        }
        System.out.println("***End of test***");
    }
}

class Address {
    private String line1;
    private String line2;
    private String city;
    private String state;
    private String country;
    private String zip;

    /**
     * Default constructor.
     */
    public Address() {}

    public String getLine1() {
        return line1;
    }
    public void setLine1(String line1) {
        this.line1 = line1;
    }
    public String getLine2() {
        return line2;
    }
    public void setLine2(String line2) {
        this.line2 = line2;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getState() {
        return state;
    }
    public void setState(String state) {
        this.state = state;
    }
    public String getCountry() {
        return country;
    }
    public void setCountry(String country) {
        this.country = country;
    }
    public String getZip() {
        return zip;
    }
    public void setZip(String zip) {
        this.zip = zip;
    }
}

class AddressFormatter {
    // more readable than formatFine()
    public static String formatPoor(Address obj) {
        StringBuilder str = new StringBuilder();
        str.append("<div class=\"address-wrapper\">\n");
        str.append("\t<div class=\"addr-line\">" + obj.getLine1() + "</div>\n");
        str.append("\t<div class=\"addr-line\">" + obj.getLine2() + "</div>\n");
        str.append("\t<div class=\"addr-city\">" + obj.getCity() + "</div>\n");
        str.append("\t<div class=\"addr-state\">" + obj.getState() + "</div>\n");
        str.append("\t<div class=\"addr-country\">" + obj.getCountry() + "</div>\n");
        str.append("\t<div class=\"addr-zip\">" + obj.getZip() + "</div>\n");
        str.append("</div>\n");

        return str.toString();
    }

    // grouping all constants, removing string concatenations
    public static String formatFine(Address obj) {
        StringBuilder str = new StringBuilder();
        str.append("<div class=\"address-wrapper\">\n\t<div class=\"addr-line\">");
        str.append(obj.getLine1());
        str.append("</div>\n\t<div class=\"addr-line\">");
        str.append(obj.getLine2());
        str.append("</div>\n\t<div class=\"addr-city\">");
        str.append(obj.getCity());
        str.append("</div>\n\t<div class=\"addr-state\">");
        str.append(obj.getState());
        str.append("</div>\n\t<div class=\"addr-country\">");
        str.append(obj.getCountry());
        str.append("</div>\n\t<div class=\"addr-zip\">");
        str.append(obj.getZip());
        str.append("</div>\n</div>\n");

        return str.toString();
    }

    public static String formatRich(Address obj) {
        return "<div class=\"address-wrapper\">\n"
        + "\t<div class=\"addr-line\">" + obj.getLine1() + "</div>\n"
        + "\t<div class=\"addr-line\">" + obj.getLine2() + "</div>\n"
        + "\t<div class=\"addr-city\">" + obj.getCity() + "</div>\n"
        + "\t<div class=\"addr-state\">" + obj.getState() + "</div>\n"
        + "\t<div class=\"addr-country\">" + obj.getCountry() + "</div>\n"
        + "\t<div class=\"addr-zip\">" + obj.getZip() + "</div>\n"
        + "</div>\n";
    }
}

I get the following results when running this program in Eclipse:

Test cases: 1000
Average time to format (SB Poor): 13513 ns
Average time to format (SB Fine): 7052 ns
Average time to format (String) : 14088 ns

Test cases: 10000
Average time to format (SB Poor): 3061 ns
Average time to format (SB Fine): 3290 ns
Average time to format (String) : 1618 ns

Test cases: 100000
Average time to format (SB Poor): 3486 ns
Average time to format (SB Fine): 1568 ns
Average time to format (String) : 589 ns

Test cases: 1000000
Average time to format (SB Poor): 616 ns
Average time to format (SB Fine): 547 ns
Average time to format (String) : 497 ns

Test cases: 10000000
Average time to format (SB Poor): 657 ns
Average time to format (SB Fine): 626 ns
Average time to format (String) : 191 ns

***End of test***

Why String version is faster than StringBuilder version?

Why average time is reducing after every iteration?

EDIT: I have added another formatting function by removing all concatenation operations from 'StringBuilder' version (as pointed out by one answer).

In the first iteration 'String' version is the slowest.

In the last iteration 'String' version is the fastest.

Vince
  • 14,470
  • 7
  • 39
  • 84
Jitendra Kumar
  • 503
  • 1
  • 6
  • 13
  • 2
    That's the doing of Java's Just-in-time ([JIT](https://en.wikipedia.org/wiki/Just-in-time_compilation)) compiler. It recognizes unchanging code and optimizes it. But it takes some time, that's why it gets faster with every iteration. **EDIT:** You've tagged this question with "jit", so you probably know what it is and why this happens – QBrute May 29 '17 at 10:02
  • 1
    also [how to benchmark](https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java), you pretty much artificially manipulate the result as the `Stringbuilder` is your warmup, the `String` is a followed up run. The result really doesn´t speak for the real performance. – SomeJavaGuy May 29 '17 at 10:03
  • @SomeJavaGuy This happens even when I put the `String` version before `StringBuilder`. – Jitendra Kumar May 29 '17 at 10:08
  • @SomeJavaGuy He's using a single instance for `addr`, so I doubt there's any crossover warmup. – chrylis -cautiouslyoptimistic- May 29 '17 at 10:13
  • Ok, I know about Jit because anyone working with Java will know about it because now it is an integral part of jvm. The reason why i added the tag becuase I thought it has something to do with jit. The reason i removed it because people who read that tag thought that i know about how jit works but I dont know is how jit works? And why I am getting the above results. May be you could explain. – Jitendra Kumar Aug 23 '17 at 07:12
  • 1
    @JitendraKumar Not to sound rude, but why not read up on it? You should be aware that JIT compilation interferes with benchmarks. There are tons of great articles explaining the optimizations performed by the JIT compiler. and JITWatch allows you to see what optimizations were performed on your code (even tells you which compiler performed it: C1 or C2, assuming you're using tiered compilation). I'll retract my downvote, but I highly recommend looking into topics you feel uncomfortable with - you won't regret it. – Vince Aug 23 '17 at 15:46
  • Well that sounds a bit helping. I think what @CoronA has suggested is also correct. I have made changes as he suggested and results are different now. Trying to understand more about his solution. – Jitendra Kumar Aug 24 '17 at 05:32

2 Answers2

3

The second part of your question is easy: The JVM is recognizing repeated executions and optimizing the machine code, which is why it's important to handle benchmarks carefully.

Here's what is happening to explain the difference in implementations:

Your "StringBuilder" implementation is very poorly written. Instead of appending each component, you're performing string concatenation (creating and then discarding a new StringBuilder) for each method call and then appending the result of that. If you correctly used .append for each element, you'd see much less of a difference.

However, modern Java compilers turn a series of string concatenations with + into an implicit StringBuilder invocation to minimize object creation. The Java compiler also merges compile-time string constants that are concatenated. Therefore, your format2 method is also using a StringBuilder, with one important difference--all of the line-wrapped adjacent string constants are merged. Therefore, even if you were to fix your format method, format2 will be faster because it is grouping more of the fixed content.

chrylis -cautiouslyoptimistic-
  • 75,269
  • 21
  • 115
  • 152
  • 1
    That’s not a property of “modern Java compilers”. Java compilers always did this. – Holger Feb 14 '20 at 09:15
  • @Holger _Necessarily_ not as stated, as `StringBuilder` didn't exist until 1.5, and there's no requirement for compilers to do so at all. – chrylis -cautiouslyoptimistic- Feb 15 '20 at 05:29
  • 1
    Before Java 5, compilers used `StringBuffer` instead of `StringBuilder`, to the same effect. That’s why [the specification](https://docs.oracle.com/javase/specs/jls/se12/html/jls-15.html#jls-15.18.1) still refers to `StringBuffer`, see also [this answer](https://stackoverflow.com/a/29228376/2711488).Yes, Java compilers are not required to do this, but this applies to “modern Java compilers” too. They were never required to do it, but they did and still do. Besides, a comparison between manual use of `StringBuilder` with the string concatenation operator only makes sense in version having both. – Holger Feb 17 '20 at 08:25
2

I expected the byte code of formatRich and formatFine were equivalent, yet it was not. So I tried to get two equivalent methods:

Rewrite your StringBuilder method to

public static String formatFine(Address obj) {
    return new StringBuilder("<div class=\"address-wrapper\">\n\t<div class=\"addr-line\">")
        .append(obj.getLine1())
        .append("</div>\n\t<div class=\"addr-line\">")
        .append(obj.getLine2())
        .append("</div>\n\t<div class=\"addr-city\">")
        .append(obj.getCity())
        .append("</div>\n\t<div class=\"addr-state\">")
        .append(obj.getState())
        .append("</div>\n\t<div class=\"addr-country\">")
        .append(obj.getCountry())
        .append("</div>\n\t<div class=\"addr-zip\">")
        .append(obj.getZip())
        .append("</div>\n</div>\n").toString();
}

This method is equivalent to following in java byte code:

public static String formatRich(Address obj) {
    return "<div class=\"address-wrapper\">\n\t<div class=\"addr-line\">"
        + obj.getLine1()
        + "</div>\n\t<div class=\"addr-line\">"
        + obj.getLine2()
        + "</div>\n\t<div class=\"addr-city\">"
        + obj.getCity()
        + "</div>\n\t<div class=\"addr-state\">"
        + obj.getState()
        + "</div>\n\t<div class=\"addr-country\">"
        + obj.getCountry()
        + "</div>\n\t<div class=\"addr-zip\">"
        + obj.getZip()
        + "</div>\n</div>\n";
}

Executing your main program resolves (on my machine) to:

...

Test cases: 10000000
Average time to format (SB Poor): 633 ns
Average time to format (SB Fine): 151 ns
Average time to format (String) : 152 ns

Explanation:

  • The explicit statement str.append has to load str from the stack. The result is pushed on the stack but never used.
  • The direct concatenation (and the chained StringBuilder) reuses the result of str.append which is already on the stack
  • Both (str and the result of str.append) point to the same heap location, yet I do not know if this can be derived by the compiler. It seems that the current optimization level is not able to optimize it.
CoronA
  • 7,717
  • 2
  • 26
  • 53
  • Thankyou, I have made changes as suggested by you and now SB Fine is even faster than String in all cases. Trying to understand more of your explanation. – Jitendra Kumar Aug 24 '17 at 05:34