Java Math.min / максимальная производительность

РЕДАКТИРОВАТЬ: maaartinus дал ответ, который я искал, и данные tmyklebu по проблеме очень помогли, так что спасибо обоим! :)

Я немного читал о том, что у HotSpot есть «встроенные функции», которые внедряются в код, особенно для стандартных математических библиотек Java ( отсюда )

Поэтому я решил попробовать, чтобы увидеть, насколько сильно HotSpot может сделать против прямого сравнения (тем более, что я слышал, что min / max может компилироваться в asms без ветвей).

    public static final int max ( final int a, final int b )
{
    if ( a > b )
    {
        return a;
    }

    return b;
}

Это моя реализация. Из другого вопроса SO, который я читал, что использование тернарного оператора использует дополнительный регистр, я не нашел существенных различий между выполнением блока if и использованием троичного оператора (т. Е. Return (a> b)? A: b).

Выделив массив Int 8Mb (т.е. 2 миллиона значений) и сделав его рандомизированным, я провожу следующий тест:

try ( final Benchmark bench = new Benchmark( "millis to max" ) )
    {
        int max = Integer.MIN_VALUE;

        for ( int i = 0; i < array.length; ++i )
        {
            max = OpsMath.max( max, array[i] );
            // max = Math.max( max, array[i] );
        }
    }

Я использую объект Benchmark в блоке try-with-resources , Когда он завершается, он вызывает close () для объекта и печатает время, необходимое блоку для завершения. Тесты выполняются отдельно, комментируя входящие / исходящие вызовы max в приведенном выше коде.

'max' добавляется в список за пределами блока эталонного теста и печатается позже, поэтому во избежание оптимизации всего блока JVM.

Массив рандомизируется при каждом запуске теста.

Выполнение теста 6 раз дает следующие результаты:

Стандарт Java Math:

millis to max 9.242167 
millis to max 2.1566199999999998
millis to max 2.046396 
millis to max 2.048616  
millis to max 2.035761
millis to max 2.001044 

Столь же стабильно после первого запуска, и повторный запуск тестов дает аналогичные результаты.

OpsMath:

millis to max 8.65418 
millis to max 1.161559  
millis to max 0.955851 
millis to max 0.946642 
millis to max 0.994543 
millis to max 0.9469069999999999 

Опять же, очень стабильные результаты после первого запуска.

Вопрос в следующем: Почему? Это довольно большая разница. И я понятия не имею, почему. Даже если я реализую свой метод max () точно , как Math.max () (т. Е. Return (a> = b)? A: b) Я все равно получу лучшие результаты! Это не имеет смысла.

Характеристики:

Процессор: Intel i5 2500, 3,3 ГГц. Версия Java: JDK 8 (общедоступная версия от 18 марта), x64. Debian Jessie (тестовый выпуск) x64.

Мне еще предстоит попробовать 32-битную JVM.

РЕДАКТИРОВАТЬ: Автономный тест по запросу. Добавлена ​​строка, чтобы заставить JVM предварительно загружать классы Math и OpsMath. Это исключает 18 мс стоимость первой итерации для теста OpsMath.

// Constant nano to millis.
final double TO_MILLIS = 1.0d / 1000000.0d;
// 8Mb alloc.
final int[] array = new int[(8*1024*1024)/4];
// Result and time array.
final ArrayList results = new ArrayList<>();
final ArrayList times = new ArrayList<>();
// Number of tests.
final int itcount = 6;
// Call both Math and OpsMath method so JVM initializes the classes.
System.out.println("initialize classes " + 
OpsMath.max( Math.max( 20.0f, array.length ), array.length / 2.0f ));

final Random r = new Random();
for ( int it = 0; it < itcount; ++it )
{
    int max = Integer.MIN_VALUE;

    // Randomize the array.
    for ( int i = 0; i < array.length; ++i )
    {
        array[i] = r.nextInt();
    }

    final long start = System.nanoTime();
    for ( int i = 0; i < array.length; ++i )
    {
        max = Math.max( array[i], max );
            // OpsMath.max() method implemented as described.
        // max = OpsMath.max( array[i], max );
    }
    // Calc time.
    final double end = (System.nanoTime() - start);
    // Store results.
    times.add( Double.valueOf( end ) );
    results.add( Integer.valueOf(  max ) );
}
// Print everything.
for ( int i = 0; i < itcount; ++i )
{
    System.out.println( "IT" + i + " result: " + results.get( i ) );
    System.out.println( "IT" + i + " millis: " + times.get( i ) * TO_MILLIS );
}

Результат Java Math.max:

IT0 result: 2147477409
IT0 millis: 9.636998
IT1 result: 2147483098
IT1 millis: 1.901314
IT2 result: 2147482877
IT2 millis: 2.095551
IT3 result: 2147483286
IT3 millis: 1.9232859999999998
IT4 result: 2147482828
IT4 millis: 1.9455179999999999
IT5 result: 2147482475
IT5 millis: 1.882047

Результат OpsMath.max:

IT0 result: 2147482689
IT0 millis: 9.003616
IT1 result: 2147483480
IT1 millis: 0.882421
IT2 result: 2147483186
IT2 millis: 1.079143
IT3 result: 2147478560
IT3 millis: 0.8861169999999999
IT4 result: 2147477851
IT4 millis: 0.916383
IT5 result: 2147481983
IT5 millis: 0.873984

Все тот же общий результат. Я попытался рандомизировать массив только один раз, и, повторяя тесты для того же массива, я в целом получаю более быстрые результаты, но с той же разницей в 2 раза между Java Math.max и OpsMath.max.

24
задан 18.05.2020, 08:29

1 ответ

Используя JDK 8:

java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)

В Ubuntu 13.10

я запустил следующее:

import java.util.Random;
import java.util.function.BiFunction;

public class MaxPerformance {
  private final BiFunction<Integer, Integer, Integer> max;
  private final int[] array;

  public MaxPerformance(BiFunction<Integer, Integer, Integer> max, int[] array) {
    this.max = max;
    this.array = array;
  }

  public double time() {
    long start = System.nanoTime();

    int m = Integer.MIN_VALUE;
    for (int i = 0; i < array.length; ++i) m = max.apply(m, array[i]);

    m = Integer.MIN_VALUE;
    for (int i = 0; i < array.length; ++i) m = max.apply(array[i], m);

    // total time over number of calls to max
    return ((double) (System.nanoTime() - start)) / (double) array.length / 2.0;
  }

  public double averageTime(int repeats) {
    double cumulativeTime = 0;
    for (int i = 0; i < repeats; i++)
      cumulativeTime += time();
    return (double) cumulativeTime / (double) repeats;
  }

  public static void main(String[] args) {
    int size = 1000000;
    Random random = new Random(123123123L);
    int[] array = new int[size];
    for (int i = 0; i < size; i++) array[i] = random.nextInt();

    double tMath = new MaxPerformance(Math::max, array).averageTime(100);
    double tAlt1 = new MaxPerformance(MaxPerformance::max1, array).averageTime(100);
    double tAlt2 = new MaxPerformance(MaxPerformance::max2, array).averageTime(100);

    System.out.println("Java Math: " + tMath);
    System.out.println("Alt 1:     " + tAlt1);
    System.out.println("Alt 2:     " + tAlt2);
  }

  public static int max1(final int a, final int b) {
    if (a >= b) return a;
    return b;
  }

  public static int max2(final int a, final int b) {
    return (a >= b) ? a : b; // same as JDK implementation
  }
}

И получил следующие результаты (средние наносекунды, взятые для каждый вызов max):

Java Math: 15.443555810000003
Alt 1:     14.968298919999997
Alt 2:     16.442204045

Таким образом, в долгосрочной перспективе кажется, что вторая реализация является самой быстрой, хотя и с относительно небольшим отрывом.

Чтобы провести немного более научный тест, имеет смысл вычислить максимум пар элементов, где каждый вызов не зависит от предыдущего. Это можно сделать, используя два рандомизированных массива вместо одного, как в этом тесте:

import java.util.Random;
import java.util.function.BiFunction;
public class MaxPerformance2 {
  private final BiFunction<Integer, Integer, Integer> max;
  private final int[] array1, array2;

  public MaxPerformance2(BiFunction<Integer, Integer, Integer> max, int[] array1, int[] array2) {
    this.max = max;
    this.array1 = array1;
    this.array2 = array2;
    if (array1.length != array2.length) throw new IllegalArgumentException();
  }

  public double time() {
    long start = System.nanoTime();

    int m = Integer.MIN_VALUE;
    for (int i = 0; i < array1.length; ++i) m = max.apply(array1[i], array2[i]);
    m += m; // to avoid optimizations!

    return ((double) (System.nanoTime() - start)) / (double) array1.length;
  }

  public double averageTime(int repeats) {
    // warm up rounds:
    double tmp = 0;
    for (int i = 0; i < 10; i++) tmp += time();
    tmp *= 2.0;

    double cumulativeTime = 0;
    for (int i = 0; i < repeats; i++)
        cumulativeTime += time();
    return cumulativeTime / (double) repeats;
  }

  public static void main(String[] args) {
    int size = 1000000;
    Random random = new Random(123123123L);
    int[] array1 = new int[size];
    int[] array2 = new int[size];
    for (int i = 0; i < size; i++) {
        array1[i] = random.nextInt();
        array2[i] = random.nextInt();
    }

    double tMath = new MaxPerformance2(Math::max, array1, array2).averageTime(100);
    double tAlt1 = new MaxPerformance2(MaxPerformance2::max1, array1, array2).averageTime(100);
    double tAlt2 = new MaxPerformance2(MaxPerformance2::max2, array1, array2).averageTime(100);

    System.out.println("Java Math: " + tMath);
    System.out.println("Alt 1:     " + tAlt1);
    System.out.println("Alt 2:     " + tAlt2);
  }

  public static int max1(final int a, final int b) {
    if (a >= b) return a;
    return b;
  }

  public static int max2(final int a, final int b) {
    return (a >= b) ? a : b; // same as JDK implementation
  }
}

Что и дало мне:

Java Math: 15.346468170000005
Alt 1:     16.378737519999998
Alt 2:     20.506475350000006

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

1114 Кто-то упомянул суппорт. Что ж, если вы прочитаете вики , первое, что они скажут о микро-бенчмаркинге, это , а не , чтобы это сделать: это потому, что трудно получить точные результаты в целом. Я думаю, что это яркий пример этого.

1
ответ дан 18.05.2020, 08:30
  • 1
    Ошеломите 5 хороших ответов в течение 12 минут после регистрации вопроса! Все заслуживают зеленой проверки, но я могу только принять тот так I' m взятие того с самой ранней меткой времени. – RobertL 19.02.2020, 00:42
  • 2
    Да я искал его и пытался тестировать с набором раундов прогрева (звонит в эти time() функция это don' t добавляют к совокупной сумме). Я получаю то же. – Giovanni Botta 18.05.2020, 08:30
  • 3
    (1) That' s положительная сторона. Я haven' t. (2) я попробовал различную версию, нагревают раунды, и я получаю те же результаты. Нижняя строка - то, что условия испытания являются тем же для всех функций. Я думал, что это могло бы дать больше " sterile" среда. Однако я мог бы быть ужасно неправым! Это - боль микросравнительного тестирования. – Giovanni Botta 18.05.2020, 08:30
  • 4
    Намерение было корректно: синхронизация должна быть , составил в среднем к smoot потенциал " random" скачки. Однако существует (по крайней мере) 2 проблемы: 1. Вы проигнорировали результат (m) в первом тесте - таким образом, функции могли , были оптимизированы далеко (Вы обратились к этому со своим РЕДАКТИРОВАНИЕМ). И 2: Запущение тестов только однажды в предопределенном порядке могло представить искажения, потому что на первом показе, BiFunction является мономорфным (существует только одна реализация), во втором, это биморфно, и третий является полиморфным (см. javaspecialists.eu/archive/Issue158.html ). – Marco13 18.05.2020, 08:31
  • 5
    (1) Имейте Вас, осмотрел ассемблерный код для проверки it' s встраивание цели эти BiFunction вызов в обоих случаях? (2) Вы уверенный Вы - not' t синхронизация интерпретируемого или код OSR? I' m почти уверенный Вы. – tmyklebu 18.05.2020, 08:31
  • 6
    OSR = на замене стека: azulsystems.com/blog/cliff/… – Fortega 18.05.2020, 08:32

Теги

Похожие вопросы