Polish "Optimize allocation in StringUtils#cleanPath"
This commit also introduces JMH benchmarks related to the code optimizations. Closes gh-2631
This commit is contained in:
parent
8d3e8ca3a2
commit
cc026fcb8a
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||||
|
import org.openjdk.jmh.annotations.Level;
|
||||||
|
import org.openjdk.jmh.annotations.Mode;
|
||||||
|
import org.openjdk.jmh.annotations.Param;
|
||||||
|
import org.openjdk.jmh.annotations.Scope;
|
||||||
|
import org.openjdk.jmh.annotations.Setup;
|
||||||
|
import org.openjdk.jmh.annotations.State;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benchmarks for {@link StringUtils}.
|
||||||
|
*
|
||||||
|
* @author Brian Clozel
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.Throughput)
|
||||||
|
public class StringUtilsBenchmark {
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void collectionToDelimitedString(DelimitedStringState state, Blackhole bh) {
|
||||||
|
bh.consume(StringUtils.collectionToCommaDelimitedString(state.elements));
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class DelimitedStringState {
|
||||||
|
|
||||||
|
@Param("10")
|
||||||
|
int elementMinSize;
|
||||||
|
|
||||||
|
@Param("20")
|
||||||
|
int elementMaxSize;
|
||||||
|
|
||||||
|
@Param("10")
|
||||||
|
int elementCount;
|
||||||
|
|
||||||
|
Collection<String> elements;
|
||||||
|
|
||||||
|
@Setup(Level.Iteration)
|
||||||
|
public void setup() {
|
||||||
|
Random random = new Random();
|
||||||
|
this.elements = new ArrayList<>(this.elementCount);
|
||||||
|
int bound = this.elementMaxSize - this.elementMinSize;
|
||||||
|
for (int i = 0; i < this.elementCount; i++) {
|
||||||
|
this.elements.add(String.format("%0" + (random.nextInt(bound) + this.elementMinSize) + "d", 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public void cleanPath(CleanPathState state, Blackhole bh) {
|
||||||
|
for (String path : state.paths) {
|
||||||
|
bh.consume(StringUtils.cleanPath(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State(Scope.Benchmark)
|
||||||
|
public static class CleanPathState {
|
||||||
|
|
||||||
|
private static final List<String> SEGMENTS = Arrays.asList("some", "path", ".", "..", "springspring");
|
||||||
|
|
||||||
|
@Param("10")
|
||||||
|
int segmentCount;
|
||||||
|
|
||||||
|
@Param("20")
|
||||||
|
int pathsCount;
|
||||||
|
|
||||||
|
Collection<String> paths;
|
||||||
|
|
||||||
|
@Setup(Level.Iteration)
|
||||||
|
public void setup() {
|
||||||
|
this.paths = new ArrayList<>(this.pathsCount);
|
||||||
|
Random random = new Random();
|
||||||
|
for (int i = 0; i < this.pathsCount; i++) {
|
||||||
|
this.paths.add(createSamplePath(random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createSamplePath(Random random) {
|
||||||
|
String separator = random.nextBoolean() ? "/" : "\\";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("jar:file:///c:");
|
||||||
|
for (int i = 0; i < this.segmentCount; i++) {
|
||||||
|
sb.append(separator);
|
||||||
|
sb.append(SEGMENTS.get(random.nextInt(SEGMENTS.size())));
|
||||||
|
}
|
||||||
|
sb.append(separator);
|
||||||
|
sb.append("the%20file.txt");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -735,42 +735,11 @@ public abstract class StringUtils {
|
||||||
pathElements.addFirst(CURRENT_PATH);
|
pathElements.addFirst(CURRENT_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String joined = joinStrings(pathElements, FOLDER_SEPARATOR);
|
final String joined = collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
|
||||||
// avoid string concatenation with empty prefix
|
// avoid string concatenation with empty prefix
|
||||||
return prefix.isEmpty() ? joined : prefix + joined;
|
return prefix.isEmpty() ? joined : prefix + joined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a {@link Collection Collection<String>} to a delimited {@code String} (e.g. CSV).
|
|
||||||
* <p>This is an optimized variant of {@link #collectionToDelimitedString(Collection, String)}, which does not
|
|
||||||
* require dynamic resizing of the StringBuilder's backing array.
|
|
||||||
* @param coll the {@code Collection Collection<String>} to convert (potentially {@code null} or empty)
|
|
||||||
* @param delim the delimiter to use (typically a ",")
|
|
||||||
* @return the delimited {@code String}
|
|
||||||
*/
|
|
||||||
private static String joinStrings(@Nullable Collection<String> coll, String delim) {
|
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(coll)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// precompute total length of resulting string
|
|
||||||
int totalLength = (coll.size() - 1) * delim.length();
|
|
||||||
for (String str : coll) {
|
|
||||||
totalLength += str.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder(totalLength);
|
|
||||||
Iterator<?> it = coll.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
sb.append(it.next());
|
|
||||||
if (it.hasNext()) {
|
|
||||||
sb.append(delim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two paths after normalization of them.
|
* Compare two paths after normalization of them.
|
||||||
* @param path1 first path for comparison
|
* @param path1 first path for comparison
|
||||||
|
@ -1330,7 +1299,12 @@ public abstract class StringUtils {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
int totalLength = coll.size() * (prefix.length() + suffix.length()) + (coll.size() - 1) * delim.length();
|
||||||
|
for (Object element : coll) {
|
||||||
|
totalLength += element.toString().length();
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(totalLength);
|
||||||
Iterator<?> it = coll.iterator();
|
Iterator<?> it = coll.iterator();
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
sb.append(prefix).append(it.next()).append(suffix);
|
sb.append(prefix).append(it.next()).append(suffix);
|
||||||
|
|
Loading…
Reference in New Issue