Почему не стоит пользоваться функцией Pow для целых чисел?
p.s. Если что я говорю про C++ функцию Pow.
Мне не раз приходилось встречать рекомендации по типу "не пользуйтесь функцией Pow". В связи с чем многие дают эту рекомендацию?
void MyPow(int& a, int n)
{
int c = a;
for (size_t i = 0; i < n; i++)
a *= c;
}
Данная функция будет быстрее, чем стандартный Pow? Или данную рекомендацию дают из-за того, что Pow реализован не бинарно, а линейно? (вот этого я кстати не знаю бинарный он или нет)
Ответы (4 шт):
В вашей реализации pow - ошибка: она дает неверный ответ для нулевого n (и для отрицательных). Не говоря уже о том, что она не поддерживает дробные n.
Стандартная pow - быстрее, она работает за константное время (независимое от n) и может быть реализована несколькими инструкциями процессора pow(x,y)==exp( log(x)*y ). Но! Она преобразует числа в double и обратно, что может привести к потере точности т.к. 64-bit int - содержит больше значащих цифр, чем мантисса в double.
Вынужден извиниться заранее - я хочу поговорить о применимости pow вообще, а не только для целочисленных значений.
Даже если вы хотите возводить в степень умножением - то не надо делать это столь прямолинейно, есть метод быстрого возведения в степень n со скоростью O(lg n).
Далее, как и любой совет, это совет, а не догма. Этот совет совершенно справедлив, например (и я его постоянно даю в таких ситуациях), когда начинают вычислять что-то типа pow(-1,n) (сами догадаетесь, как это вычислить быстро и точно?) или pow(x,2) - потому что даже в том же VC++ pow с целочисленной степенью в реализована как (выбросил лишнее для понимания)
double pow(double _Xx, int _Yx) noexcept
{
if (_Yx == 2) return (_Xx * _Xx);
return pow(_Xx, static_cast<double>(_Yx));
}
Так какой смысл в постоянной проверке равно ли два - двум? :) При малых значениях степени также может оказаться, что непосредственное вычисление быстрее, чем вызов функции.
Если вы намекаете на шаблонную реализацию типа pow<int,int> - то, выбрасывая несущественное для понимания, в VC++ она выглядит так:
template<class _Ty1, class _Ty2,
class = enable_if_t<is_arithmetic_v<_Ty1> && is_arithmetic_v<_Ty2>>>
_Common_float_type_t<_Ty1, _Ty2> pow(const _Ty1 _Left, const _Ty2 _Right)
{ // bring mixed types to a common type
using _Common = _Common_float_type_t<_Ty1, _Ty2>;
return (pow(static_cast<_Common>(_Left), static_cast<_Common>(_Right)));
}
Т.е. все равно сводится к обычной pow с плавающей точкой. Которая начинает выполнять ряд телодвижений по проверке аргументов и т.п., так что простая замена на exp(y*log(x)) работает несколько быстрее (впрочем, эта разница существенно зависит от используемой модели с плавающей запятой - у VC++ 2017 от практически равных при /fp:fast до разницы в 1.8 раза при /fp:precise). Кстати, думаю (точнее - знаю :), см. P.P.S.), если применить даже ваш линейный способ вычисления - он будет опережать стандартный до достаточно больших значений степени.
Точность при возведении в степень целочисленного значения также страдает, но об этом уже писали выше.
Словом, всякий инструмент хорош, когда правильно применен.
Еще одно замечание в связи с последней фразой - меня также бесит, когда начинают использовать pow для вычисления какого-нибудь ряда типа
когда каждый член вычисляется возведением в степень, а не умножением на x, или когда так же в лоб вычисляют полином, игнорируя схему Горнера. Здесь применение pow глупо не потому, что она плоха, а потому, что здесь вообще не требуется возведение в степень!
P.S. А вообще, в программировании, как и во многих других областях деятельности - в том же кино масса примеров - сначала нечто начинают бездумно применять везде просто потому, что научились использовать это нечто. Потом приходит отрезвление - явный ведь перебор, может, вообще нужно отказаться от такой возможности?.. И только потом приходит понимание, что все хорошо в меру и на своем месте :) Но это так, отвлеченные размышления, не относящиеся к конкретно вопросу..
P.P.S. Не выдержал - заинтересовало, а в самом деле, когда будет быстрее использовать pow, чем просто линейное умножение? Набросал небольшой код, разово просчитал, построил график...
Получается, где-то до 30 степени лучше просто множить, чем считать экспоненту от логарифма, и где-то до 50 - если использовать pow. Если использовать быстрое возведение в степень - то эта кривая на графике просто не видна, так как ее значение на всем диапазоне колеблется около 0.25-0.3 мс..
Мне не раз приходилось встречать рекомендации по типу "не пользуйтесь функцией Pow". В связи с чем многие дают эту рекомендацию?
Дурная рекомендация, дело нехитрое. Функцию pow() использовать можно, и даже обязательно нужно, как минимум, в юнит-тестах.
Я уж и не знаю, специально, с определённым умыслом, в вопросе дана именно такая, достаточно странная реализация функции MyPow(), или так просто случайно вышло. Но это хорошая иллюстрация почему pow() следует использовать. В самом деле, если взглянуть на результаты тестов:
#include <cassert>
#include <climits>
#include <cmath>
#include <iostream>
#include <type_traits>
using namespace std;
void MyPow(int& a, int n)
{
int c = a;
for (size_t i = 0; i < n; i++)
a *= c;
}
template<typename I>
I ipow(I a, I b) {
if(a == 1 || b == 0) {
return 1;
}
if(is_signed<I>::value && b < 0) {
return (a == 0 ? numeric_limits<I>::min() :
a != -1 ? 0 :
b&1 ? -1 : 1);
}
I ret = 1;
while(b) {
if(b&1) {
ret *= a;
}
a *= a;
b >>= 1;
}
return ret;
}
#include <boost/core/demangle.hpp>
#include <typeinfo>
#ifdef COMPARE_POWL
typedef long double dtype_t;
#else
typedef double dtype_t;
#endif
static_assert(numeric_limits<dtype_t>::is_iec559, "Соответствие IEEE 754");
template<typename I>
void test_ipow(int verbose = 1) {
I start = 0;
if(is_signed<I>::value) {
start = -5;
}
I finish = 0;
while(pow<dtype_t>(finish, finish) <= (dtype_t)numeric_limits<I>::max() &&
pow<dtype_t>(finish, finish)
<= pow<dtype_t>(2, 1 + numeric_limits<dtype_t>::digits)) {
finish++;
}
--finish;
if(verbose) {
cout << "test_ipow<"
<< boost::core::demangle(typeid(I).name()).c_str()
<< ">(): start " << start
<< " finish " << finish
<< '\n';
}
for(I a = start; a <= finish; a++) {
for(I b = start; b <= finish; b++) {
if(verbose >= 2) {
cout << a << ", " << b
<< " -> " << ipow(a, b)
<< ", " << pow<dtype_t>(a, b)
<< ", " << (I)pow<dtype_t>(a, b)
<< '\n';
}
assert(ipow(a, b) == (I)pow<dtype_t>(a, b));
}
}
const I check_start = 3;
const I check_finish = (I)trunc(cbrt(numeric_limits<I>::max()));
intmax_t total_power = 0;
int max_diff = 0;
int min_diff = 0;
intmax_t sum_diff = 0;
intmax_t cnt_diff = 0;
for(I a = check_start; a <= check_finish; a++) {
I fi = (I)trunc(log((dtype_t)numeric_limits<I>::max())/log(a));
for(I b = check_start; b <= fi; b++) {
dtype_t d = pow<dtype_t>(a, b);
I i = ipow(a, b);
I diff = i - (I)d;
total_power++;
if(diff) {
int idiff = (int)diff;
assert(abs(idiff) <= d*numeric_limits<decltype(d)>::epsilon());
if(verbose) {
cnt_diff++;
sum_diff += idiff;
if(idiff < min_diff) {
min_diff = idiff;
}
if(idiff > max_diff) {
max_diff = idiff;
}
}
}
}
}
if(verbose) {
cout << "Total power: " << total_power
<< " Total diff: " << cnt_diff
<< " Mean diff: " << (dtype_t)sum_diff/cnt_diff
<< " Min: " << min_diff
<< " Max: " << max_diff
<< '\n';
}
}
int main(int ac, char *av[]) {
for(int i = 0; i <= 3; i++) {
int a = 2;
MyPow(a, i);
cout << "MyPow(2, " << i
<< ") -> " << a
<< " - " << pow(2, i)
<< (a == pow(2, i) ? ": норма\n" : ": ОШИБКА\n");
}
test_ipow<int>();
test_ipow<unsigned>();
test_ipow<int64_t>();
test_ipow<uint64_t>();
return 0;
}
Результат:
$ clang++-mp-15 -O3 -I/opt/local/include MyPow.cpp && ./a.out
MyPow(2, 0) -> 2 - 1: ОШИБКА
MyPow(2, 1) -> 4 - 2: ОШИБКА
MyPow(2, 2) -> 8 - 4: ОШИБКА
MyPow(2, 3) -> 16 - 8: ОШИБКА
test_ipow<int>(): start -5 finish 9
Total power: 1669 Total diff: 0 Mean diff: nan Min: 0 Max: 0
test_ipow<unsigned int>(): start 0 finish 9
Total power: 2067 Total diff: 0 Mean diff: nan Min: 0 Max: 0
test_ipow<long long>(): start -5 finish 14
Total power: 2161089 Total diff: 1587677 Mean diff: 4.30641 Min: -515 Max: 520
test_ipow<unsigned long long>(): start 0 finish 14
Total power: 2717817 Total diff: 2108847 Mean diff: 3.56374 Min: -1032 Max: 1040
Можно увидеть, что аргументация против использования функции pow() так себе:
- Точность. Для типа
intдля которого предназначенаMyPow()функцияpow()абсолютно точна, а функцияMyPow()всегда выдаёт странный результат. Функцияpowl()для Intel/AMD без SSE/AVX абсолютно точна до uint64_t; - Понятность, доказательность, корректность интерфейса. Функция
pow()- часть стандартов POSIX/C/C++, аMyPow(), прошу прощения, но это ж "колхоз на коленке". Любой, кто читает вызовMyPow(a, 0)илиMyPow(a, 1)некоторое время будет считать, что это возведение в 0 и в 1 степень, но, через некоторое время будет обескуражен, что это совсем не так; - Производительность. И в части производительности
MyPow()не блещет, во многом, благодаря своему странному интерфейсу, т.е. если компилятор видит её определение и может обеспечить inline подстановку, то ещё более менее, для неслишком больших степеней даже сможет немного обогнатьpow(), но при раздельной трансляции - беда;
Понятно, что как любая библиотечная функция pow() является определённым компромиссом и в каждом конкретном случае может быть, в идеале, заменена на более лучший вариант. Но это ж только в идеале, в частности, при условии понимания автором кода численных методов и соответствующем документировании кода.
В части использования pow() для плавающих и комплексных чисел, можно отметить:
- Почти всегда,
pow(x, 2)иpow(x, 3)можно заменить наx*xиx*x*x, хотя я и не уверен в пользе такой замены дляpow(x, 2); - Также не стоит использовать
pow(x, 0.5)иpow(x, 1./3.), вместоsqrt(x)иcbrk(x), хотя дляpow(x, 0.5)это вопрос только производительности; - В соседних ответах сравнивают производительности
pow(x, y)сexp(y*log(x))или сexp2(y*log2(x)), но такое сравнение имеет весьма ограниченную ценность, поскольку даже выражениеexp2(y*log2(x))всегда вычисляется с гораздо худшей точностью. Если диапазон данных ограничен, то можно рассмотреть заменуpow(x, y)на комбинациюexpm1()иlog1p(), но это ж для тех, кто понимает.
Уточнение по результату замечания @wololo, добавлен static_assert(numeric_limits<dtype_t>::is_iec559, "Соответствие IEEE 754");.
Все просто: функция std::pow принимает в качестве параметра число с плавающей запятой, двойное или длинное двойное число и возвращает то же самое в качестве возвращаемого типа. Это вообще не целочисленно-совместимая функция. Благодаря неявным приведениям (превращению одного типа в другой) это все равно может работать.
Я бы посоветовал вам открыть сайт https://godbolt.org/ и поэкспериментировать с ним, изучая дизассемблированный код.

