10

Is monadic programming in Java 8 slower? Below is my test (a right-biased Either is used that creates new instances for each computation). The imperative version is 1000 times faster. How do I program monadicaly in Java8 while getting comparable performance?

Main.java

public class Main {

    public static void main(String args[]){
        Main m = new Main();
        m.work();
        m.work2();
    }


    public void work(){
        final long start = System.nanoTime();
        final Either<Throwable,Integer> result =
                Try(this::getInput).flatMap((s) ->
                Try(this::getInput).flatMap((s2) ->
                parseInt(s).flatMap((i) ->
                parseInt(s2).map((i2) ->
                i + i2
                ))));
        final long end = System.nanoTime();
        result.map(this::println).leftMap(this::println);
        System.out.println((end-start)/1000+"us to execute");
    }

    public void work2(){
        Object result;
        final long start = System.nanoTime();
        try {
            final String s = getInput();
            final String s2 = getInput();

            final int i = parzeInt(s);
            final int i2 = parzeInt(s2);
            result = i + i2;
        }catch(Throwable t){
            result=t;
        }
        final long end = System.nanoTime();
        println(result);
        System.out.println((end-start)/1000+"us to execute");
    }

    public <A> A println(final A a){
        System.out.println(a);
        return a;
    }

    public  String getInput(){
        final Integer value = new Random().nextInt();
        if(value % 2 == 0) return "Surprise!!!";
        return value+"";
    }

    public Either<Throwable,Integer> parseInt(final String s){
        try{
            return Either.right(Integer.parseInt(s));
        }catch(final Throwable t){
            return Either.left(t);
        }
    }

    public Integer parzeInt(final String s){
        return Integer.parseInt(s);
    }
}

Either.java

public abstract class Either<L,R>
{
    public static <L,R> Either<L,R> left(final L l){
        return new Left(l);
    }

    public static <L,R> Either<L,R> right(final R r){
        return new Right(r);
    }

    public static<L,R> Either<L,R> toEither(final Optional<R> oR,final L l){
        return oR.isPresent() ? right(oR.get()) : left(l);
    }

    public static <R> Either<Throwable,R> Try(final Supplier<R> sr){
        try{
            return right(sr.get());
        }catch(Throwable t){
            return left(t);
        }
    }

    public abstract <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f);

    public abstract  <R2> Either<L,R2> map(final Function<R,R2> f);

    public abstract  <L2> Either<L2,R> leftMap(final Function<L,L2> f);

    public abstract  Either<R,L> swap();

    public static class Left<L,R> extends Either<L,R> {
        final L l;

        private Left(final L l){
            this.l=l;
        }

        public <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f){
            return (Either<L,R2>)this;
        }

        public <R2> Either<L,R2> map(final Function<R,R2> f){
            return (Either<L,R2>)this;
        }

        public <L2> Either<L2,R> leftMap(final Function<L,L2> f){
            return new Left(f.apply(l));
        }

        public Either<R,L> swap(){
            return new Right(l);
        }
    }

    public static class Right<L,R> extends Either<L,R> {
        final R r;

        private Right(final R r){
            this.r=r;
        }

        public <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f){
            return f.apply(r);
        }

        public <R2> Either<L,R2> map(final Function<R,R2> f){
            return new Right(f.apply(r));
        }

        public <L2> Either<L2,R> leftMap(final Function<L,L2> f){
            return (Either<L2,R>)this;
        }

        public Either<R,L> swap(){
            return new Left(r);
        }
    }
}
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
jordan3
  • 877
  • 5
  • 13
  • 5
    Just run `work` a few more times. Bootstrapping a lambda is expensive. – Sotirios Delimanolis Mar 12 '15 at 20:45
  • 1
    With 1.000.000 runs, the difference is more like 10x slower. With 80% of the time spent in Main.parseInt(String), for some reason... – Adrian Leonhard Mar 12 '15 at 20:50
  • changed work to return the time delta. Ran it for 1,000,000 runs and 10,000,000 runs. Still found the difference to be roughly 1000. I grabbed only the last evaluation (when it should be fairly warm). You can see the results in microseconds below Func:57411000 Imper:83000 – jordan3 Mar 12 '15 at 21:42
  • 1
    The JIT is not optimized for this sort of code; it is not obvious that this is necessarily possible. – Louis Wasserman Mar 12 '15 at 21:42

1 Answers1

9

While I don't quite understand your effort – apparently you are using map for side effects and you don't really have any alternative to get the result unboxed from the Either type – I have measured your work on JMH as it is. Your usage of Random was wrong, I corrected that. This is the code I used:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@OperationsPerInvocation(Measure.SIZE)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@Fork(1)
public class Measure
{
  static final int SIZE = 1;

  @Benchmark public Either<Throwable, Integer> workMonadically() {
    final Either<Throwable,Integer> result =
        Try(this::getInput).flatMap((s) ->
            Try(this::getInput).flatMap((s2) ->
                parseInt(s).flatMap((i) ->
                    parseInt(s2).map((i2) ->
                            i + i2
                    ))));
    return result;
  }

  @Benchmark public Object workImperatively() {
    Object result;
    try {
      final String s = getInput();
      final String s2 = getInput();

      final int i = parzeInt(s);
      final int i2 = parzeInt(s2);
      result = i + i2;
    }catch(Throwable t){
      result=t;
    }
    return result;
  }

  public String getInput() {
    final Integer value = ThreadLocalRandom.current().nextInt();
    if (value % 2 == 0) return "Surprise!!!";
    return String.valueOf(value);
  }

  public Either<Throwable,Integer> parseInt(final String s){
    try{
      return Either.right(Integer.parseInt(s));
    }catch(final Throwable t){
      return Either.left(t);
    }
  }

  public Integer parzeInt(final String s){
    return Integer.parseInt(s);
  }

  public static abstract class Either<L,R>
  {
    public static <L,R> Either<L,R> left(final L l){
      return new Left<>(l);
    }

    public static <L,R> Either<L,R> right(final R r){
      return new Right<>(r);
    }

    public static<L,R> Either<L,R> toEither(final Optional<R> oR,final L l){
      return oR.isPresent() ? right(oR.get()) : left(l);
    }

    public static <R> Either<Throwable,R> Try(final Supplier<R> sr){
      try{
        return right(sr.get());
      }catch(Throwable t){
        return left(t);
      }
    }

    public abstract <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f);

    public abstract  <R2> Either<L,R2> map(final Function<R,R2> f);

    public abstract  <L2> Either<L2,R> leftMap(final Function<L,L2> f);

    public abstract  Either<R,L> swap();

    public static class Left<L,R> extends Either<L,R> {
      final L l;

      private Left(final L l){
        this.l=l;
      }

      @Override public <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f){
        return (Either<L,R2>)this;
      }

      @Override public <R2> Either<L,R2> map(final Function<R,R2> f){
        return (Either<L,R2>)this;
      }

      @Override public <L2> Either<L2,R> leftMap(final Function<L,L2> f){
        return new Left<>(f.apply(l));
      }

      @Override public Either<R,L> swap(){
        return new Right<>(l);
      }
    }

    public static class Right<L,R> extends Either<L,R> {
      final R r;

      private Right(final R r){
        this.r=r;
      }

      @Override public <R2> Either<L,R2> flatMap(final Function<R,Either<L,R2>> f){
        return f.apply(r);
      }

      @Override public <R2> Either<L,R2> map(final Function<R,R2> f){
        return new Right<>(f.apply(r));
      }

      @Override public <L2> Either<L2,R> leftMap(final Function<L,L2> f){
        return (Either<L2,R>)this;
      }

      @Override public Either<R,L> swap(){
        return new Left<>(r);
      }
    }
  }
}

and this is the result:

Benchmark                 Mode  Cnt     Score     Error  Units
Measure.workImperatively  avgt    5  1646,874 ± 137,326  ns/op
Measure.workMonadically   avgt    5  1990,668 ± 281,646  ns/op

So there's almost no difference at all.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • "Almost no difference"?!? **Imperative is 15-20% faster.** – Has QUIT--Anony-Mousse Mar 14 '15 at 13:27
  • @Anony-Mousse When looked from the height of "1000 times faster", "0.2 times faster" does indeed sound like "almost no difference". And even in absolute terms, such difference would be almost negligible in any practical situation. But since OP's code does not focus enough on just the difference between idioms, I would expect the ratio of pure overheads of binding steps together to be quite higher: more like 2-3. – Marko Topolnik Mar 14 '15 at 19:58
  • In such tiny cases, inlining usually works. That's why the difference isn't more than that. But in more complex situations, it may well accumulate, unfortunately. In particular once `Either`, streams, etc. are polymorph or multimorph etc. – Has QUIT--Anony-Mousse Mar 14 '15 at 20:05
  • @Anony-Mousse I would wager that generating a random int, turning it to string, and then parsing back to int, would take a *bit* more time than four method dispatches. Therefore the true overhead will rise. To confirm I have replaced OP's implementation with constant-returning methods, now the result is 26 ns/op for imperative and 64 ns/op for FP. That matches exactly my estimate above. – Marko Topolnik Mar 14 '15 at 20:14
  • The problem is that this is the cost in the easiest-to-optimize case. Consider some nested scenario like matrix multiplication, when hotspot gives up on inlining, for example. :-( – Has QUIT--Anony-Mousse Mar 14 '15 at 20:34
  • JVM won't *give up* inlining, it will just not inline the whole thing. So you may e.g. have nine levels of inlined nesting plus one or two monomorphic calls in the end. – Marko Topolnik Mar 14 '15 at 21:27