In this blog we will write a small unmarshaller by hand at code-time and measure its performance.
This blog is part 1 of 4, the series consists of the following posts:
- Writing an unmarshaller by hand
- Constructing an unmarshaller using reflection
- Generating an unmarshaller using annotations
- Creating an unmarshaller using bytecode
For a definition of the some of the terminology used here see Phases for generating code.
All programs use data structures (e.g. objects) in memory, however to be truly useful these structures often need to be converted to a format suitable for storage or transmission. This processes is called marshalling. After retrieving the data from storage/receiving the transmission the process must be reverted, or the data unmarshalled, to be used again.
Many libraries exist for (un)marshalling data to different formats
- XML (e.g. JAXB, XStream)
- JSON (e.g. Jackson, GSON)
- SQL (e.g. Hibernate ORM, EclipseLink)
and many more.
What all these libraries have in common is that they start with a definition of the data format, often in the form of a JavaBean, and generate code to marshall and unmarshall based on the definition. In general these libraries build on a parser. A parser can read the underlying format, but is not (fully) aware of the structure of the data.
(All example code can be found on GitHub)
In this example we will focus on unmarshalling and use a very simple parser, defined by the following interface:
public interface Parser {
boolean readBoolean() throws IOException;
int readInteger() throws IOException;
String readString() throws IOException;
}
Our example parser has no concept of structure and only allows for reading booleans, integers, and strings. This keeps the code simple, while still allowing to demonstrate the most interesting aspects and challenges of unmarshalling.
Throughout this series we will be using a simple representation of an [employee] defined by the following class:
public class Employee {
private int id;
private boolean active;
private String firstName;
private String lastName;
private int startYear;
private String jobTitle;
/* Getters and setters for all fields */
}
The goal is to implement the following interface:
public interface Unmarshaller<T> {
default void init() {
}
T read(Parser parser) throws IOException;
}
Since we know the exact data structure and are provided with a parser writing an unmarshaller by hand is trivial:
public class CodeTimeUnmarshaller implements Unmarshaller<Employee> {
@Override
public Employee read(Parser parser) throws IOException {
var employee = new Employee();
employee.setId(parser.readInteger());
employee.setActive(parser.readBoolean());
employee.setFirstName(parser.readString());
employee.setLastName(parser.readString());
employee.setStartYear(parser.readInteger());
employee.setJobTitle(parser.readString());
return employee;
}
}
We will use this code as a base for comparing the code generated in the next 3 posts in the series. However in order to use this implementation as a baseline we will need to determine its performance.
Benchmarking, especially benchmarking small code sniplets known as microbenchmarking, inside a complex environment such as the Java Virtual Machine is hard. You have to deal with different stages of compilation, profiling, warm-up, dead-code elimination, garbage collection, etc. Fortunately the good people providing us with the JVM have also provided the Java Microbenchmark Harness or JMH.
JMH comes some excellent examples. Writing a benchmark is pretty easy:
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CodeTimeBenchmark {
byte[] input;
Unmarshaller unmarshaller;
@Setup
public void setup() throws IOException {
this.input = Data.generateEmployee();
this.unmarshaller = new CodeTimeUnmarshaller();
this.unmarshaller.init();
}
@Benchmark
public Object benchmark() throws IOException {
var parser = new DataInputParser(new ByteArrayInputStream(input));
return unmarshaller.readEmployee(parser);
}
}
This is all that is needed to create the benchmark. The code should speak for itself. I have chosen to use milliseconds as the default time unit to make the numbers easier to comprehend.
After compiling the code using Maven running the benchmark can be done by executing the following command:
java -jar code-generation/target/benchmarks.jar CodeTime
This will take some time and on my desktop gives the following result:
Benchmark Mode Cnt Score Error Units
CodeTimeBenchmark.benchmark thrpt 25 6249,235 ± 29,212 ops/ms
The code ran almost 6250 times per millisecond. This is referred to as the throughput, and higher is better. In part 2 of this series we will provide the same functionality, but then at run-time, using reflection, and compare the performance against the baseline as shown above.