Search code examples
javastringstringbuilder

Why String concatenation is faster than StringBuilder in Java?


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.


Solution

  • 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.