Minborg

Minborg
Minborg
Showing posts with label record. Show all posts
Showing posts with label record. Show all posts

Thursday, September 14, 2023

Java Records are "Trusted" and Consequently Faster

 

Java Records are "Trusted" and Consequently Faster

Did you know Java records are trusted by the Hotspot VM in a special way? This makes their speed superior in some aspects compared to regular Java classes. In this short article, we will take a look at constant folding of instance fields and how this can bolster the performance of your Java application.

Background

Suppose we want to model an immutable point:

public interface Point {
    int x();
    int y();
}

Before record classes were introduced in Java, data classes had to be "manually" coded using a regular Java class like this:

public final class RegularPoint implements Point {

    private final int x;
    private final int y;

    public RegularPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int x() {
        return x;
    }

    @Override
    public int y() {
        return y;
    }

    // Implementations of toString(), hashCode() and equals()
    // omitted for brevity

}

With records, it became much easier and, we also got reasonable default implementations of the methods toString()hashCode() and equals():

public record RecordPoint(int x, int y) implements Point {}

As an extra bonus, there is an emerging property for records that makes them eligible for constant folding optimizations if used in a static context. Read more about this in the following chapters.

Setup

Suppose we keep track of the unique origin point in a static variable like this:

public static final Point ORIGIN = new RecordPoint(0, 0);

Further, assume we have a method that determines if a given Point is at the origin point:

public static boolean isOrigin(Point point) {
        return point.x() == ORIGIN.x() &&
               point.y() == ORIGIN.y();
}

We could then write a small program that demonstrates the principles:

public class Demo {

    public static final Point ORIGIN = new RecordPoint(0, 0);

    public static void main(String[] args) {
        analyze(new RegularPoint(0, 0));
        analyze(new RegularPoint(1, 1));
    }

    public static void analyze(Point point) {
        System.out.format("The point %s is %s at the origin.%n",
                point, isOrigin(point) ? "" : "not");
    }

    public static boolean isOrigin(Point point) {
        return point.x() == ORIGIN.x() &&
               point.y() == ORIGIN.y();
    }

}

When run, the code above will produce the following output:

The point RegularPoint{x=0, y=0} is at the origin.
The point RegularPoint{x=1, y=1} is not at the origin.

We could easily replace the use of new RegularPoint(…​) with new RecordPoint(…​) in the code above, and we would get a similar output:

The point RecordPoint[x=0, y=0] is at the origin.
The point RecordPoint[x=1, y=1] is not at the origin.

It appears the two implementation variants of the interface Point work as expected. But how is the performance of code affected by switching from regular Java classes to records?

Benchmarks

Here is a benchmark that can be used to measure the effects of using records over regular classes:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(value=3)
public class Bench {

    private static final RegularPoint REGULAR_ORIGIN = new RegularPoint(0, 0);
    private static final RecordPoint RECORD_ORIGIN = new RecordPoint(0, 0);

    private List<RegularPoint> regularPoints;
    private List<RecordPoint> recordPoints;

    @Setup
    public void setup() {
        regularPoints = IntStream.range(0, 16)
                .mapToObj(i -> new RegularPoint(i, i))
                .toList();

        recordPoints = IntStream.range(0, 16)
                .mapToObj(i -> new RecordPoint(i, i))
                .toList();
    }

    @Benchmark
    public void regular(Blackhole bh) {
        for (RegularPoint point: regularPoints) {
            if (point.x() == REGULAR_ORIGIN.x() && point.y() == REGULAR_ORIGIN.y()) {
                bh.consume(1);
            } else {
                bh.consume(0);
            }
        }
    }

    @Benchmark
    public void record(Blackhole bh) {
        for (RecordPoint point: recordPoints) {
            if (point.x() == RECORD_ORIGIN.x() && point.y() == RECORD_ORIGIN.y()) {
                bh.consume(1);
            } else {
                bh.consume(0);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

}

When run on a Mac M1 laptop, the following results emerged (lower is better):

Benchmark      Mode  Cnt   Score   Error  Units
Bench.regular  avgt   15  10.424 ± 0.257  ns/op
Bench.record   avgt   15   9.412 ± 0.181  ns/op

As can be seen, records are about 10% faster than regular classes in this benchmark.

Here is what it looks like in a graph:

Graph 1, shows the performance of regular and record classes.

Under the Hood

Looking at why records can be faster in cases like the above, there is a clue in the class ciField.cpp which tells the Hotspot compiler which instance fields should be "trusted" when performing constant folding. The class also gives away some clues about other Java classes that benefit from the same optimizations. One example is the Foreign Function & Memory API that is slated to be finalized in Java 22 and where, for example, all classes implementing the various MemoryLayout variants all are eligible for constant folding optimizations.

The C++ class above is not available to regular Java programs but, by switching to records, we may directly reap the benefits of constant folding for instance fields.

As a final note, it should be said that modifying records fields (which are private and final) using, for example, Unsafe is …​ well …​ unsafe and would produce an undefined result. Don’t do that!

UPDATE:
Unsafe and reflection provide special protection against tampering with records making it very difficult to update record fields via backdoors. For example, trying to obtain the field offset for a record component via 
Unsafe will result in an UnsupportedOperationExcption being thrown.

Conclusion

Records offer a convenient way of expressing data carriers. As an added benefit, they also provide improved performance compared to regular Java classes in some applications.

Tuesday, February 11, 2020

Java14: Join Database Tables with Java 14's new Record

Java14 Records in Joins

Did you know that you can join database tables into a Java Stream with Java 14's preview record feature? Read this short article and find out how it is done using the Speedment Stream ORM. We will start with how to set up your project.

Setup

Download Java 14. Go to the Speedment Initializer and download your project skelaton (including pom.xml). Modify the following lines in your pom.xml file:
 
<maven.compiler.source>14</maven.compiler.source>
<maven.compiler.target>14</maven.compiler.target>

...

    <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
            <release>14</release>
            <compilerArgs>
                --enable-preview
            </compilerArgs>
        </configuration>
    </plugin>


Make sure that you have the latest version of your ide (e.g. IDEA 2010.1) that supports the new Java 14 features.

Speedment Joins

Speedment allows dynamically JOIN:ed database tables to be consumed as standard Java Streams. In this article, we will use the exemplary Sakila database that contains films, actors, languages etc. Download Sakila here or grab a Docker version here

Tables, views and joins can easily be turned into standard Java streams with Speedment. This is how it can look like in Java 14:

            
    var speedment = new SakilaApplicationBuilder()
            .withPassword("sakila")
            .withBundle(JoinBundle.class)
            .build();

    var joinComponent = speedment.getOrThrow(JoinComponent.class);

    var films = speedment.getOrThrow(FilmManager.class);

    // Define a Java 14 "record" that can hold a Film and a Language
    record FilmLanguage(Film film, Language language) {}

    var join = joinComponent.from(films.getTableIdentifier())
            .leftJoinOn(Language.LANGUAGE_ID).equal(Film.LANGUAGE_ID)
            // Provide the constructor of the Java 14 "record"
            // to be used to construct Film/Language composites
            .build(FilmLanguage::new);

    join.stream()
            .forEach(filmLanguage -> System.out.format(
                    "%s is in %s%n",
                    filmLanguage.film().getTitle(),
                    filmLanguage.language().getName())
            );


This will produce the following output:

ACADEMY DINOSAUR is in English
ACE GOLDFINGER is in English
ADAPTATION HOLES is in English
...

Code Breakdown

The from() method takes the first table we want to use (Film). The innerJoinOn() method takes a specific column of the second table we want to join. Then, the equal() method takes a column from the first table that we want to use as our join condition. So, in this example, we will get matched Film and Language entities where the column Language.LANGUAGE_ID equal Film.LANGUAGE_ID.

Finally, build() will construct our Join object that can, in turn, be used to create Java Streams. The Join object can be re-used over and over again.

Note how the constructor of the record FilmLanguage is provided in the build() method. Note also how a film and language entity can be obtained from the record (e.g. filmLanguage.film()). This is a big improvement over previous Java version where we had to provide rather lengthy custom classes or use tuples with accessor like get0() and get1() rather than the much more descriptive film() and language()

JOIN Types and Conditions

We can use innerJoinOn(), leftJoinOn(), rightJoinOn() and crossJoin() and tables can be joined using the conditions equal(), notEqual(), lessThan(), lessOrEqual(), greaterThan() and lessOrEqual().

What's Next?

Download open-source Java 14 here.
Download Speedment here.
Read all about the JOIN functionality in the Speedment User's Guide.