StringConverterBenchmark.java
/*
* Copyright (C) 2015, 2017, 2021, 2022 XStream Committers.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
* Created on 8. November 2015 by Joerg Schaible
*/
package com.thoughtworks.xstream.benchmark.jmh;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.BenchmarkParams;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.SingleValueConverter;
import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter;
import com.thoughtworks.xstream.core.util.WeakCache;
import com.thoughtworks.xstream.io.xml.CompactWriter;
import com.thoughtworks.xstream.io.xml.MXParserDriver;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.security.ArrayTypePermission;
import com.thoughtworks.xstream.security.NoTypePermission;
/**
* Benchmark for different StringConverter implementations.
*
* @author Jörg Schaible
* @since 1.4.9
*/
@BenchmarkMode(Mode.AverageTime)
@Fork(value = 1)
@Measurement(iterations = 16)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Threads(4)
@Warmup(iterations = 5)
public class StringConverterBenchmark {
private XStream xstream;
private String xml;
/**
* No memory usage for cache, but any string is a separate instance after deserialization. Memory consumption of the
* deserialized array is nearly 3 times compared to a converter that caches and reuses the strings.
*
* @since 1.4.9
*/
public static final class NonCachingStringConverter extends AbstractSingleValueConverter {
@Override
public boolean canConvert(final Class<?> type) {
return type == String.class;
}
@Override
public Object fromString(final String str) {
return str;
}
}
/**
* Cache based on String.intern(). Uses PermGenSpace for Java 7 and below.
*
* @since 1.4.9
*/
public static final class InternStringConverter extends AbstractSingleValueConverter {
@Override
public boolean canConvert(final Class<?> type) {
return type == String.class;
}
@Override
public Object fromString(final String str) {
return str.intern();
}
}
/**
* Cache based on a synchronized WeakHashMap with weak keys. Ensures that the deserialized strings vanish when the
* deserialized object is GC'ed.
*
* @since 1.4.9
*/
public class SynchronizedWeakCacheStringConverter extends AbstractSingleValueConverter {
private final Map<String, String> cache;
private final int lengthLimit;
private SynchronizedWeakCacheStringConverter(final Map<String, String> map, final int lengthLimit) {
cache = map;
this.lengthLimit = lengthLimit;
}
/**
* Constructs a SynchronizedWeakCacheStringConverter.
*
* @param lengthLimit length limit for cached strings
* @since 1.4.9
*/
public SynchronizedWeakCacheStringConverter(final int lengthLimit) {
this(Collections.synchronizedMap(new WeakCache<String, String>()), lengthLimit);
}
@Override
public boolean canConvert(final Class<?> type) {
return type.equals(String.class);
}
@Override
public Object fromString(final String str) {
if (cache != null && str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
String s = cache.get(str);
if (s == null) {
// fill cache
cache.put(str, str);
s = str;
}
return s;
} else {
return str;
}
}
}
/**
* Cache based on a ConcurrentMap. Cache is never flushed.
*
* @since 1.4.9
*/
public class ConcurrentHashMapStringConverter extends AbstractSingleValueConverter {
private final ConcurrentMap<String, String> cache;
private final int lengthLimit;
private ConcurrentHashMapStringConverter(final ConcurrentMap<String, String> map, final int lengthLimit) {
cache = map;
this.lengthLimit = lengthLimit;
}
/**
* Constructs a ConcurrentHashMapStringConverter.
*
* @param lengthLimit length limit for cached strings
* @since 1.4.9
*/
public ConcurrentHashMapStringConverter(final int lengthLimit) {
this(new ConcurrentHashMap<>(), lengthLimit);
}
@Override
public boolean canConvert(final Class<?> type) {
return type.equals(String.class);
}
@Override
public Object fromString(final String str) {
if (cache != null && str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
final String s = cache.putIfAbsent(str, str);
return s == null ? str : s;
} else {
return str;
}
}
}
/**
* Initialize the XML string to deserialize.
*
* @since 1.4.9
*/
@Setup
public void init() {
final String array[] = new String[300];
for (int i = 0; i < 100;) {
array[i] = String.valueOf(++i);
}
for (int i = 100; i < 200;) {
array[i] = "Binary value " + i + ": " + Integer.toString(++i, 2);
}
for (int i = 200; i < 300;) {
array[i++] = UUID.randomUUID().toString().replace('-', ':');
}
final StringWriter stringWriter = new StringWriter();
final PrettyPrintWriter writer = new CompactWriter(stringWriter);
writer.startNode("string-array");
for (int i = 0; i < 10000; ++i) {
writer.startNode("string");
final String s;
if ((i & 1) == 1) {
s = array[(i >> 1) % 100];
} else if ((i & 2) == 2) {
s = array[100 + (i >> 2) % 100];
} else if ((i & 4) == 4) {
s = array[200 + (i >> 3) % 100];
} else {
s = "Random UUID: " + UUID.randomUUID().toString();
}
writer.setValue(s);
writer.endNode();
}
writer.endNode();
writer.close();
xml = stringWriter.toString();
}
/**
* Setup the data to deserialize.
*
* @param params the parameters of the benchmark
* @since 1.4.9
*/
@Setup(Level.Trial)
public void setUp(final BenchmarkParams params) {
final String benchmark = params.getBenchmark();
final SingleValueConverter converter;
switch (benchmark.substring(StringConverterBenchmark.class.getName().length() + 1)) {
case "nonCaching":
converter = new NonCachingStringConverter();
break;
case "intern":
converter = new InternStringConverter();
break;
case "unlimitedSynchronizedWeakCache":
converter = new SynchronizedWeakCacheStringConverter(Integer.MAX_VALUE);
break;
case "limitedSynchronizedWeakCache":
converter = new SynchronizedWeakCacheStringConverter(UUID.randomUUID().toString().length());
break;
case "unlimitedConcurrentMap":
converter = new SynchronizedWeakCacheStringConverter(Integer.MAX_VALUE);
break;
case "limitedConcurrentMap":
converter = new SynchronizedWeakCacheStringConverter(UUID.randomUUID().toString().length());
break;
default:
throw new IllegalStateException("Unsupported benchmark type: " + benchmark);
}
xstream = new XStream(new MXParserDriver());
xstream.addPermission(NoTypePermission.NONE);
xstream.addPermission(ArrayTypePermission.ARRAYS);
xstream.allowTypes(String.class);
xstream.registerConverter(converter);
}
/**
* No cache for deserialized strings, each string is an own instance.
*
* @since 1.4.9
*/
@Benchmark
public void nonCaching() {
run();
}
/**
* Any string is stored also in the String's internal memory space.
*
* @since 1.4.9
*/
@Benchmark
public void intern() {
run();
}
/**
* Any string is cached in a weak entry.
*
* @since 1.4.9
*/
@Benchmark
public void unlimitedSynchronizedWeakCache() {
run();
}
/**
* Strings of 38 characters or less are cached in a weak entry.
*
* @since 1.4.9
*/
@Benchmark
public void limitedSynchronizedWeakCache() {
run();
}
/**
* Any string is cached in a concurrent map.
*
* @since 1.4.9
*/
@Benchmark
public void unlimitedConcurrentMap() {
run();
}
/**
* Strings of 38 characters or less are cached in a concurrent map.
*
* @since 1.4.9
*/
@Benchmark
public void limitedConcurrentMap() {
run();
}
private void run() {
final String[] array = xstream.fromXML(xml);
assert array.length == 10000 : "array length is " + array.length;
assert array[1].equals("1") : "2nd element was: " + array[1];
assert array[9999].equals("100") : "last element was: " + array[9999];
}
}