Does Java – JIT optimize new objects?
I created this class, which is immutable and has a smooth API:
public final class Message { public final String email; public final String escalationEmail; public final String assignee; public final String conversationId; public final String subject; public final String userId; public Message(String email,String escalationEmail,String assignee,String conversationId,String subject,String userId) { this.email = email; this.escalationEmail = escalationEmail; this.assignee = assignee; this.conversationId = conversationId; this.subject = subject; this.userId = userId; } public Message() { email = ""; escalationEmail = ""; assignee = ""; conversationId = ""; subject = ""; userId = ""; } public Message email(String e) { return new Message(e,escalationEmail,assignee,conversationId,subject,userId); } public Message escalationEmail(String e) { return new Message(email,e,userId); } public Message assignee(String a) { return new Message(email,a,userId); } public Message conversationId(String c) { return new Message(email,c,userId); } public Message subject(String s) { return new Message(email,s,userId); } public Message userId(String u) { return new Message(email,u); } }
My question is whether the optimizer can avoid a large number of object creation when creating a new object like this:
Message m = new Message() .email("foo@bar.com") .assignee("bar@bax.com") .subject("subj");
Instead, what are the benefits of making a separate variable builder object?
Update 2: after reading apangin's answer, my benchmark is invalid I'll leave it here for reference. How not to benchmark:)
Update: I venture to use this code to measure myself:
public final class Message { public final String email; public final String escalationEmail; public final String assignee; public final String conversationId; public final String subject; public final String userId; public static final class MessageBuilder { private String email; private String escalationEmail; private String assignee; private String conversationId; private String subject; private String userId; MessageBuilder email(String e) { email = e; return this; } MessageBuilder escalationEmail(String e) { escalationEmail = e; return this; } MessageBuilder assignee(String e) { assignee = e; return this; } MessageBuilder conversationId(String e) { conversationId = e; return this; } MessageBuilder subject(String e) { subject = e; return this; } MessageBuilder userId(String e) { userId = e; return this; } public Message create() { return new Message(email,userId); } } public static MessageBuilder createNew() { return new MessageBuilder(); } public Message(String email,String userId) { this.email = email; this.escalationEmail = escalationEmail; this.assignee = assignee; this.conversationId = conversationId; this.subject = subject; this.userId = userId; } public Message() { email = ""; escalationEmail = ""; assignee = ""; conversationId = ""; subject = ""; userId = ""; } public Message email(String e) { return new Message(e,userId); } public Message escalationEmail(String e) { return new Message(email,userId); } public Message assignee(String a) { return new Message(email,userId); } public Message conversationId(String c) { return new Message(email,userId); } public Message subject(String s) { return new Message(email,userId); } public Message userId(String u) { return new Message(email,u); } static String getString() { return new String("hello"); // return "hello"; } public static void main(String[] args) { int n = 1000000000; long before1 = System.nanoTime(); for (int i = 0; i < n; ++i) { Message m = new Message() .email(getString()) .assignee(getString()) .conversationId(getString()) .escalationEmail(getString()) .subject(getString()) .userId(getString()); } long after1 = System.nanoTime(); long before2 = System.nanoTime(); for (int i = 0; i < n; ++i) { Message m = Message.createNew() .email(getString()) .assignee(getString()) .conversationId(getString()) .escalationEmail(getString()) .subject(getString()) .userId(getString()) .create(); } long after2 = System.nanoTime(); System.out.println("no builder : " + (after1 - before1)/1000000000.0); System.out.println("with builder: " + (after2 - before2)/1000000000.0); } }
If the string parameter is not a new object, then I find that the difference is great (the builder is faster), but all are the same (see the comment code in getString)
I imagine a more realistic scenario. When all strings are new objects, the difference is negligible, and JVM startup will slow down the first string (I tried two ways)
Using "new string" code is many times slower in total (I have to reduce N), which may indicate that some optimization of "new message" is in progress, rather than "new string"
Solution
Yes, hotspot JIT can eliminate redundant allocation in the local environment
This optimization is provided by JDK 6u23 enabled escape analysis It is often confused with stack allocation, but it is actually more powerful because it not only allows objects to be allocated on the stack, but also completely eliminates allocation by replacing object fields with variables (scalar substitution) Optimization
Optimization is controlled by the - XX: eliminateallocations JVM option, which is on by default
Because of allocation elimination optimization, the examples you create message objects work effectively in the same way They do not assign intermediate objects; Just the last one
Your benchmark shows misleading results because it collects many micro benchmarks for common pitfalls:
>It contains several benchmarks in a method; > It measures the OSR stub instead of the final compiled version; > It does not do warm-up iterations; > It does not consume results, etc
Let's measure it correctly with jmh As a reward, jmh has an allocation profiler (- Prof GC), which shows how many bytes are actually allocated for each iteration I added a third test that disables elite allocations optimization to show differences
package bench; import org.openjdk.jmh.annotations.*; @State(Scope.Benchmark) public class MessageBench { @Benchmark public Message builder() { return Message.createNew() .email(getString()) .assignee(getString()) .conversationId(getString()) .escalationEmail(getString()) .subject(getString()) .userId(getString()) .create(); } @Benchmark public Message immutable() { return new Message() .email(getString()) .assignee(getString()) .conversationId(getString()) .escalationEmail(getString()) .subject(getString()) .userId(getString()); } @Benchmark @Fork(jvmArgs = "-XX:-EliminateAllocations") public Message immutableNoOpt() { return new Message() .email(getString()) .assignee(getString()) .conversationId(getString()) .escalationEmail(getString()) .subject(getString()) .userId(getString()); } private String getString() { return "hello"; } }
The results are as follows Both builder and immutable perform the same operation, allocating only 40 bytes per iteration (exactly the size of a message object)
Benchmark Mode Cnt score Error Units MessageBench.builder avgt 10 6,232 ± 0,111 ns/op MessageBench.immutable avgt 10 6,213 ± 0,087 ns/op MessageBench.immutableNoOpt avgt 10 41,660 ± 2,466 ns/op MessageBench.builder:·gc.alloc.rate.norm avgt 10 40,000 ± 0,001 B/op MessageBench.immutable:·gc.alloc.rate.norm avgt 10 40,001 B/op MessageBench.immutableNoOpt:·gc.alloc.rate.norm avgt 10 280,001 B/op