Blog-Archiv

Donnerstag, 20. Januar 2022

Java String Concatenation Performance

Premature optimization is the root of all evil (Donald Knuth).
And it kills simplicity.


How to correctly implement Java string concatenations was a long discussed developer topic. What do you think is faster, this StringBuilder technique ...


    return new StringBuilder()
            .append("Hello World, current date and time is ")
            .append(new Date())
            .append(" and we are at time zone ")
            .append(TimeZone.getDefault())
            .toString();

... or this plus-operator technique:


    return
            "Hello World, current date and time is "+
            new Date()+
            " and we are at time zone "+
            TimeZone.getDefault();

?

Most people would say, the plus-operator was discouraged in the Java beginning years, thus the first snippet is faster. Wrong, they are equally fast. Here are some measurements over exactly these snippets, with warm-up and 1000000 repeats, results are in milliseconds:

Technique Test 1 Test 2 Test 3 Test 4
StringBuilder 917 939 1025 992
Plus-Operator 863 903 1045 996

But what about StringBuffer, is this faster?
No, it is not, it is a little slower than StringBuilder, but that depends.

Surprisingly in some cases the plus-operator is much faster than both StringBuilder and StringBuffer, but sometimes it is also ways slower. It depends on how the concatenation is done, in a loop, or in a programmed sequence.

Loop concatenation (STRING_APPEND_COUNT = 4):


    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < STRING_APPEND_COUNT; i++)
        stringBuilder.append(string);
    return stringBuilder.toString();
    


    String s = "";
    for (int i = 0; i < STRING_APPEND_COUNT; i++)
        s += string;
    return s;
    

Sequence concatenation:


    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(string);
    stringBuilder.append(string);
    stringBuilder.append(string);
    stringBuilder.append(string);
    return stringBuilder.toString();
    


    String s = ""+
        string+
        string+
        string+
        string;
    return s;
    

The used string in these examples is not final.
Here are results:

Technique Test 1 Test 2 Test 3 Test 4
StringBuilder Loop 490 379 449 432
Plus-Operator Loop 776 623 828 802
StringBuilder Sequence 319 337 420 348
Plus-Operator Sequence 228 260 265 262

StringBuilder is much faster in loops, but in sequences the plus-operator wins.

When I make string final, following can be seen:

Technique Test 1
StringBuilder Loop 321
Plus-Operator Loop 689
StringBuilder Sequence 210
Plus-Operator Sequence 90

A dramatic win of plus-operator in sequences. This is because the compiler optimizes such code chunks. Everything else must be done at runtime, where the StringBuilder wins.

If you want to hack more, here is the complete source of my experiment, it also includes StringBuffer (click to expand):
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public class StringConcatPerformanceTest
{
    public static void main(String[] args) {
        StringConcatPerformanceTest test = new StringConcatPerformanceTest();
        test.start(Usage.LOOP);
        test.start(Usage.SEQUENCE);
    }
    
    private enum Technique    // the string-concat technique to test
    {
        STRING_BUFFER,
        STRING_BUILDER,
        PLUS_OPERATOR,
    }

    private enum Usage    // the way how to use the class to test
    {
        SEQUENCE,
        LOOP,
    }

    private /*final*/ String string = "Hello";
    
    private long time1;
    private long time2;
    private long time3;
    
    public StringConcatPerformanceTest() {
        for (Technique technique : Technique.values())    // warm up
            for (Usage usage : Usage.values())
                run(technique, usage);
    }
    
    public void start(Usage usage)    {
        time1 = time2 = time3 = 0L;
        
        for (int i = 0; i < 1000000; i++)
            run(usage);
        
        System.out.println("StringBuffer  "+usage+": "+duration(time1));
        System.out.println("StringBuilder "+usage+": "+duration(time2));
        System.out.println("Plus Operator "+usage+": "+duration(time3));
        System.out.println("====================");
    }
    
    private String duration(long nanos)    {
        return ""+Math.round((double)nanos / 1000000)+" millis";
    }
    
    
    private void run(Usage usage)    {
        time1 += run(Technique.STRING_BUFFER, usage);
        time2 += run(Technique.STRING_BUILDER, usage);
        time3 += run(Technique.PLUS_OPERATOR, usage);
        
        time2 += run(Technique.STRING_BUILDER, usage);
        time3 += run(Technique.PLUS_OPERATOR, usage);
        time1 += run(Technique.STRING_BUFFER, usage);
        
        time3 += run(Technique.PLUS_OPERATOR, usage);
        time1 += run(Technique.STRING_BUFFER, usage);
        time2 += run(Technique.STRING_BUILDER, usage);
    }
    
    private long run(Technique technique, Usage usage)    {
        final long time = System.nanoTime();
        
        if (usage == Usage.SEQUENCE)    {
            switch (technique)    {
                case STRING_BUFFER: stringBufferSequence(); break;
                case STRING_BUILDER: stringBuilderSequence(); break;
                case PLUS_OPERATOR: plusOperatorSequence(); break;
                default: throw new IllegalArgumentException();
            }
        }
        else    {    // Usage.LOOP
            switch (technique)    {
                case STRING_BUFFER: stringBufferLoop(); break;
                case STRING_BUILDER: stringBuilderLoop(); break;
                case PLUS_OPERATOR: plusOperatorLoop(); break;
                default: throw new IllegalArgumentException();
            }
        }
        
        return System.nanoTime() - time;
    }
    
    
    private static final int STRING_APPEND_COUNT = 4;
    
    
    private String stringBufferLoop() {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            stringBuffer.append(string);
        return stringBuffer.toString();
    }

    private String stringBuilderLoop() {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            stringBuilder.append(string);
        return stringBuilder.toString();
    }

    private String plusOperatorLoop() {
        String s = "";
        for (int i = 0; i < STRING_APPEND_COUNT; i++)
            s += string;
        return s;
    }

    
    private String stringBufferSequence() {
        StringBuffer stringBuffer = new StringBuffer("");
        stringBuffer.append(string);    // 1
        stringBuffer.append(string);
        stringBuffer.append(string);
        stringBuffer.append(string);    // 4 == STRING_APPEND_COUNT
        return stringBuffer.toString();
    }

    private String stringBuilderSequence() {
        StringBuilder stringBuilder = new StringBuilder("");
        stringBuilder.append(string);    // 1
        stringBuilder.append(string);
        stringBuilder.append(string);
        stringBuilder.append(string);    // 4 == STRING_APPEND_COUNT
        return stringBuilder.toString();
    }

    private String plusOperatorSequence() {
        String s = "" +
            string +    // 1
            string +
            string +
            string ;    // 4 == STRING_APPEND_COUNT
        return s;
    }
}


I would always use the plus-operator by default. It is the much more readable and simpler variant. Just in cases where you really build big texts, and non-final fields or method calls are involved, you should use StringBuilder.




Keine Kommentare: