Вчера я увидел вопрос, почему Math.pow(int,int) работает так медленно, но вопрос был плохо сформулирован и не требовал никаких исследований, поэтому он был быстро закрыт.

Я провел небольшое собственное испытание и обнаружил, что метод Math.pow действительно работает очень медленно по сравнению с моей собственной наивной реализацией (которая даже не является особенно эффективной реализацией) при работе с целочисленными аргументами. Ниже приведен код, который я запустил, чтобы проверить это:

class PowerTest {

    public static double myPow(int base, int exponent) {
        if(base == 0) return 0;
        if(exponent == 0) return 1;
        int absExponent = (exponent < 0)? exponent * -1 : exponent;
        double result = base;
        for(int i = 1; i < absExponent; i++) {
            result *= base;
        }
        if(exponent < 1) result = 1 / result;
        return result;
    }

    public static void main(String args[]) {
        long startTime, endTime;

        startTime = System.nanoTime();
        for(int i = 0; i < 5000000; i++) {
            Math.pow(2,2);
        }
        endTime = System.nanoTime();
        System.out.printf("Math.pow took %d milliseconds.\n", (endTime - startTime) / 1000000);

        startTime = System.nanoTime();
        for(int i = 0; i < 5000000; i++) {
            myPow(2,2);
        }
        endTime = System.nanoTime();
        System.out.printf("myPow took %d milliseconds.\n", (endTime - startTime) / 1000000);
    }

}

На моем компьютере (linux на процессоре Intel x86_64) в выводе почти всегда сообщалось, что Math.pow занимает 10 мс, а myPow - 2 мс. Время от времени время от времени колебалось на миллисекунду, но в среднем Math.pow выполнялся примерно в 5 раз медленнее .

Я провел небольшое исследование и, согласно grepcode, Math.pow предлагает только метод с сигнатурой типа (double, double) и переносит это на StrictMath.pow, который является вызовом собственного метода.

Тот факт, что библиотека Math предлагает только функцию pow, которая имеет дело с двойниками, кажется, указывает на возможный ответ на этот вопрос. Очевидно, что алгоритм мощности, который должен обрабатывать возможность основания или экспоненты типа double, потребует больше времени для выполнения, чем мой алгоритм, который имеет дело только с целыми числами. Однако, в конце концов, все сводится к архитектурно-зависимому собственному коду (который почти всегда работает быстрее, чем байт-код JVM, возможно, C или сборка в моем случае). Похоже, что на этом уровне будет произведена оптимизация, чтобы проверить тип данных и, если возможно, запустить более простой алгоритм.

Учитывая эту информацию, почему собственный метод Math.pow постоянно работает намного медленнее, чем мой неоптимизированный и наивный метод myPow, когда заданы целочисленные аргументы?

7
Woodrow Barlow 6 Янв 2016 в 19:40

4 ответа

Лучший ответ

Как говорили другие, нельзя просто игнорировать использование double, поскольку арифметика с плавающей запятой почти наверняка будет медленнее. Однако это не единственная причина - если вы измените свою реализацию, чтобы использовать их, это все равно будет быстрее.

Это происходит по двум причинам: во-первых, 2^2 (показатель степени, а не xor) - это очень быстрое вычисление, поэтому ваш алгоритм подходит для этого - попробуйте использовать два значения из Random#nextInt (или nextDouble), и вы увидите, что Math#pow на самом деле намного быстрее.

Другая причина заключается в том, что вызов собственных методов имеет накладные расходы, что на самом деле имеет смысл, потому что 2^2 вычисляется так быстро, а вы вызываете Math#pow так много раз. См. Что замедляет вызовы JNI? для получения дополнительной информации.

9
Toby 7 Янв 2016 в 15:29

Math.pow(x, y) вероятно реализовано как exp(y log x). Это позволяет использовать дробные показатели степени и работает очень быстро.

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

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

Кстати, ваша версия интегрального типа могла бы быть быстрее. Изучите возведение в степень возведением в квадрат .

0
Bathsheba 6 Янв 2016 в 17:00

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

Простое умножение чисел часто выполняется быстрее, поскольку собственные вызовы в Java намного эффективнее.

Изменить: также стоит отметить, что математические функции используют двойные значения, что также может занять больше времени, чем использование целых чисел.

0
M Lizz 6 Янв 2016 в 16:46

Нет функции pow(int,int). Вы сравниваете яблоки с апельсинами, исходя из вашего упрощенного предположения, что числа с плавающей запятой можно игнорировать.

1
erickson 6 Янв 2016 в 16:43