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.

7
  • 2
    That's the doing of Java's Just-in-time (JIT) 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 Commented May 29, 2017 at 10:02
  • 1
    also how to benchmark, 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. Commented May 29, 2017 at 10:03
  • @SomeJavaGuy This happens even when I put the String version before StringBuilder. Commented May 29, 2017 at 10:08
  • @SomeJavaGuy He's using a single instance for addr, so I doubt there's any crossover warmup. Commented May 29, 2017 at 10:13
  • 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. Commented Aug 23, 2017 at 15:46

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

3 Comments

That’s not a property of “modern Java compilers”. Java compilers always did this.
@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.
Before Java 5, compilers used StringBuffer instead of StringBuilder, to the same effect. That’s why the specification still refers to StringBuffer, see also this answer.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.
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.

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.