Коротка відповідь:
Спеціалізація того, pow(x, n)
де n
є натуральне число, часто корисна для виконання часу . Але загальна загальна бібліотека pow()
все ще працює досить (на диво! ) Добре для цієї мети, і абсолютно важливо якомога менше включити до стандартної бібліотеки С, щоб вона була зроблена максимально портативною та максимально простою для впровадження. З іншого боку, це зовсім не перешкоджає знаходженню в стандартній бібліотеці C ++ або STL, що я впевнений, що ніхто не планує використовувати в якійсь вбудованій платформі.
Тепер для довгої відповіді.
pow(x, n)
можна зробити набагато швидше у багатьох випадках, спеціалізуючись n
на натуральній кількості. Мені довелося використовувати власну реалізацію цієї функції майже для кожної програми, яку я пишу (але я пишу багато математичних програм на С). Спеціалізовану операцію можна зробити 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. Жодне обладнання не має такого екстремального значення обмеження пам’яті, за винятком деяких хитких ПІК з апаратними стеками, які займають лише 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 секунди користувача. Моє pown
зайняло 9,6 секунди, а pown_l
зайняло 12,2 секунди. Що тут сталося? Я зробив ще один тест, щоб з’ясувати.
Я робив те саме, що вище, лише з x
рівнем мільйона. Цього разу pown
виграв у 9,6с. pown_l
взяв 12,2s, а глібк-порох - 16,3s. Тепер це зрозуміло! 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 для них уже занадто великий), і він не може бути портативним, якщо для кожної простої математичної функції потрібно включати кожен альтернативний алгоритм, який може бути корисним. Отже, це не відповідає стандарту С.
виноска: під час тестування продуктивності часу я надав своїм функціям відносно щедрі прапори оптимізації ( -s -O2
), які, ймовірно, можна порівняти, якщо не гірше, ніж те, що, ймовірно, використовувалося для компіляції glibc у моїй системі (archlinux), тому результати, ймовірно, справедливий Для більш ретельної перевірки, я мав би скласти Glibc себе , і я reeeally не відчуваю , як це робити. Раніше я використовував Gentoo, тому я пам’ятаю, скільки часу займає, навіть коли завдання автоматизоване . Результати для мене достатньо переконливі (а точніше непереконливі). Звичайно, ви можете зробити це самостійно.
Раунд бонусів: спеціалізація pow(x, n)
на всі цілі числа є важливою, якщо потрібен точний цілий вихід, що і відбувається. Розглянемо виділення пам'яті для N-мірного масиву з елементами p ^ N. Якщо вимкнути p ^ N навіть по одному, це призведе до можливого випадкового виникнення segfault.