0

Je lis "Java 8 In Action" (par Raoul-Gabriel Urma, Mario Fusco et Alan Mycroft), section 5.6.3, pages 116 et 117. Le code qui est présenté gère le calcul de soi-disant "Triplets de Pythagore". La page 116 montre la première tentative, et la page 117 montre une tentative améliorée de générer ces triplets, où tous deux utilisent la méthode ".rangeClosed()".Flux numériques Exemple d'optimisation

J'ai trouvé quelques optimisations qui vont au-delà du livre et j'aimerais les partager ici. J'ai fait quelques simples calculs "System.currentTimeMillis()" pour voir si mes modifications étaient des améliorations, et elles semblaient être légèrement meilleures que celles trouvées dans le livre. Pouvez-vous fournir de meilleures améliorations, explications ou statistiques pour ce code?

public void test() { 

    long time1 = System.currentTimeMillis(); 

    /* 
    * From text, page 116 
    */ 
    IntStream.rangeClosed(1, 100).boxed() 
     .flatMap(a -> IntStream.rangeClosed(a, 100) 
       .filter(b -> Math.sqrt(a*a + b*b) % 1 == 0) 
       .mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a*a + b*b)}) 
     ) 
     .forEach(c -> System.out.println("["+c[0]+" "+c[1]+" "+c[2]+"]")); 

    long time2 = System.currentTimeMillis(); 

    System.out.println(); 

    long time3 = System.currentTimeMillis(); 

    /* 
    * From text, page 117, I added "map(...)" so that end result are ints, not doubles 
    */ 
    IntStream.rangeClosed(1, 100).boxed() 
     .flatMap(a -> IntStream.rangeClosed(a, 100) 
       .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) 
       .filter(t -> t[2] % 1 == 0) 
       .map(b -> new int[]{(int)b[0], (int)b[1], (int)b[2]}) 
     ) 
     .forEach(c -> System.out.println("["+c[0]+" "+c[1]+" "+c[2]+"]")); 

    long time4 = System.currentTimeMillis(); 

    System.out.println(); 

    long time5 = System.currentTimeMillis(); 

    /* 
    * My optimization attempt #1: now mapToObj(...) has c%1!=0 conditional, filter checks array element not null 
    */ 
    IntStream.rangeClosed(1, 100).boxed() 
    .flatMap(a -> IntStream.rangeClosed(a, 100) 
       .mapToObj(b -> { 
        double c = Math.sqrt(a*a + b*b); 
        return new Object[]{a, b, c % 1 == 0 ? (int)c : null}; 
       }) 
       .filter(d -> d[2] != null) 
       .map(e -> new int[]{(int)e[0], (int)e[1], (int)e[2]}) 
    ) 
    .forEach(f -> System.out.println("["+f[0]+" "+f[1]+" "+f[2])); 

    long time6 = System.currentTimeMillis(); 

    System.out.println(); 

    long time7 = System.currentTimeMillis(); 

    /* 
    * My optimization attempt #2: now mapToObj(...) has c%1!=0 conditional, filter checks "array element" not 0 
    */ 
    IntStream.rangeClosed(1, 100).boxed() 
     .flatMap(a -> IntStream.rangeClosed(a, 100) 
       .mapToObj(b -> { 
        double c = Math.sqrt(a*a + b*b); 
        return new int[]{a, b, c % 1 == 0 ? (int)c : 0 }; 
       }) 
       .filter(t -> t[2] != 0) 
     ) 
     .forEach(d -> System.out.println("["+d[0]+" "+d[1]+" "+d[2]+"]")); 

    long time8 = System.currentTimeMillis(); 

    System.out.println(); 

    long time9 = System.currentTimeMillis(); 

    /* 
    * My optimization attempt #3: now mapToObj(...) has c%1!=0 conditional, filter checks "collection element" not null 
    */ 
    IntStream.rangeClosed(1, 100).boxed() 
     .flatMap(a -> IntStream.rangeClosed(a, 100) 
       .mapToObj(b -> { 
        double c = Math.sqrt(a*a + b*b); 
        return (c % 1 != 0) ? null : new int[]{a, b, (int)c}; 
       }) 
       .filter(t -> t != null) 
     ) 
     .forEach(d -> System.out.println("["+d[0]+" "+d[1]+" "+d[2]+"]")); 

    long time10 = System.currentTimeMillis(); 

    System.out.println(); 

    long timeDelta1 = time2 - time1; 
    long timeDelta2 = time4 - time3; 
    long timeDelta3 = time6 - time5; 
    long timeDelta4 = time8 - time7; 
    long timeDelta5 = time10 - time9; 

    System.out.println("timeDelta1: " + timeDelta1 + ", timeDelta2: " + timeDelta2 + ", timeDelta3: " + timeDelta3 + ", timeDelta4: " + timeDelta4 + ", timeDelta5: " + timeDelta5); 

} 

public static void main(String[] args){ 
    ReduceTest reduceTest = new ReduceTest(); 
    reduceTest.test(); 
} 

REMARQUE: Il semble que vous pouvez utiliser "return;" dans une méthode ".forEach()", mais pas dans une méthode ".mapToInt()". En utilisant "retour" dans le lambda passé dans la méthode ".mapToInt()" supprimerait le besoin d'avoir la méthode ".filter()". Il semble que ce serait une amélioration pour les cours d'api.

+1

Je pense que cette question est discutée à mieux: http://codereview.stackexchange.com/ – Flown

+0

@Flown, bon point, je l'ai fait. –

+0

@Alexis, merci, cette balise stream devrait plutôt être java-stream, vous avez bien sûr raison. –

Répondre

3

D'abord, vous "benchmark" est fortement défectueux. Il semblera très probablement toujours que les dernières variantes sont plus rapides, parce qu'une plus grande partie du code partagé (par exemple des méthodes d'API de flux) a été compilée/optimisée lorsqu'elles sont exécutées. Voir “How do I write a correct micro-benchmark in Java?”

De plus, "%1" est inutile lorsque vous voulez tester si un nombre est un nombre entier. Vous pouvez convertir en int, ce qui supprimera la partie fraction, et comparez-la avec l'original double. De plus, vous ne savez pas besoin de map à un tableau int[], lorsque vous déjà ce que vous allez imprimer, sera un String:

IntStream.rangeClosed(1, 100).boxed() 
    .flatMap(a -> IntStream.rangeClosed(a, 100) 
      .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) 
      .filter(t -> ((int)t[2]) == t[2]) 
      .map(arr -> String.format("[%.0f %.0f %.0f]", arr[0], arr[1], arr[2])) 
    ) 
    .forEach(System.out::println); 

Mais, bien sûr, si vous savez que vous avez besoin une fonction coûteuse comme sqrt plusieurs fois, calculer à l'avance peut être bénéfique, en particulier, quand il est possible de préparer sans utiliser cette fonction coûteuse, voire arithmétique à virgule flottante en général:

int[] square = new int[20001]; 
IntStream.rangeClosed(1, 141).forEach(i -> square[i*i]=i); 
IntStream.rangeClosed(1, 100).boxed() 
    .flatMap(a -> IntStream.rangeClosed(a, 100) 
      .filter(b -> square[a*a+b*b]!=0) 
      .mapToObj(b -> String.format("[%d %d %d]", a, b, square[a*a+b*b])) 
    ) 
    .forEach(System.out::println); 

Notez que c'est un des quelques cas, où unimbriquéserait une alternative à flatMap:

int[] square=new int[20001]; 
IntStream.rangeClosed(1, 141).forEach(i -> square[i*i]=i); 
IntStream.rangeClosed(1, 100) 
    .forEach(a -> IntStream.rangeClosed(a, 100) 
      .filter(b -> square[a*a+b*b]!=0) 
      .forEach(b -> System.out.printf("[%d %d %d]%n", a, b, square[a*a+b*b])) 
    ); 
+1

1+ pour simplement prendre le temps d'analyser ce code. – Eugene

+0

@Holger, merci beaucoup pour les commentaires. J'apprécie votre perspicacité. Je ne suis pas familier avec les points de repère, alors je vais devoir me différencier à ce sujet. Je me demande si votre distribution de double à int dans la méthode filter() est plus intensive que la comparaison% 1, et votre appel map() final avec String.format(), alors que c'est un bon moyen de le faire, n'est pas meilleur que d'utiliser ma méthode avec des valeurs de tableau. En fait, l'appel de map() est une étape supplémentaire.Cela dit, j'apprécie toujours votre approche. –

+0

Bien sûr, un casting de 'double' à' int' est * beaucoup * moins cher qu'une opération modulo, mais vous pouvez toujours espérer que l'optimiseur de la JVM est capable de convertir votre '...% 1' en quelque chose de plus efficace, peut-être exactement à un casting à 'int'. Je ne vois pas, comment 'map' à' String' est une "étape supplémentaire", par rapport à votre code d'origine ayant un 'map' à un tableau * et * la conversion de ce tableau en un' String'. Cependant, le dernier exemple vient sans aucune étape de cartographie ... – Holger