Короткий ответ:
Специализация pow(x, n)
в n
виде натурального числа часто бывает полезна для измерения времени . Но универсальный стандарт стандартной библиотеки по- pow()
прежнему хорошо работает (что удивительно! ) Для этой цели, и абсолютно важно включать как можно меньше в стандартную библиотеку C, чтобы ее можно было сделать максимально переносимой и максимально простой в реализации. С другой стороны, это вовсе не мешает ему находиться в стандартной библиотеке C ++ или STL, которые, я почти уверен, никто не планирует использовать в какой-то встроенной платформе.
А теперь длинный ответ.
pow(x, n)
во многих случаях можно сделать намного быстрее, если n
использовать натуральное число. Мне приходилось использовать мою собственную реализацию этой функции почти для каждой программы, которую я пишу (но я пишу много математических программ на C). Специализированная операция может быть выполнена O(log(n))
вовремя, но когда n
она небольшая, более простая линейная версия может быть быстрее. Вот реализации обоих:
// Computes x^n, where n is a natural number.
double pown(double x, unsigned n)
{
double y = 1;
// n = 2*d + r. x^n = (x^2)^d * x^r.
unsigned d = n >> 1;
unsigned r = n & 1;
double x_2_d = d == 0? 1 : pown(x*x, d);
double x_r = r == 0? 1 : x;
return x_2_d*x_r;
}
// The linear implementation.
double pown_l(double x, unsigned n)
{
double y = 1;
for (unsigned i = 0; i < n; i++)
y *= x;
return y;
}
(Я оставил, x
и возвращаемое значение удваивается, потому что результат pow(double x, unsigned n)
будет вписываться в удвоение примерно так часто, как pow(double, double)
будет.)
(Да, pown
это рекурсивно, но сломать стек абсолютно невозможно, так как максимальный размер стека будет примерно равным log_2(n)
и n
является целым числом. Если n
это 64-битное целое число, это дает максимальный размер стека около 64. Никакое оборудование не имеет таких экстремальных значений. ограничения памяти, за исключением некоторых хитрых PIC с аппаратными стеками, которые обрабатывают от 3 до 8 вызовов функций.)
Что касается производительности, вы будете удивлены тем, на что pow(double, double)
способны садовые сорта . Я протестировал сто миллионов итераций на моем 5-летнем IBM Thinkpad с x
номером итерации, n
равным 10. В этом сценарии pown_l
победил. glibc pow()
занял 12,0 пользовательских секунды, pown
7,4 пользовательских секунды и pown_l
всего 6,5 пользовательских секунды. Так что это не слишком удивительно. Этого мы более или менее ожидали.
Затем я позволил x
быть постоянным (я установил его на 2,5) и n
сделал цикл от 0 до 19 сто миллионов раз. На этот раз совершенно неожиданно pow
победила glibc , причем безоговорочно! Потребовалось всего 2,0 пользовательских секунды. My pown
занял 9,6 секунды и pown_l
12,2 секунды. Что здесь случилось? Я сделал еще один тест, чтобы узнать.
Я сделал то же самое, только с x
равным миллиону. На этот раз pown
выиграл с результатом 9,6 с. pown_l
занял 12,2 секунды, а glibc pow - 16,3 секунды. Теперь ясно! glibc pow
работает лучше трех при x
низком уровне и хуже всего при x
высоком. Когда x
высокий, pown_l
лучше всего работает, когда n
низкий, и pown
лучше всего работает, когда x
высокий.
Итак, вот три разных алгоритма, каждый из которых может работать лучше других при правильных обстоятельствах. Таким образом, в конечном счете, что использовать , скорее всего , зависит от того, как вы планируете использовать pow
, но используя правильную версию это стоит, и иметь все версии хорошо. Фактически, вы даже можете автоматизировать выбор алгоритма с помощью такой функции:
double pown_auto(double x, unsigned n, double x_expected, unsigned n_expected) {
if (x_expected < x_threshold)
return pow(x, n);
if (n_expected < n_threshold)
return pown_l(x, n);
return pown(x, n);
}
Пока x_expected
и n_expected
являются константами, определяемыми во время компиляции, наряду, возможно, с некоторыми другими предостережениями, оптимизирующий компилятор, стоящий его соли, автоматически удалит весь pown_auto
вызов функции и заменит его соответствующим выбором из трех алгоритмов. (Теперь, если вы действительно собираетесь попробовать это использовать , вам, вероятно, придется немного поиграть с этим, потому что я точно не пытался скомпилировать то, что написал выше.;))
С другой стороны, glibc pow
действительно работает, и glibc уже достаточно большой. Стандарт C должен быть переносимым, в том числе на различные встроенные устройства (на самом деле встроенные разработчики во всем мире в целом согласны с тем, что glibc уже слишком велик для них), и он не может быть переносимым, если для каждой простой математической функции необходимо включать все альтернативный алгоритм, который может пригодиться. Вот почему этого нет в стандарте C.
сноска: во время тестирования производительности времени я дал своим функциям относительно щедрые флаги оптимизации ( -s -O2
), которые, вероятно, будут сопоставимы, если не хуже, чем то, что, вероятно, использовалось для компиляции glibc в моей системе (archlinux), поэтому результаты, вероятно, будут Справедливая. Для более тщательной проверки, я должен был бы составить Glibc себя , и я reeeally не чувствую , как это делать. Раньше я использовал Gentoo, поэтому помню, сколько времени это занимает, даже если задача автоматизирована . Результаты для меня убедительны (или, скорее, неубедительны). Вы, конечно, можете сделать это сами.
Бонусный раунд: специализация pow(x, n)
для всех целых чисел важна, если требуется точный целочисленный вывод, что действительно происходит. Рассмотрите возможность выделения памяти для N-мерного массива с элементами p ^ N. Отключение p ^ N даже на единицу приведет к возможной случайной ошибке segfault.