C# and Java Comparison: The C# Struct Advantage
C# vs. Java and .Net vs. JVM are never-ending wars. Each ecosystem has its own advantages, loyal fans, and success stories. But, I think .NET has a weapon that Java doesn't: C#'s struct.
Java Test Case
I create a naive class with two public fields of type integer.
private static class Coords{
public int x;
public int y;
public Coords(int x, int y) {
this.x = x;
this.y = y;
}
}
Then, I create a test case with JMH.
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testLoopAndGetData(BenchMarkState state, Blackhole bh){
Coords value;
for(int i = 0; i < state.SIZE;i++) {
value = state.data[i];
bh.consume(value.x);
bh.consume(value.y);
}
}
@State(Scope.Benchmark)
public static class BenchMarkState {
@Setup
public void init() {
Random r = new Random();
for(int i = 0; i < SIZE; i++) {
int nextInt = r.nextInt();
data[i] = new Coords(nextInt, nextInt);
}
}
public final int SIZE = 5000000;
public Coords[] data = new Coords[SIZE];
}
Java takes nearly 20ms for each operation, as seen below:
Benchmark |
Mode |
Cnt |
Score |
Error |
Units |
TestJavaStruct.testLoopAndGetData |
avgt |
200 |
19.787 |
± 0.152 |
ms/op |
C# Test Case
I run .NET on Ubuntu 16 with .NET Core and MonoDevelop as my IDE. I created a similar structure with the class version of Java.
public struct Coords
{
public int x, y;
public Coords(int p1, int p2)
{
x = p1;
y = p2;
}
}
Then, I use BenchmarkDotNet to create a test case.
[CoreJob]
[RPlotExporter, RankColumn]
public class BenchmarkStruct
{
private Coords[] data;
public int N = 5000000;
[GlobalSetup]
public void Setup()
{
Random r= new Random(42);
data = new Coords[N];
for (int i = 0; i < N; i++)
{
data[i] = new Coords(r.Next(), r.Next());
}
}
[Benchmark]
public int Loop() {
int a = 0, b = 0;
for(int i = 0; i < N; i++) {
a = data[i].x;
b = data[i].y;
}
return a & b;
}
}
We can see that .NET performs very well with struct in this scenario.
| Method | Mean | Error | StdDev | Rank |
|------- |---------:|----------:|----------:|-----:|
| Loop | 3.167 ms | 0.0473 ms | 0.0420 ms | 1 |
A Solution From Java
There is an attempt to "deliver struct types for Java programming language" called Junion. As they described, a struct can save a significant amount of memory when compared with a standard class in Java. It's a very good optimization when you have some arrays with millions of objects. But I was wondering if Junion can provide a solution as fast as a struct in C#.
Declaring a struct with Junion is simple.
@Struct
private static class CoordsStruct{
public int x;
public int y;
}
Then, the test case with JMH.
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testJunion(BenchMarkState state, Blackhole bh){
CoordsStruct value;
for(int i = 0; i < state.SIZE;i++) {
value = state.dataStruct[i];
bh.consume(value.x);
bh.consume(value.y);
}
}
@State(Scope.Benchmark)
public static class BenchMarkState {
@Setup
public void init() {
Random r = new Random();
for(int i = 0; i < SIZE; i++) {
int nextInt = r.nextInt();
dataStruct[i] = new CoordsStruct();
dataStruct[i].x = nextInt;
dataStruct[i].y = nextInt;
}
}
public final int SIZE = 5000000;
public CoordsStruct[] dataStruct = new CoordsStruct[SIZE];
}
This is no surprise — Junion can save memory, but it cannot boost the processing time.
Benchmark |
Mode |
Cnt |
Score |
Error |
Units |
TestJavaStruct.testJunion |
avgt |
200 |
19.970 |
± 0.236 |
ms/op |
Conclusion
As I understand it, C#'s struct is a ValueType, and it's "stack-allocated or allocated inline in a structure." So, when we declare an array of Struct, .NET can allocate a region of memory to store the array, and all items of the array are consecutive. Consequently, when we iterate on an array of structs, this action takes advantage of the CPU cache line and performs very fast.
Java is a different story; an array of objects stores only a pointer (or reference, the memory address of the object). Every access might force JVM to read data from memory (or the L3 cache of the CPU). This action isn't "Mechanical Sympathy" and slows down the iteration. You can get more detail about the CPU cache line and how it impacts iteration in my previous article.
Disclaimer: I have not used .NET or C# for professional purposes since 2012, and I know a little bit about Java (a small part of the JVM ecosystem). So, please correct me if I'm wrong; I appreciate any comment or correction.