Самый эффективный способ найти наименьшее из 3 чисел Java?

34

У меня есть алгоритм, написанный на Java, который я хотел бы сделать более эффективным. Часть, которую, я думаю, можно сделать более эффективной, - это найти наименьшее из 3 чисел. В настоящее время я использую метод Math.min как Math.min ниже:

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

Насколько это эффективно? Будет ли эффективнее заменить на операторы if, как показано ниже:

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

Или если какой-либо другой способ более эффективен

Мне интересно, стоит ли менять то, что я сейчас использую?

Любое увеличение скорости было бы очень полезно

  • 19
    Ожидание получения какого-либо значимого влияния на производительность от базовых числовых сравнений прыгает слишком далеко вниз по кроличьей норе.
  • 1
    @debracey это когда мой алгоритм работает уже почти 23 часа? :П
Показать ещё 7 комментариев
Теги:
performance
algorithm
min

16 ответов

22
Лучший ответ

Нет, это серьезно не стоит меняться. Такие улучшения, которые вы получите, когда играете с микро-оптимизациями, как это, не будут стоить того. Даже стоимость вызова метода будет удалена, если функция min называется достаточно.

Если у вас возникла проблема с вашим алгоритмом, лучше всего посмотреть на макрооптимизации (например, выбор или настройка алгоритма) - вы, как правило, получите гораздо лучшие улучшения производительности.

И ваш комментарий, что удаление Math.pow дает улучшения, может быть правильным, но это потому, что это относительно дорогостоящая операция. Math.min не будет даже близко к цене с точки зрения стоимости.

  • 0
    Спасибо, очень полезно. Я экспериментирую с разными оптимизациями, но мне также нужно собрать результаты для этого базового случая, чтобы можно было сравнить :(
  • 0
    Когда вам приходится звонить по трехсторонней минуте сотни тысяч раз, это добавляет
Показать ещё 1 комментарий
23

Для многих методов типа утилит библиотеки apache-сообществ имеют простую реализацию, с помощью которой вы можете либо использовать, либо получить дополнительную информацию. В этом случае существует метод поиска наименьшего из трех двухлокальных номеров, доступных в org.apache.commons.lang.math.NumberUtils. Их реализация на самом деле почти идентична вашей первоначальной мысли:

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

Не обязательно быстрее, чем ваш код.

8

Позвольте мне вначале повторить то, что уже сказали другие, цитируя статью "Структурированное программирование с переходом к заявлениям" Дональда Кнута:

Мы должны забыть о небольшой эффективности, скажем, около 97% времени: преждевременная оптимизация - корень всего зла.

Однако мы не должны упускать наши возможности в этих критических 3%. Хороший программист не уклонится от самоуспокоения такими рассуждениями, он будет мудрым взглянуть на критический код; но только после того, как этот код был идентифицирован.

(выделение мной)

Итак, если вы определили, что кажущаяся тривиальная операция, такая как вычисление минимума трех чисел , является фактическим узким местом (то есть "критическим 3%" ) в вашем приложении, тогда вы может рассмотреть возможность его оптимизации.

И в этом случае это действительно возможно: метод Math#min(double,double) в Java имеет очень специальную семантику:

Возвращает меньшее из двух двойных значений. То есть результат - это значение, которое ближе к отрицательной бесконечности. Если аргументы имеют одинаковое значение, результат будет таким же значением. Если любое значение NaN, то результатом является NaN. В отличие от операторов численного сравнения этот метод считает отрицательный ноль строго меньше положительного нуля. Если один аргумент положительный ноль, а другой отрицательный ноль, результат отрицательный 0.

Можно взглянуть на реализацию и увидеть, что она на самом деле довольно сложная:

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;
}

Теперь, возможно, важно указать, что это поведение отличается от простого сравнения. Это можно легко рассмотреть в следующем примере:

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();
    }
}

Однако: если обработка NaN и положительного/отрицательного нуля не имеет отношения к вашему приложению, вы можете заменить решение, основанное на Math.min, на решение, основанное на простом сравнении, и посмотреть, это имеет значение.

Это, конечно, будет зависящим от приложения. Вот простой, искусственный микробенчмарк (, который нужно взять с зерном соли!)

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;
    }
}

(Отказ от ответственности: Microbenchmarking в Java - это искусство, и для получения более надежных результатов следует рассмотреть возможность использования JMH или Caliper).

Запуск этого с помощью JRE 1.8.0_31 может привести к чему-то вроде

....
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

Это, по крайней мере, предполагает, что здесь можно было бы выжать несколько процентов (опять же, в очень искусственном примере).


Анализируя это далее, просмотрев вывод разборки горячих точек, созданный с помощью

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

можно увидеть оптимизированные версии обоих методов, minA и minB.

Во-первых, вывод для метода, который использует 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

Можно видеть, что обработка особых случаев требует определенных усилий - по сравнению с выходом, который использует простые сравнения, что довольно просто:

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

Можно ли сказать, есть ли случаи, когда такая оптимизация действительно влияет на приложение, трудно. Но, по крайней мере, нижняя строка:

  • Метод Math#min(double,double) не аналогичен простому сравнению, а обработка особых случаев не предоставляется бесплатно
  • Есть случаи, когда обработка особого случая, выполняемая с помощью Math#min, не нужна, и тогда более эффективный подход может быть более эффективным
  • Как уже указывалось в других ответах: в большинстве случаев разница в производительности не имеет значения. Однако для этого конкретного примера следует, во всяком случае, создать способ утилиты min(double,double,double) для лучшего удобства и удобочитаемости, а затем было бы легко выполнить два прогона с различными реализациями и посмотреть, действительно ли это влияет на производительность.

(Замечание: методы интегрального типа, такие как Math.min(int,int), на самом деле являются простым сравнением, поэтому я бы ожидал разницы нет).

4

Вы можете использовать тройной оператор следующим образом:

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

Для этого требуется только одно назначение и минимум два сравнения.

Но я думаю, что эти утверждения не повлияли бы на время выполнения, ваша первоначальная логика займет то же самое время, что и мое и все остальные.

  • 2
    Обратите внимание, что это возвращает наибольшее число, а не наименьшее.
  • 0
    @Sietse Это не вернется. Он назначает наименьшее из a , b и c smallest .
2

В эффективном коде OP есть ошибка:

когда a == b и a (or b) < c, код выберет c вместо a или b.

  • 0
    Хотя это хорошая находка, о которой никто (явно) не упомянул ранее, это не отвечает на вопрос: голосование только для того, чтобы дать вам возможность превратить это в комментарий раньше. Рассмотрите возможность редактирования вопроса !
2
double smallest;
if(a<b && a<c){
    smallest = a;
}else if(b<c && b<a){
    smallest = b;
}else{
    smallest = c;
}

можно улучшить, чтобы:

double smallest;
if(a<b && a<c){
smallest = a;
}else if(b<c){
    smallest = b;
}else{
    smallest = c;
}
  • 1
    На мой взгляд, это один из немногих реальных ответов. Это очень небольшое изменение в коде OP, что повышает его эффективность. Объяснение того, как эта проблема является микрооптимизацией, НЕ является ответом. Некоторые люди могут иметь свою горячую точку именно в такой функции.
1

Для эффективности чистых символов кода я не могу найти ничего лучше, чем

smallest = a<b&&a<c?a:b<c?b:c;
  • 0
    Возможно, вы захотите поиграть в гольф.
1

Если вы вызовете min() около 1kk раз с разными a, b, c, используйте мой метод:

Здесь только два сравнения. Невозможно ускорить вычисление: 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

Math.min использует простое сравнение для выполнения своей задачи. Единственное преимущество не использовать Math.min - это сохранить дополнительные вызовы функций, но это незначительная экономия.

Если у вас есть не более трех чисел, метод minimum для любого числа double может быть ценным и выглядел бы примерно так:

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;
}

Для трех чисел это функциональный эквивалент Math.min(a, Math.min(b, c));, но вы сохраняете один вызов метода.

  • 0
    (Поздний) комментарий: Math.min использует только простое сравнение для целочисленных типов данных. Но для double и float , он также заботится о специальных случаях, таких как NaN .
1

Все выглядит нормально, ваш код будет в порядке, если вы не делаете это в плотном цикле. Я также рассмотрел бы

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

попробуйте для цикла. Я думаю, что это быстрее и удобнее при обновлении кода, например, если вы хотите добавить другое целое число для сравнения.

Спасибо

int lowestnum;

for(Integer i = 0; i < x; i++){

    if(lowestnum == null || x < lowestnum){
            lowestnum = x;

        }               
    }
0

Для тех, кто находит эту тему намного позже:

Если у вас есть только три значения для сравнения, то нет существенной разницы. Но если вам нужно найти минимум, скажем, тридцати или шестидесяти значений, "min" может быть проще для любого прочитать код в следующем году:

int smallest;

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

Но если вы думаете о скорости, возможно, лучший способ - поместить значения в список, а затем найти min из этого:

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

int smallest = Collections.min(mySet);

Согласны ли вы?

  • 0
    Не я, например. Хотя мне трудно представить 37 значений и «необходимость» минимума, я не вижу, как Collections.min() поможет speed процесс. Удача Java в «неявном параллелизме / параллелизме» - это stream .
  • 0
    Благодарю. А для 37 значений, например, это может понадобиться, когда вы выбираете диапазон графика, который вы рисуете для группы значений Y. Вам понадобится минимальное и максимальное значения, чтобы определить диапазон Y графика.
0

Я бы использовал min/max (и не беспокоиться иначе)... однако, здесь есть еще один подход "длинной руки", который может или не может быть проще для некоторых людей понять. (Я бы не ожидал, что это будет быстрее или медленнее, чем код в сообщении.)

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;
  }
}

Просто выбросьте его в микс.

Обратите внимание, что это только побочный вариант ответа Абхишека.

-4

Напишите метод minimum3, который возвращает наименьшее из трех чисел с плавающей запятой. Используйте метод Math.min для реализации minimum3. Включите этот метод в приложение, которое считывает три значения от пользователя, определяет наименьшее значение и отображает результат.

  • 1
    Вопрос заключался в том, какой из двух представленных способов более эффективен (и он знает, как это сделать), или есть ли другой способ лучше, чем два представленных, а не как это сделать. это комментарий больше, чем ответ.
-5

Просто используйте эту математическую функцию

System.out.println(Math.min(a,b,c));

Вы получите ответ в одной строке.

  • 0
    Вопрос был об эффективности. Так что в ответе вы должны дать некоторое объяснение, почему ваш код работает более эффективно. Код Shortes и быстрый код не совпадают.
  • 3
    Этот ответ фигня. В Java Math.min функции с Math.min аргументами Math.min . Прочитайте Javadocs.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню