简体   繁体   中英

How to build a more efficient functional code ? Java FP

as I am training myself with Euler project, I've hardly found a way to resolve problem #5 with Functional approach.

The objective is to find a number that is divisible by all integer numbers from 2 to 20.

I first resolved it with classical Java (I know my code is not nice one and am sorry for that) then I wanted to obtain the result with FP, thinking efficienceness would be greater.

Plain old java took 750 ms to find the result. Stream / FP took around 750 ms.

Have you any ideas / explanations about why FP way need so much time to complete ? I guess my code is not the nicer one, neither plain old java one nor FP one.

But I'd like to understand where I certainly made something wrong.

Notice that parallelizing Stream processing gains about 130 ms (750 ms -> 620 ms).

Notice 2 : it would be nice to start from 9699690L (that is : 2*3*5*7*9*11*13*17*19 ), but it seems to be very looooonger for the app (for both Plain Old Java and FP way) to start... Why ??

Here is the Plain Old Java code :

@Test
    void test() {
        long start = System.currentTimeMillis();
        boolean foundValue = false;
        long valueToFindOut = 20L;
        List<Long> divisors = Arrays.asList(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L,
                19L, 20L);

        while (!foundValue) {
            boolean found = false;
            for (long div : divisors) {
                if (isDivisible(valueToFindOut, div)) {
                    found = true;
                } else {
                    found = false;
                    break;
                }
            }
            if (!found) {
                valueToFindOut += 20L;
            } else {
                foundValue = true;
                System.out.println("Valeur trouvée = " + valueToFindOut);
            }
        }
        for (long div : divisors) {
            assertTrue(isDivisible(valueToFindOut, div));
        }
        long end = System.currentTimeMillis();
        System.out.println("Résultat obtenu en " + (end - start) + " millisecondes");
    }

private boolean isDivisible(long toDivide, long divisor) {
        return toDivide % divisor == 0;
    }

Functional code is the following :

@Test
    void testLambda() {
        long start = System.currentTimeMillis();
        List<Long> divisors = Arrays.asList(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L,
                19L, 20L);
        Predicate<Long> predicate = longPredicate(divisors);
        long result = generateLongStream().filter(predicate).findFirst().get();
        long end = System.currentTimeMillis();
        System.out.println("Resultat = " + result + " obtenu en " + (end - start) + " millisecondes.");
    }

    private boolean isDivisible(long toDivide, long divisor) {
        return toDivide % divisor == 0;
    }

    private Stream<Long> generateLongStream() {
        return Stream.iterate(20L, l -> l + 20L).parallel();
    }

    private Predicate<Long> longPredicate(List<Long> longs) {
        long start = System.currentTimeMillis();
        Predicate<Long> predicate = null;
        if(!(longs.isEmpty())) {
            List<Predicate<Long>> predicates = new ArrayList<Predicate<Long>>(longs.size());
            longs.forEach(divisor -> {
                predicates.add(valueToTest -> isDivisible(valueToTest, divisor));
            });
            for(int i = 0; i < predicates.size(); i++) {
                if(i == 0) {
                    predicate = predicates.get(i);
                } else {
                    predicate = predicate.and(predicates.get(i));
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("Predicate construit en " + (end - start) + " millisecondes.");
        return predicate;
    }

Thanx by advance for all your advices.

We can replace looping over a list of things...

for( Thing thing : things ){
    process(thing);
}

...with something more functional....

things.forEach( thing -> process( thing ) );

...but what actually happens is very similar: we have to iterate over the list calling the process method for each element of the list. The functional version might even be slightly slower because there's an extra method call to the lambda before the call to the useful method.

So I don't think it's a surprise that the functional version takes a similar time to the original.

The advantage of the functional version might be

  • it's a little shorter
  • you might find it a little easier to read
  • the lambda can be supplied from somewhere else (say as a method parameter)
  • a lambda needs a lot less boilerplate code than anonymous inner classes

But none of those will help the performance.

I will first begin by answering my comment: you should avoid Long and favor long when dealing with long based algorithm.

You must never ignore the cost of (un)boxing operation: I rewrote your code using long , long[] , LongPredicate and LongStream , and on my Ryzen 7 2700X it took the following:

┌─────────────────┬───────┐
│     Method      │ Time  │
├─────────────────┼───────┤
│ LongStream      │ 129ms │
│ Old Java (Long) │ 336ms │
│ Old Java (long) │ 273ms │
└─────────────────┴───────┘

The implementation is given below (sorry if it is too long I don't know how to attach file to Stackoverflow and I don't think pastebin will do).

The LongStream method is the winner here, but I think the benchmark is wrong:

  • You must never bench using System::currentTimeMillis . This method return the current time, which may change (let's say the clock was adjusted due to NTP). You must use System::nanoTime .
  • You bench one execution, at some random time. Thus, you don't get a clear understanding of which does the job better.
  • You are no benching right: you should use JMH ("Java Microbenchmark Harness"). This tutorial , which may be outdated (at least the maven archetype is), may help you understanding what is a benchmark.

The longPredicate method is doing too much work:

  • You don't need to convert the List<Predicate> to then convert it to one single Predicate .
  • You can simply initialize the initial predicate and call predicate = predicate.and(...) .

Note that I had to use a method return a lambda, since you would not be able to reference the i :

for (int i = 1; i < longs.length; ++i) {
  predicate = predicate.and(n -> isDisivisble(n, longs[i])); // fail because i is not final
}

You could also create a new local variable (this is done with a parameter in my method.)

So, here the result given by JMH:

┌─────────────────────────────────┬────────┬───────┬───────────┬───────────┬────────┐
│ Benchmark                       │  Mode  │  Cnt  │  Score    │  Error    │  Units │
├─────────────────────────────────┼────────┼───────┼───────────┼───────────┼────────┤
│ PlainOldJavaLongPrimitive.test  │  avgt  │  10   │  188,072  │    1,002  │  ms/op │
│ PlainOldJavaLongWrapper.test    │  avgt  │  10   │  265,649  │    0,920  │  ms/op │
│ StreamLongPrimitive.testLambda  │  avgt  │  10   │   86,046  │    1,829  │  ms/op │
│ StreamLongWrapper.testLambda    │  avgt  │  10   │  230,158  │   34,122  │  ms/op │
│ PlainOldJavaLongPrimitive.test  │  ss    │  10   │  198,192  │   37,573  │  ms/op │
│ PlainOldJavaLongWrapper.test    │  ss    │  10   │  268,587  │    7,378  │  ms/op │
│ StreamLongPrimitive.testLambda  │  ss    │  10   │  116,108  │   65,161  │  ms/op │
│ StreamLongWrapper.testLambda    │  ss    │  10   │  532,534  │  335,032  │  ms/op │
└─────────────────────────────────┴────────┴───────┴───────────┴───────────┴────────┘
  • You can observe that using primitive is better than wrapper.
  • You can observe using Stream is better than "plain old java".

--

To run this JMH project:

  • Ensure you have Maven 3.6.1 .
  • Ensure you have Java 11
  • Compile the project: mvn install
  • Run the benchmark: java -jar target/microbenchmarks.jar -rff bench

The files:

  • /pom.xml
  • /src/main/java/com/stackoverflow/nodatafound/q56622173/PlainOldJavaLongPrimitive.java
  • /src/main/java/com/stackoverflow/nodatafound/q56622173/PlainOldJavaLongWrapper.java
  • /src/main/java/com/stackoverflow/nodatafound/q56622173/StreamLongPrimitive.java
  • /src/main/java/com/stackoverflow/nodatafound/q56622173/StreamLongWrapper.java

/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.stackoverflow.nodatafound</groupId>
  <artifactId>stackoverflow-56622173</artifactId>
  <version>1</version>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>${project.build.sourceEncoding}</project.reporting.outputEncoding>
    <maven.compiler.release>11</maven.compiler.release>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>1.21</version>
    </dependency>
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-generator-annprocess</artifactId>
      <version>1.21</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-enforcer-plugin</artifactId>
        <version>3.0.0-M2</version>
        <executions>
          <execution>
            <id>enforce-maven</id>
            <inherited>true</inherited>
            <goals>
              <goal>enforce</goal>
            </goals>
            <configuration>
              <rules>
                <requireMavenVersion>
                  <version>[3.6.1,)</version>
                </requireMavenVersion>
                <requireJavaVersion>
                  <version>[11.0.0,)</version>
                </requireJavaVersion>
              </rules>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <finalName>microbenchmarks</finalName>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>org.openjdk.jmh.Main</mainClass>
                </transformer>
              </transformers>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/services/javax.annotation.processing.Processor</exclude>
                  </excludes>
                </filter>
              </filters>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

/src/main/java/com/stackoverflow/nodatafound/q56622173/PlainOldJavaLongPrimitive.java

package com.stackoverflow.nodatafound.q56622173;

import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode({ Mode.AverageTime, Mode.SingleShotTime })
public class PlainOldJavaLongPrimitive {

  @Benchmark
  public Object test() {
    boolean foundValue = false;
    long valueToFindOut = 20L;
    final long[] divisors = new long[] { 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L,
        19L, 20L };
    while (!foundValue) {
      boolean found = false;
      for (final long div : divisors) {
        if (isDivisible(valueToFindOut, div)) {
          found = true;
        } else {
          found = false;
          break;
        }
      }
      if (!found) {
        valueToFindOut += 20L;
      } else {
        foundValue = true;
      }
    }
    for (final long div : divisors) {
      if (!isDivisible(valueToFindOut, div)) {
        throw new AssertionError("valueToFindOut: " + valueToFindOut + ", div: " + div);
      }
    }
    return Long.valueOf(valueToFindOut);
  }

  private boolean isDivisible(final long toDivide, final long divisor) {
    return toDivide % divisor == 0;
  }
}

/src/main/java/com/stackoverflow/nodatafound/q56622173/PlainOldJavaLongWrapper.java

package com.stackoverflow.nodatafound.q56622173;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode({ Mode.AverageTime, Mode.SingleShotTime })
public class PlainOldJavaLongWrapper {

  @Benchmark
  public Object test() {
    boolean foundValue = false;
    long valueToFindOut = 20L;
    final List<Long> divisors = Arrays.asList(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L,
        18L, 19L, 20L);
    while (!foundValue) {
      boolean found = false;
      for (final long div : divisors) {
        if (isDivisible(valueToFindOut, div)) {
          found = true;
        } else {
          found = false;
          break;
        }
      }
      if (!found) {
        valueToFindOut += 20L;
      } else {
        foundValue = true;
      }
    }
    for (final long div : divisors) {
      if (!isDivisible(valueToFindOut, div)) {
        throw new AssertionError("valueToFindOut: " + valueToFindOut + ", div: " + div);
      }
    }
    return Long.valueOf(valueToFindOut);
  }

  private boolean isDivisible(final long toDivide, final long divisor) {
    return toDivide % divisor == 0;
  }
}

/src/main/java/com/stackoverflow/nodatafound/q56622173/StreamLongPrimitive.java

package com.stackoverflow.nodatafound.q56622173;

import java.util.concurrent.TimeUnit;
import java.util.function.LongPredicate;
import java.util.stream.LongStream;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode({ Mode.AverageTime, Mode.SingleShotTime })
public class StreamLongPrimitive {
  @Benchmark
  public Object testLambda() {
    final long[] divisors = new long[] { 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L,
        19L, 20L };
    final LongPredicate predicate = longPredicate(divisors);
    return generateLongStream().filter(predicate).findFirst().getAsLong();
  }

  private LongPredicate divisiblePredicate(final long divisor) {
    return n -> n % divisor == 0;
  }

  private LongStream generateLongStream() {
    return LongStream.iterate(20L, l -> l + 20L).parallel();
  }

  private LongPredicate longPredicate(final long[] longs) {
    if (longs.length == 0) {
      throw new IllegalArgumentException("Pas de diviseurs");
    }
    LongPredicate predicate = divisiblePredicate(longs[0]);
    for (int i = 1; i < longs.length; ++i) {
      predicate = predicate.and(divisiblePredicate(longs[i]));
    }
    return predicate;
  }
}

/src/main/java/com/stackoverflow/nodatafound/q56622173/StreamLongWrapper.java

package com.stackoverflow.nodatafound.q56622173;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

@State(Scope.Benchmark)
@Warmup(iterations = 2)
@Fork(1)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode({ Mode.AverageTime, Mode.SingleShotTime })
public class StreamLongWrapper {
  @Benchmark
  public Object testLambda() {
    final List<Long> divisors = Arrays.asList(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L,
        18L, 19L, 20L);
    final Predicate<Long> predicate = longPredicate(divisors);
    return generateLongStream().filter(predicate).findFirst().get();
  }

  private boolean isDivisible(final long toDivide, final long divisor) {
    return toDivide % divisor == 0;
  }

  private Stream<Long> generateLongStream() {
    return Stream.iterate(20L, l -> l + 20L).parallel();
  }

  private Predicate<Long> longPredicate(final List<Long> longs) {
    if (longs.isEmpty()) {
      throw new IllegalArgumentException("Pas de diviseurs");
    }
    final List<Predicate<Long>> predicates = new ArrayList<>(longs.size());
    longs.forEach(divisor -> {
      predicates.add(valueToTest -> isDivisible(valueToTest, divisor));
    });
    Predicate<Long> predicate = predicates.get(0);
    for (int i = 1; i < predicates.size(); i++) {
      predicate = predicate.and(predicates.get(i));
    }
    return predicate;
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM