Resposta curta:
Uma especialização de pow(x, n)
onde n
é um número natural costuma ser útil para o desempenho de tempo . Mas o genérico da biblioteca padrão pow()
ainda funciona muito ( surpreendentemente! ) Bem para esse propósito e é absolutamente crítico incluir o mínimo possível na biblioteca C padrão para que possa ser o mais portátil e fácil de implementar possível. Por outro lado, isso não o impede de estar na biblioteca padrão C ++ ou no STL, que tenho certeza que ninguém está planejando usar em algum tipo de plataforma embarcada.
Agora, para a longa resposta.
pow(x, n)
pode ser feito muito mais rápido em muitos casos, especializando-se n
para um número natural. Tive de usar minha própria implementação dessa função para quase todos os programas que escrevo (mas escrevo muitos programas matemáticos em C). A operação especializada pode ser feita em O(log(n))
tempo, mas quando n
é pequena, uma versão linear mais simples pode ser mais rápida. Aqui estão implementações de ambos:
// 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;
}
(Saí x
e o valor de retorno dobra porque o resultado de pow(double x, unsigned n)
caberá em um dobro com a frequência necessária pow(double, double)
.)
(Sim, pown
é recursivo, mas quebrar a pilha é absolutamente impossível, pois o tamanho máximo da pilha será aproximadamente igual log_2(n)
e n
é um inteiro. Se n
for um inteiro de 64 bits, isso dá a você um tamanho máximo de pilha de cerca de 64. Nenhum hardware tem tal extremo limitações de memória, exceto para alguns PICs duvidosos com pilhas de hardware que têm profundidade de 3 a 8 chamadas de função.)
Quanto ao desempenho, você ficará surpreso com o que uma variedade de jardim pow(double, double)
é capaz. Testei cem milhões de iterações em meu IBM Thinkpad de 5 anos x
igual ao número da iteração e n
igual a 10. Nesse cenário, pown_l
venceu. glibc pow()
levou 12,0 segundos do usuário, pown
7,4 segundos do usuário e pown_l
apenas 6,5 segundos do usuário. Portanto, isso não é muito surpreendente. Estávamos mais ou menos esperando isso.
Então, eu deixo x
ser constante (eu configurei para 2,5), e fiz um loop n
de 0 a 19 cem milhões de vezes. Desta vez, de forma bastante inesperada, glibc pow
venceu, e por um deslizamento de terra! Demorou apenas 2,0 segundos do usuário. Meu pown
demorou 9,6 segundos e pown_l
12,2 segundos. O que aconteceu aqui? Fiz outro teste para descobrir.
Eu fiz a mesma coisa acima apenas com x
igual a um milhão. Desta vez, pown
venceu a 9.6s. pown_l
levou 12,2s e glibc pow levou 16,3s. Agora está claro! glibc tem pow
melhor desempenho que os três quando x
está baixo, mas pior quando x
está alto. Quando x
está alto, tem pown_l
melhor desempenho quando n
está baixo e tem pown
melhor desempenho quando x
está alto.
Portanto, aqui estão três algoritmos diferentes, cada um capaz de ter um desempenho melhor do que os outros nas circunstâncias certas. Então, em última análise, que a utilização mais provável depende de como você está pensando em usar pow
, mas usando a versão correta é vale a pena, e com todas as versões é bom. Na verdade, você pode até automatizar a escolha do algoritmo com uma função como esta:
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);
}
Contanto que x_expected
e n_expected
sejam constantes decididas em tempo de compilação, junto com possivelmente algumas outras advertências, um compilador de otimização que valha a pena removerá automaticamente a pown_auto
chamada de função inteira e a substituirá pela escolha apropriada dos três algoritmos. (Agora, se você realmente vai tentar usar isso, provavelmente terá que brincar um pouco com isso, porque eu não tentei exatamente compilar o que escrevi acima.;))
Por outro lado, glibc pow
funciona e glibc já é grande o suficiente. O padrão C deve ser portátil, incluindo para vários dispositivos incorporados (na verdade, desenvolvedores incorporados em todos os lugares geralmente concordam que glibc já é muito grande para eles), e não pode ser portátil se para cada função matemática simples ele precisa incluir todos algoritmo alternativo que pode ser útil. Então, é por isso que não está no padrão C.
nota de rodapé: no teste de desempenho de tempo, dei às minhas funções sinalizadores de otimização relativamente generosos ( -s -O2
) que provavelmente são comparáveis, senão piores do que o que provavelmente foi usado para compilar glibc em meu sistema (archlinux), então os resultados são provavelmente justo. Para um teste mais rigoroso, eu teria que compilar glibc mim e eu reeeally não sentir vontade de fazer isso. Eu costumava usar o Gentoo, então me lembro quanto tempo leva, mesmo quando a tarefa é automatizada . Os resultados são conclusivos (ou melhor, inconclusivos) o suficiente para mim. É claro que você pode fazer isso sozinho.
Rodada de bônus: A especialização de pow(x, n)
para todos os inteiros é instrumental se uma saída inteira exata for necessária, o que acontece. Considere alocar memória para uma matriz N-dimensional com p ^ N elementos. Tirar p ^ N igual a um resultará em um segfault que pode ocorrer aleatoriamente.