Como o Math.Pow () é implementado no .NET Framework?

433

Eu estava procurando uma abordagem eficiente para calcular a b (digamos a = 2e b = 50). Para começar, decidi dar uma olhada na implementação da Math.Pow()função. Mas no .NET Reflector , tudo o que encontrei foi o seguinte:

[MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical]
public static extern double Pow(double x, double y);

Quais são alguns dos recursos em que posso ver o que está acontecendo lá dentro quando chamo Math.Pow()função?

Pawan Mishra
fonte
15
Assim como um FYI, se você está confuso sobre o todo InternalCallcom um externmodificador (como eles parecem estar em conflito), consulte a pergunta (e as respostas resultantes) que publiquei sobre essa mesma coisa.
CraigTP
6
Para uma 2^xoperação se xfor inteiro, o resultado é uma operação de deslocamento. Então, talvez você possa construir o resultado usando uma mantissa de 2e um expoente de x.
Ja17 dez12
@SurajJain, seu comentário é realmente uma pergunta que você precisa postar separadamente.
ja72
@SurajJain Eu concordo com você. Eu não sou um moderador, então não posso fazer muito aqui. Talvez a pergunta downvote possa ser feita em meta.stackoverflow.com
ja72 14/16

Respostas:

855

MethodImplOptions.InternalCall

Isso significa que o método é realmente implementado no CLR, escrito em C ++. O compilador just-in-time consulta uma tabela com métodos implementados internamente e compila a chamada para a função C ++ diretamente.

A análise do código requer o código fonte do CLR. Você pode obter isso da distribuição SSCLI20 . Foi escrito em torno do período do .NET 2.0. Encontrei as implementações de baixo nível, que Math.Pow()ainda são amplamente precisas para versões posteriores do CLR.

A tabela de pesquisa está localizada em clr / src / vm / ecall.cpp. A seção que é relevante Math.Pow()é assim:

FCFuncStart(gMathFuncs)
    FCIntrinsic("Sin", COMDouble::Sin, CORINFO_INTRINSIC_Sin)
    FCIntrinsic("Cos", COMDouble::Cos, CORINFO_INTRINSIC_Cos)
    FCIntrinsic("Sqrt", COMDouble::Sqrt, CORINFO_INTRINSIC_Sqrt)
    FCIntrinsic("Round", COMDouble::Round, CORINFO_INTRINSIC_Round)
    FCIntrinsicSig("Abs", &gsig_SM_Flt_RetFlt, COMDouble::AbsFlt, CORINFO_INTRINSIC_Abs)
    FCIntrinsicSig("Abs", &gsig_SM_Dbl_RetDbl, COMDouble::AbsDbl, CORINFO_INTRINSIC_Abs)
    FCFuncElement("Exp", COMDouble::Exp)
    FCFuncElement("Pow", COMDouble::Pow)
    // etc..
FCFuncEnd()

A busca por "COMDouble" leva você a clr / src / classlibnative / float / comfloat.cpp. Vou poupar o código, basta dar uma olhada. Ele basicamente verifica os casos de canto e depois chama a versão do CRT pow().

O único outro detalhe de implementação interessante é a macro FCIntrinsic na tabela. Essa é uma dica de que o jitter pode implementar a função como intrínseca. Em outras palavras, substitua a chamada de função por uma instrução de código de máquina de ponto flutuante. O que não é o caso Pow(), não há instruções de FPU para isso. Mas certamente para as outras operações simples. Notável é que isso pode tornar a matemática de ponto flutuante em C # substancialmente mais rápida que o mesmo código em C ++, verifique esta resposta pelo motivo.

A propósito, o código fonte do CRT também estará disponível se você tiver a versão completa do diretório vc / crt / src do Visual Studio. Você atingirá o muro pow(), porém, a Microsoft comprou esse código da Intel. Fazer um trabalho melhor do que os engenheiros da Intel é improvável. Embora a identidade do meu livro do ensino médio fosse duas vezes mais rápida quando eu tentei:

public static double FasterPow(double x, double y) {
    return Math.Exp(y * Math.Log(x));
}

Mas não é um substituto verdadeiro, pois acumula erro de três operações de ponto flutuante e não lida com os problemas de domínio esquisito que Pow () possui. Como 0 ^ 0 e -Infinity aumentado para qualquer poder.

Hans Passant
fonte
437
Ótima resposta, o StackOverflow precisa de mais desse tipo de coisa, em vez de 'Por que você gostaria de saber isso?' isso acontece com muita frequência.
Tom W
16
@ Blue - Eu não sei, falta de tirar sarro dos engenheiros da Intel. Meu livro do ensino médio tem um problema em elevar algo ao poder de uma integral negativa. Pow (x, -2) é perfeitamente computável, Pow (x, -2.1) é indefinido. Problemas de domínio são uma chatice de lidar.
precisa
12
@ BlueRaja-DannyPflughoeft: Muito esforço é gasto tentando garantir que as operações de ponto flutuante sejam o mais próximo possível do valor arredondado corretamente. powé notoriamente difícil de implementar com precisão, sendo uma função transcendental (consulte o Dilema do criador de tabelas ). É muito mais fácil com um poder integral.
PORGES
9
@ Hans Passant: Por que Pow (x, -2.1) seria indefinido? Matematicamente pow é definido em todos os lugares para todos os xey. Você costuma obter números complexos para x negativo e y não inteiro.
Jules
8
@Jules pow (0, 0) não está definido.
grava
110

A resposta de Hans Passant é ótima, mas eu gostaria de acrescentar que, se bfor um número inteiro, a^bpode ser computado com muita eficiência com decomposição binária. Aqui está uma versão modificada de Henry Warren's Hacker's Delight :

public static int iexp(int a, uint b) {
    int y = 1;

    while(true) {
        if ((b & 1) != 0) y = a*y;
        b = b >> 1;
        if (b == 0) return y;
        a *= a;
    }    
}

Ele observa que essa operação é ideal (faz o número mínimo de operações aritméticas ou lógicas) para todos os b <15. Também não há solução conhecida para o problema geral de encontrar uma sequência ótima de fatores para calcular a^bqualquer b que não seja um extenso procurar. É um problema NP-Hard. Então, basicamente, isso significa que a decomposição binária é a melhor possível.

Michael Graczyk
fonte
11
Este algoritmo ( quadrado e multiplicar ) também se aplica se afor um número de ponto flutuante.
CodesInChaos
14
Na prática, é possível fazer um pouco melhor do que o quadrado nativo e a multiplicação. Por exemplo, preparando tabelas de pesquisa para pequenos expoentes para que você possa fazer o quadrado várias vezes e só depois multiplicar ou criar cadeias de adição quadrada otimizadas para expoentes fixos. Esse tipo de problema é parte integrante de importantes algoritmos criptográficos, portanto, houve bastante trabalho para otimizá-lo. A dureza NP é apenas sobre os assintóticos de pior caso ; geralmente podemos produzir soluções ótimas ou quase otimizadas para instâncias do problema que surge na prática.
CodesInChaos
O texto não menciona aser um número inteiro, mas o código sim. Como conseqüência disso, pergunto-me sobre a precisão do resultado da computação "muito eficiente" do texto.
Andrew Morton
69

Se a versão C disponível gratuitamentepow for qualquer indicação, ela não se parece com o que você esperaria. Não seria de muita ajuda para você encontrar a versão .NET, porque o problema que você está resolvendo (ou seja, aquele com números inteiros) é de ordens de magnitudes mais simples e pode ser resolvido em algumas linhas de código C # com a exponenciação ao quadrado do algoritmo .

dasblinkenlight
fonte
Obrigado pela sua resposta. O primeiro link me surpreendeu, pois eu não esperava uma implementação técnica tão maciça da função Pow (). Embora a resposta de Hans Passant confirme que também é a mesma no mundo .Net. Penso que posso resolver o problema em questão, utilizando algumas das técnicas listadas no link do algoritmo quadrático. Obrigado novamente.
Pawan Mishra
2
Não acredito que esse código seja eficiente. 30 variáveis ​​locais devem apenas colidir com todos os registradores. Suponho apenas que seja a versão ARM, mas no x86 30 variáveis ​​locais no método são impressionantes.
Alex Zhukovskiy