web-dev-qa-db-fra.com

Le moyen le plus efficace de trouver le plus petit des 3 nombres Java?

J'ai un algorithme écrit en Java que je voudrais rendre plus efficace. Une partie de ce que je pense pourrait être plus efficace consiste à trouver le plus petit des 3 chiffres. Actuellement, j'utilise le Math.min méthode comme ci-dessous:

double smallest = Math.min(a, Math.min(b, c));

Est-ce efficace? Serait-il plus efficace de remplacer par if si des énoncés comme ci-dessous:

double smallest;
if (a <= b && a <= c) {
    smallest = a;
} else if (b <= c && b <= a) {
    smallest = b;
} else {
    smallest = c;
}

Ou si un autre moyen est plus efficace

Je me demande si cela vaut la peine de changer ce que j'utilise actuellement?

Toute augmentation de vitesse serait grandement utile

37
Chris

Non, c'est sérieux pas Cela vaut la peine de changer. Le type d’améliorations que vous obtiendrez en manipulant des micro-optimisations comme celle-ci ne vaudra pas la peine. Même le coût d’appel de méthode sera supprimé si la fonction min est appelée suffisamment.

Si vous rencontrez un problème avec votre algorithme, votre meilleur choix est d’examiner les macro-optimisations (éléments globaux tels que la sélection ou le réglage d’un algorithme). Vous obtiendrez généralement beaucoup de meilleures améliorations de performances. Là.

Et votre commentaire selon lequel enlever Math.pow _ a donné des améliorations peut être correct, mais c’est parce que c’est une opération relativement coûteuse. Math.min ne sera même pas proche de cela en termes de coût.

22
paxdiablo

Pour de nombreuses méthodes de type utilitaire, les bibliothèques communes Apache ont des implémentations solides que vous pouvez exploiter ou obtenir des informations supplémentaires. Dans ce cas, il existe une méthode permettant de trouver le plus petit des trois doublons disponible dans org.Apache.commons.lang.math.NumberUtils. Leur mise en œuvre est en réalité presque identique à votre pensée initiale:

public static double min(double a, double b, double c) {
    return Math.min(Math.min(a, b), c);
}
27
wpgreenway
double smallest = a;
if (smallest > b) smallest = b;
if (smallest > c) smallest = c;

Pas nécessairement plus rapide que votre code.

16
Joop Eggen

Permettez-moi de répéter tout d'abord ce que d'autres ont déjà dit, en citant l'article "Programmation structurée avec instructions" de Donald Knuth:

Nous devrions oublier les petites efficacités, disons environ 97% du temps: une optimisation prématurée est la racine de tout mal.

Pourtant, nous ne devrions pas laisser passer nos opportunités dans ces 3% critiques. Un bon programmeur ne se laissera pas aller à la complaisance par un tel raisonnement, il sera sage de regarder attentivement le code critique; mais seulement après que ce code a été identifié.

(souligné par moi)

Donc, si vous avez identifié qu'une opération apparemment triviale comme le calcul du minimum de trois nombres est le goulet d'étranglement réel (c'est-à-dire le "seuil critique de 3% ") dans votre application, vous pouvez alors l’optimiser.

Et dans ce cas, cela est réellement possible: La méthode Math#min(double,double) in Java a une sémantique très spéciale:

Retourne la plus petite des deux valeurs doubles. C'est-à-dire que le résultat est la valeur la plus proche de l'infini négatif. Si les arguments ont la même valeur, le résultat est la même valeur. Si l'une ou l'autre valeur est NaN, le résultat est NaN. Contrairement aux opérateurs de comparaison numérique, cette méthode considère que le zéro négatif est strictement inférieur au zéro positif. Si un argument est à zéro positif et l'autre à zéro négatif, le résultat est zéro négatif.

On peut regarder l'implémentation et voir que c'est en fait assez complexe:

public static double min(double a, double b) {
    if (a != a)
        return a;   // a is NaN
    if ((a == 0.0d) &&
        (b == 0.0d) &&
        (Double.doubleToRawLongBits(b) == negativeZeroDoubleBits)) {
        // Raw conversion ok since NaN can't map to -0.0.
        return b;
    }
    return (a <= b) ? a : b;
}

Maintenant, il peut être important de souligner que ce comportement est différent à partir d'une simple comparaison. Ceci peut facilement être examiné avec l'exemple suivant:

public class MinExample
{
    public static void main(String[] args)
    {
        test(0.0, 1.0);
        test(1.0, 0.0);
        test(-0.0, 0.0);
        test(Double.NaN, 1.0);
        test(1.0, Double.NaN);
    }

    private static void test(double a, double b)
    {
        double minA = Math.min(a, b);
        double minB = a < b ? a : b;
        System.out.println("a: "+a);
        System.out.println("b: "+b);
        System.out.println("minA "+minA);
        System.out.println("minB "+minB);
        if (Double.doubleToRawLongBits(minA) !=
            Double.doubleToRawLongBits(minB))
        {
            System.out.println(" -> Different results!");
        }
        System.out.println();
    }
}

Cependant: si le traitement de NaN et du zéro positif/négatif n’est pas pertinent pour votre application, vous pouvez remplacer la solution basée sur Math.min Par une solution basée sur une simple comparaison, et voir si cela fait une différence.

Bien entendu, cela dépendra de l'application. Voici un micro-repère artificiel simple ( à prendre avec un grain de sel! )

import Java.util.Random;

public class MinPerformance
{
    public static void main(String[] args)
    {
        bench();
    }

    private static void bench()
    {
        int runs = 1000;
        for (int size=10000; size<=100000; size+=10000)
        {
            Random random = new Random(0);
            double data[] = new double[size];
            for (int i=0; i<size; i++)
            {
                data[i] = random.nextDouble();
            }
            benchA(data, runs);
            benchB(data, runs);
        }
    }

    private static void benchA(double data[], int runs)
    {
        long before = System.nanoTime();
        double sum = 0;
        for (int r=0; r<runs; r++)
        {
            for (int i=0; i<data.length-3; i++)
            {
                sum += minA(data[i], data[i+1], data[i+2]);
            }
        }
        long after = System.nanoTime();
        System.out.println("A: length "+data.length+", time "+(after-before)/1e6+", result "+sum);
    }

    private static void benchB(double data[], int runs)
    {
        long before = System.nanoTime();
        double sum = 0;
        for (int r=0; r<runs; r++)
        {
            for (int i=0; i<data.length-3; i++)
            {
                sum += minB(data[i], data[i+1], data[i+2]);
            }
        }
        long after = System.nanoTime();
        System.out.println("B: length "+data.length+", time "+(after-before)/1e6+", result "+sum);
    }

    private static double minA(double a, double b, double c)
    {
        return Math.min(a, Math.min(b, c));
    }

    private static double minB(double a, double b, double c)
    {
        if (a < b)
        {
            if (a < c)
            {
                return a;
            }
            return c;
        }
        if (b < c)
        {
            return b;
        }
        return c;
    }
}

(Avertissement: Microbenchmarking in Java est un art, et pour des résultats plus fiables, envisagez d'utiliser JMH - Calibre ).

L’exécution de cette opération avec JRE 1.8.0_31 peut donner lieu à quelque chose comme:

....
A: length 90000, time 545.929078, result 2.247805342620906E7
B: length 90000, time 441.999193, result 2.247805342620906E7
A: length 100000, time 608.046928, result 2.5032781001456387E7
B: length 100000, time 493.747898, result 2.5032781001456387E7

Ceci au moins suggère qu'il pourrait être possible d'extraire quelques pour cent ici (encore une fois, dans un exemple très artificiel).


En analysant cela plus en profondeur, en regardant la sortie de désassemblage des points chauds créée avec

Java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly MinPerformance

on peut voir les versions optimisées des deux méthodes, minA et minB.

Tout d’abord, le résultat de la méthode qui utilise Math.min:

Decoding compiled method 0x0000000002992310:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x000000001c010910} &apos;minA&apos; &apos;(DDD)D&apos; in &apos;MinPerformance&apos;
  # parm0:    xmm0:xmm0   = double
  # parm1:    xmm1:xmm1   = double
  # parm2:    xmm2:xmm2   = double
  #           [sp+0x60]  (sp of caller)
  0x0000000002992480: mov    %eax,-0x6000(%rsp)
  0x0000000002992487: Push   %rbp
  0x0000000002992488: sub    $0x50,%rsp
  0x000000000299248c: movabs $0x1c010cd0,%rsi
  0x0000000002992496: mov    0x8(%rsi),%edi
  0x0000000002992499: add    $0x8,%edi
  0x000000000299249c: mov    %edi,0x8(%rsi)
  0x000000000299249f: movabs $0x1c010908,%rsi   ; {metadata({method} {0x000000001c010910} &apos;minA&apos; &apos;(DDD)D&apos; in &apos;MinPerformance&apos;)}
  0x00000000029924a9: and    $0x3ff8,%edi
  0x00000000029924af: cmp    $0x0,%edi
  0x00000000029924b2: je     0x00000000029924e8  ;*dload_0
                        ; - MinPerformance::minA@0 (line 58)

  0x00000000029924b8: vmovsd %xmm0,0x38(%rsp)
  0x00000000029924be: vmovapd %xmm1,%xmm0
  0x00000000029924c2: vmovapd %xmm2,%xmm1       ;*invokestatic min
                        ; - MinPerformance::minA@4 (line 58)

  0x00000000029924c6: nop
  0x00000000029924c7: callq  0x00000000028c6360  ; OopMap{off=76}
                        ;*invokestatic min
                        ; - MinPerformance::minA@4 (line 58)
                        ;   {static_call}
  0x00000000029924cc: vmovapd %xmm0,%xmm1       ;*invokestatic min
                        ; - MinPerformance::minA@4 (line 58)

  0x00000000029924d0: vmovsd 0x38(%rsp),%xmm0   ;*invokestatic min
                        ; - MinPerformance::minA@7 (line 58)

  0x00000000029924d6: nop
  0x00000000029924d7: callq  0x00000000028c6360  ; OopMap{off=92}
                        ;*invokestatic min
                        ; - MinPerformance::minA@7 (line 58)
                        ;   {static_call}
  0x00000000029924dc: add    $0x50,%rsp
  0x00000000029924e0: pop    %rbp
  0x00000000029924e1: test   %eax,-0x27623e7(%rip)        # 0x0000000000230100
                        ;   {poll_return}
  0x00000000029924e7: retq
  0x00000000029924e8: mov    %rsi,0x8(%rsp)
  0x00000000029924ed: movq   $0xffffffffffffffff,(%rsp)
  0x00000000029924f5: callq  0x000000000297e260  ; OopMap{off=122}
                        ;*synchronization entry
                        ; - MinPerformance::minA@-1 (line 58)
                        ;   {runtime_call}
  0x00000000029924fa: jmp    0x00000000029924b8
  0x00000000029924fc: nop
  0x00000000029924fd: nop
  0x00000000029924fe: mov    0x298(%r15),%rax
  0x0000000002992505: movabs $0x0,%r10
  0x000000000299250f: mov    %r10,0x298(%r15)
  0x0000000002992516: movabs $0x0,%r10
  0x0000000002992520: mov    %r10,0x2a0(%r15)
  0x0000000002992527: add    $0x50,%rsp
  0x000000000299252b: pop    %rbp
  0x000000000299252c: jmpq   0x00000000028ec620  ; {runtime_call}
  0x0000000002992531: hlt
  0x0000000002992532: hlt
  0x0000000002992533: hlt
  0x0000000002992534: hlt
  0x0000000002992535: hlt
  0x0000000002992536: hlt
  0x0000000002992537: hlt
  0x0000000002992538: hlt
  0x0000000002992539: hlt
  0x000000000299253a: hlt
  0x000000000299253b: hlt
  0x000000000299253c: hlt
  0x000000000299253d: hlt
  0x000000000299253e: hlt
  0x000000000299253f: hlt
[Stub Code]
  0x0000000002992540: nop                       ;   {no_reloc}
  0x0000000002992541: nop
  0x0000000002992542: nop
  0x0000000002992543: nop
  0x0000000002992544: nop
  0x0000000002992545: movabs $0x0,%rbx          ; {static_stub}
  0x000000000299254f: jmpq   0x000000000299254f  ; {runtime_call}
  0x0000000002992554: nop
  0x0000000002992555: movabs $0x0,%rbx          ; {static_stub}
  0x000000000299255f: jmpq   0x000000000299255f  ; {runtime_call}
[Exception Handler]
  0x0000000002992564: callq  0x000000000297b9e0  ; {runtime_call}
  0x0000000002992569: mov    %rsp,-0x28(%rsp)
  0x000000000299256e: sub    $0x80,%rsp
  0x0000000002992575: mov    %rax,0x78(%rsp)
  0x000000000299257a: mov    %rcx,0x70(%rsp)
  0x000000000299257f: mov    %rdx,0x68(%rsp)
  0x0000000002992584: mov    %rbx,0x60(%rsp)
  0x0000000002992589: mov    %rbp,0x50(%rsp)
  0x000000000299258e: mov    %rsi,0x48(%rsp)
  0x0000000002992593: mov    %rdi,0x40(%rsp)
  0x0000000002992598: mov    %r8,0x38(%rsp)
  0x000000000299259d: mov    %r9,0x30(%rsp)
  0x00000000029925a2: mov    %r10,0x28(%rsp)
  0x00000000029925a7: mov    %r11,0x20(%rsp)
  0x00000000029925ac: mov    %r12,0x18(%rsp)
  0x00000000029925b1: mov    %r13,0x10(%rsp)
  0x00000000029925b6: mov    %r14,0x8(%rsp)
  0x00000000029925bb: mov    %r15,(%rsp)
  0x00000000029925bf: movabs $0x515db148,%rcx   ; {external_Word}
  0x00000000029925c9: movabs $0x2992569,%rdx    ; {internal_Word}
  0x00000000029925d3: mov    %rsp,%r8
  0x00000000029925d6: and    $0xfffffffffffffff0,%rsp
  0x00000000029925da: callq  0x00000000512a9020  ; {runtime_call}
  0x00000000029925df: hlt
[Deopt Handler Code]
  0x00000000029925e0: movabs $0x29925e0,%r10    ; {section_Word}
  0x00000000029925ea: Push   %r10
  0x00000000029925ec: jmpq   0x00000000028c7340  ; {runtime_call}
  0x00000000029925f1: hlt
  0x00000000029925f2: hlt
  0x00000000029925f3: hlt
  0x00000000029925f4: hlt
  0x00000000029925f5: hlt
  0x00000000029925f6: hlt
  0x00000000029925f7: hlt

On peut voir que le traitement de cas spéciaux demande un certain effort - comparé au résultat qui utilise des comparaisons simples, ce qui est plutôt simple:

Decoding compiled method 0x0000000002998790:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x000000001c0109c0} &apos;minB&apos; &apos;(DDD)D&apos; in &apos;MinPerformance&apos;
  # parm0:    xmm0:xmm0   = double
  # parm1:    xmm1:xmm1   = double
  # parm2:    xmm2:xmm2   = double
  #           [sp+0x20]  (sp of caller)
  0x00000000029988c0: sub    $0x18,%rsp
  0x00000000029988c7: mov    %rbp,0x10(%rsp) ;*synchronization entry
                        ; - MinPerformance::minB@-1 (line 63)

  0x00000000029988cc: vucomisd %xmm0,%xmm1
  0x00000000029988d0: ja     0x00000000029988ee  ;*ifge
                        ; - MinPerformance::minB@3 (line 63)

  0x00000000029988d2: vucomisd %xmm1,%xmm2
  0x00000000029988d6: ja     0x00000000029988de  ;*ifge
                        ; - MinPerformance::minB@22 (line 71)

  0x00000000029988d8: vmovapd %xmm2,%xmm0
  0x00000000029988dc: jmp    0x00000000029988e2
  0x00000000029988de: vmovapd %xmm1,%xmm0 ;*synchronization entry
                        ; - MinPerformance::minB@-1 (line 63)

  0x00000000029988e2: add    $0x10,%rsp
  0x00000000029988e6: pop    %rbp
  0x00000000029988e7: test   %eax,-0x27688ed(%rip)        # 0x0000000000230000
                        ;   {poll_return}
  0x00000000029988ed: retq
  0x00000000029988ee: vucomisd %xmm0,%xmm2
  0x00000000029988f2: ja     0x00000000029988e2  ;*ifge
                        ; - MinPerformance::minB@10 (line 65)

  0x00000000029988f4: vmovapd %xmm2,%xmm0
  0x00000000029988f8: jmp    0x00000000029988e2
  0x00000000029988fa: hlt
  0x00000000029988fb: hlt
  0x00000000029988fc: hlt
  0x00000000029988fd: hlt
  0x00000000029988fe: hlt
  0x00000000029988ff: hlt
[Exception Handler]
[Stub Code]
  0x0000000002998900: jmpq   0x00000000028ec920  ;   {no_reloc}
[Deopt Handler Code]
  0x0000000002998905: callq  0x000000000299890a
  0x000000000299890a: subq   $0x5,(%rsp)
  0x000000000299890f: jmpq   0x00000000028c7340  ; {runtime_call}
  0x0000000002998914: hlt
  0x0000000002998915: hlt
  0x0000000002998916: hlt
  0x0000000002998917: hlt

Il est difficile de dire s'il existe ou non des cas où une telle optimisation fait vraiment une différence dans une application. Mais au moins, l’essentiel est:

  • La méthode Math#min(double,double) est pas identique à une simple comparaison, et le traitement des cas particuliers n'est pas gratuit
  • Il y a des cas où le traitement de cas particulier effectué par Math#min N'est pas nécessaire, puis une approche basée sur la comparaison peut être plus efficace
  • Comme déjà souligné dans d'autres réponses: Dans la plupart des cas, la différence de performance n'aura pas d'importance. Cependant, pour cet exemple particulier, on devrait probablement créer une méthode d’utilité min(double,double,double) de toute façon, pour plus de commodité et de lisibilité, et il serait alors facile de faire deux exécutions avec les différentes implémentations, et de voir si cela affecte réellement la performance.

(Note latérale: Les méthodes de type intégral, comme Math.min(int,int) en fait sont une simple comparaison - donc je m'attendrais à non différence pour ceux-ci).

8
Marco13

Vous pouvez utiliser l'opérateur ternaire comme suit:

smallest=(a<b)?((a<c)?a:c):((b<c)?b:c);

Ce qui prend seulement une tâche et deux comparaisons minimum.

Mais je pense que ces déclarations n’auraient aucun effet sur le temps d’exécution, votre logique initiale prendra le même temps que le mien et tous les autres.

4
Abhishek

Le code efficace de l'OP a un bug:

quand a == b et a (or b) < c, le code choisira c au lieu de a ou b.

3
newbie
double smallest;
if(a<b && a<c){
    smallest = a;
}else if(b<c && b<a){
    smallest = b;
}else{
    smallest = c;
}

peut être amélioré pour:

double smallest;
if(a<b && a<c){
smallest = a;
}else if(b<c){
    smallest = b;
}else{
    smallest = c;
}
2
Maximus Ali

Si vous appelez min () environ 1kk fois avec différents a, b, c, utilisez ma méthode:

Ici seulement deux comparaisons. Il n'y a aucun moyen de calculer plus vite: P

public static double min(double a, double b, double c) {
    if (a > b) {     //if true, min = b
        if (b > c) { //if true, min = c
            return c;
        } else {     //else min = b
            return b; 
        }
    }          //else min = a
    if (a > c) {  // if true, min=c
        return c;
    } else {
        return a;
    }
}
1
Alexander Cyberman

Tout semble aller, votre code ira bien, à moins que vous ne le fassiez dans une boucle serrée. Je considérerais aussi

double min;
min = (a<b) ? a : b;
min = (min<c) ? min : c;
1
QED

Math.min Utilise une simple comparaison pour faire son travail. Le seul avantage de ne pas utiliser Math.min est de sauvegarder les appels de fonctions supplémentaires, mais il s’agit d’une sauvegarde négligeable.

Si vous avez plus que trois nombres, avoir une méthode minimum pour un nombre quelconque de doubles pourrait être utile et ressembler à quelque chose comme:

public static double min(double ... numbers) {
    double min = numbers[0];
    for (int i=1 ; i<numbers.length ; i++) {
        min = (min <= numbers[i]) ? min : numbers[i];
    }
    return min;
}

Pour trois nombres, il s'agit de l'équivalent fonctionnel de Math.min(a, Math.min(b, c));, mais vous enregistrez une invocation de méthode.

1
Kris

Pour une efficacité optimale des caractères de code, je ne trouve rien de mieux que

smallest = a<b&&a<c?a:b<c?b:c;
1
Thomas Allenbaugh

Pour ceux qui trouveront ce sujet beaucoup plus tard:

Si vous n'avez que trois valeurs à comparer, il n'y a pas de différence significative. Mais si vous devez trouver min, par exemple, trente ou soixante valeurs, "min" pourrait être plus facile à lire dans le code l'année prochaine:

int smallest;

smallest = min(a1, a2);
smallest = min(smallest, a3);
smallest = min(smallest, a4);
...
smallest = min(smallest, a37);

Mais si vous pensez à la vitesse, peut-être un meilleur moyen serait-il de mettre les valeurs dans la liste, puis de trouver le minimum:

List<Integer> mySet = Arrays.asList(a1, a2, a3, ..., a37);

int smallest = Collections.min(mySet);

Accepteriez-vous?

1
Sinisa

J'utiliserais min/max (et ne vous inquiétez pas sinon) ... voici cependant une autre approche "à la main longue" qui peut être ou ne pas être plus facile à comprendre pour certaines personnes. (Je voudrais pas m'attendre à ce que ce soit plus rapide ou plus lent que le code de l'article.)

int smallest;
if (a < b) {
  if (a > c) {
    smallest = c;
  } else { // a <= c
    smallest = a;
  }
} else { // a >= b
  if (b > c) {
    smallest = c;
  } else { // b <= c
    smallest = b;
  }
}

Je viens de le jeter dans le mélange.

Notez qu'il ne s'agit que de la variante à effet secondaire de la réponse d'Abhishek.

0
user166390