Performance drawback with subtyping

For feed our discussion, I tried to reconstruct Leandro Martinez’s experiment with 5 lines types and n = 1,000,000 in Java language.

If I was unfair at some point, please tell me. The results were:

  • Naivesum Mean Duration: 15.65 ms
  • OO Mean Duration with objects: 19.513 ms

The Code

import java.util.List;
import java.util.ArrayList;
import java.util.Random;

abstract class LineAbstract {
    public abstract Float getLength();
    public abstract void setLength(Float l);
}

class Line1 extends LineAbstract { 
    private Float length; 
    public Float getLength() { return length; } 
    public void setLength(Float l){ this.length = l; } 
}
class Line2 extends LineAbstract { 
    private Float length; 
    public Float getLength() { return length; } 
    public void setLength(Float l){ this.length = l; }
}
class Line3 extends LineAbstract { 
    private Float length; 
    public Float getLength() { return length; } 
    public void setLength(Float l){ this.length = l; } 
}
class Line4 extends LineAbstract { 
    private Float length; 
    public Float getLength() { return length; } 
    public void setLength(Float l){ this.length = l; } 
}
class Line5 extends LineAbstract { 
    private Float length; 
    public Float getLength() { return length; } 
    public void setLength(Float l){ this.length = l; } 
}

class Picture {
    public List<LineAbstract> lines = new ArrayList<>();
    
    public Float f(Float v){ return (float) Math.sin(v); }
    
    public Float paint(){
        float s = 0F;
        for (LineAbstract l: lines){
            s += f(l.getLength());
        }        
        return s;
    }
}

public class Experiment {
    public static void main(String[] args) throws Exception {
        int n = 1000000;
        float[] x = new float[n];
        Random r = new Random();

        Class[] line_types = {Line1.class, Line2.class, Line3.class, Line4.class, Line5.class};
        Picture p = new Picture();

        for (int i = 0; i < n; i++){
            x[i] = (float) Math.random();
            Class clazz = line_types[ r.nextInt(5) ];
            LineAbstract la = (LineAbstract) clazz.newInstance();
            la.setLength(x[i]);
            p.lines.add( la );
        }

        int repetitions = 1000;
        long totalDuration = 0;

        float s1 = 0F;
        for (int i = 0; i < repetitions; i++){
            s1 = 0F;
            long startTime = System.currentTimeMillis();
    
            //Naivesum
            for (int j = 0; j < n; j++) 
                s1 += (float) Math.sin(x[j]);
    
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            totalDuration += duration;
        }

        System.out.println("Naivesum Mean Duration: " + (float) (totalDuration) / repetitions + " ms");

        totalDuration = 0;

        float s2 = 0F;
        for (int i = 0; i < repetitions; i++){
            long startTime = System.currentTimeMillis();
    
            s2 = p.paint();
    
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            totalDuration += duration;
        }

        System.out.println("OO Mean Duration with objects: " + (float) (totalDuration) / repetitions + " ms");
        
        Float tol = 0.001F;
        System.out.println("\ns1 = " + s1 + " s2 = " + s2);
        System.out.println("The results are the same: " + (Math.abs(s1 - s2) < tol) );
    }
}

I’m using Java 15.0.1.

So, In a simple sum Java was slower than Julia. Julia took 4.849 ms (at my PC) and Java took 15.65 ms. I wasn’t expecting this.
When we use splitting and annotated splitting Julia was faster than Java for a structured list, Julia took 12.604 and 12.501 ms (my PC) and Java took 19.513 ms.

Dynamic dispatch and functors were slower than Java, they took 30.833 and 31.213 ms.

Details results of Leandro Martinez’s expriments at my PC:

  • with runtime dispatch: 30.833 ms (1000000 allocations: 15.26 MiB)
  • with splitting: 12.604 ms (0 allocations: 0 bytes)
  • with annotated splitting: 12.501 ms (0 allocations: 0 bytes)
  • with functors: 31.213 ms (1000000 allocations: 15.26 MiB)

In a nutshell, if you are creating a new code. You can put a splitting or annotated splitting to get a good performance. But if you are reusing someone code, dynamic dispatch will be approximately 2,5 times slower compared with splitting.

(Legal Leandro conhecer outro brasileiro trabalhando com Julia. Sou professor de computação no IF Goiano Ceres)

1 Like