1 /*
2  * Copyright (C) 2012 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.base;
18 
19 import com.google.caliper.BeforeExperiment;
20 import com.google.caliper.Benchmark;
21 import com.google.caliper.Param;
22 import java.util.Arrays;
23 import java.util.Iterator;
24 
25 /**
26  * Benchmarks {@link Joiner} against some common implementations of delimiter-based string joining.
27  *
28  * @author Adomas Paltanavicius
29  */
30 public class JoinerBenchmark {
31 
32   private static final String DELIMITER_STRING = ",";
33   private static final char DELIMITER_CHARACTER = ',';
34 
35   private static final Joiner JOINER_ON_STRING = Joiner.on(DELIMITER_STRING);
36   private static final Joiner JOINER_ON_CHARACTER = Joiner.on(DELIMITER_CHARACTER);
37 
38   @Param({"3", "30", "300"})
39   int count;
40 
41   @Param({"0", "1", "16", "32", "100"})
42   int componentLength;
43 
44   private Iterable<String> components;
45 
46   @BeforeExperiment
setUp()47   void setUp() {
48     String component = Strings.repeat("a", componentLength);
49     String[] raw = new String[count];
50     Arrays.fill(raw, component);
51     components = Arrays.asList(raw);
52   }
53 
54   /** {@link Joiner} with a string delimiter. */
55   @Benchmark
joinerWithStringDelimiter(int reps)56   int joinerWithStringDelimiter(int reps) {
57     int dummy = 0;
58     for (int i = 0; i < reps; i++) {
59       dummy ^= JOINER_ON_STRING.join(components).length();
60     }
61     return dummy;
62   }
63 
64   /** {@link Joiner} with a character delimiter. */
65   @Benchmark
joinerWithCharacterDelimiter(int reps)66   int joinerWithCharacterDelimiter(int reps) {
67     int dummy = 0;
68     for (int i = 0; i < reps; i++) {
69       dummy ^= JOINER_ON_CHARACTER.join(components).length();
70     }
71     return dummy;
72   }
73 
74   /**
75    * Mimics what the {@link Joiner} class does internally when no extra options like ignoring {@code
76    * null} values are used.
77    */
78   @Benchmark
joinerInlined(int reps)79   int joinerInlined(int reps) {
80     int dummy = 0;
81     for (int i = 0; i < reps; i++) {
82       StringBuilder sb = new StringBuilder();
83       Iterator<String> iterator = components.iterator();
84       if (iterator.hasNext()) {
85         sb.append(iterator.next().toString());
86         while (iterator.hasNext()) {
87           sb.append(DELIMITER_STRING);
88           sb.append(iterator.next());
89         }
90       }
91       dummy ^= sb.toString().length();
92     }
93     return dummy;
94   }
95 
96   /**
97    * Only appends delimiter if the accumulated string is non-empty. Note: this isn't a candidate
98    * implementation for Joiner since it fails on leading empty components.
99    */
100   @Benchmark
stringBuilderIsEmpty(int reps)101   int stringBuilderIsEmpty(int reps) {
102     int dummy = 0;
103     for (int i = 0; i < reps; i++) {
104       StringBuilder sb = new StringBuilder();
105       for (String comp : components) {
106         if (sb.length() > 0) {
107           sb.append(DELIMITER_STRING);
108         }
109         sb.append(comp);
110       }
111       dummy ^= sb.toString().length();
112     }
113     return dummy;
114   }
115 
116   /**
117    * Similar to the above, but keeps a boolean flag rather than checking for the string accumulated
118    * so far being empty. As a result, it does not have the above-mentioned bug.
119    */
120   @Benchmark
booleanIfFirst(int reps)121   int booleanIfFirst(int reps) {
122     int dummy = 0;
123     for (int i = 0; i < reps; i++) {
124       StringBuilder sb = new StringBuilder();
125       boolean append = false;
126       for (String comp : components) {
127         if (append) {
128           sb.append(DELIMITER_STRING);
129         }
130         sb.append(comp);
131         append = true;
132       }
133       dummy ^= sb.toString().length();
134     }
135     return dummy;
136   }
137 
138   /**
139    * Starts with an empty delimiter and changes to the desired value at the end of the iteration.
140    */
141   @Benchmark
assignDelimiter(int reps)142   int assignDelimiter(int reps) {
143     int dummy = 0;
144     for (int i = 0; i < reps; i++) {
145       StringBuilder sb = new StringBuilder();
146       String delim = "";
147       for (String comp : components) {
148         sb.append(delim);
149         sb.append(comp);
150         delim = DELIMITER_STRING;
151       }
152       dummy ^= sb.toString().length();
153     }
154     return dummy;
155   }
156 
157   /**
158    * Always append the delimiter after the component, and in the very end shortens the buffer to get
159    * rid of the extra trailing delimiter.
160    */
161   @Benchmark
alwaysAppendThenBackUp(int reps)162   int alwaysAppendThenBackUp(int reps) {
163     int dummy = 0;
164     for (int i = 0; i < reps; i++) {
165       StringBuilder sb = new StringBuilder();
166       for (String comp : components) {
167         sb.append(comp);
168         sb.append(DELIMITER_STRING);
169       }
170       if (sb.length() > 0) {
171         sb.setLength(sb.length() - DELIMITER_STRING.length());
172       }
173       dummy ^= sb.toString().length();
174     }
175     return dummy;
176   }
177 }
178