¿Cómo se implementa Math.Pow () en .NET Framework?

433

Estaba buscando un enfoque eficiente para calcular a b (digamos a = 2y b = 50). Para comenzar, decidí echar un vistazo a la implementación de la Math.Pow()función. Pero en .NET Reflector , todo lo que encontré fue esto:

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

¿Cuáles son algunos de los recursos en los que puedo ver lo que sucede dentro cuando llamo a la Math.Pow()función?

Pawan Mishra
fuente
15
Al igual que para su información, si está confundido sobre el conjunto InternalCallcon un externmodificador (ya que parecen estar en conflicto), vea la pregunta (y las respuestas resultantes) que publiqué sobre esto mismo.
CraigTP
66
Para una 2^xoperación si xes entera, el resultado es una operación de cambio. Entonces, tal vez podrías construir el resultado usando una mantisa de 2y un exponente de x.
ja72
@SurajJain tu comentario es en realidad una pregunta que debes publicar por separado.
ja72
@SurajJain Estoy de acuerdo contigo. No soy moderador, así que no puedo hacer mucho aquí. Tal vez la pregunta de voto
negativo

Respuestas:

855

MethodImplOptions.InternalCall

Eso significa que el método se implementa realmente en el CLR, escrito en C ++. El compilador justo a tiempo consulta una tabla con métodos implementados internamente y compila la llamada a la función C ++ directamente.

Echar un vistazo al código requiere el código fuente para el CLR. Puede obtener eso de la distribución SSCLI20 . Fue escrito alrededor del marco de tiempo de .NET 2.0, he encontrado que las implementaciones de bajo nivel Math.Pow()siguen siendo muy precisas para versiones posteriores de CLR.

La tabla de búsqueda se encuentra en clr / src / vm / ecall.cpp. La sección relevante para se Math.Pow()ve así:

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()

La búsqueda de "COMDouble" lo lleva a clr / src / classlibnative / float / comfloat.cpp. Te ahorraré el código, solo échale un vistazo. Básicamente verifica si hay casos de esquina, luego llama a la versión de CRT pow().

El único otro detalle de implementación que es interesante es la macro FCIntrinsic en la tabla. Esa es una pista de que el jitter puede implementar la función como intrínseca. En otras palabras, sustituya la llamada a la función con una instrucción de código de máquina de coma flotante. Lo cual no es el caso Pow(), no hay instrucciones de FPU para ello. Pero ciertamente para las otras operaciones simples. Es notable que esto puede hacer que las matemáticas de coma flotante en C # sean sustancialmente más rápidas que el mismo código en C ++, verifique esta respuesta por la razón.

Por cierto, el código fuente para el CRT también está disponible si tiene la versión completa del directorio Visual Studio vc / crt / src. Sin pow()embargo, te toparás con la pared , Microsoft compró ese código de Intel. Hacer un mejor trabajo que los ingenieros de Intel es poco probable. Aunque la identidad de mi libro de secundaria era el doble de rápido cuando lo probé:

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

Pero no es un verdadero sustituto porque acumula el error de 3 operaciones de coma flotante y no se ocupa de los problemas de dominio raros que tiene Pow (). Como 0 ^ 0 y -Infinity elevado a cualquier poder.

Hans Passant
fuente
437
Gran respuesta, StackOverflow necesita más de este tipo de cosas, en lugar de "¿Por qué quieres saber eso?" eso sucede con demasiada frecuencia.
Tom W
16
@Blue: no sé, salvo burlarse de los ingenieros de Intel. Mi libro de secundaria tiene un problema para plantear algo al poder de una integral negativa. Pow (x, -2) es perfectamente computable, Pow (x, -2.1) no está definido. Los problemas de dominio son muy difíciles de manejar.
Hans Passant
12
@ BlueRaja-DannyPflughoeft: Se dedica un gran esfuerzo a tratar de garantizar que las operaciones de punto flotante estén lo más cerca posible del valor redondeado correctamente. powes notoriamente difícil de implementar con precisión, ya que es una función trascendental (ver el dilema del creador de tablas ). Es mucho más fácil con un poder integral.
porges
99
@Hans Passant: ¿Por qué Pow (x, -2.1) estaría indefinido? Matemáticamente, pow se define en todas partes para todos los x e y. Tiende a obtener números complejos para x negativo y no entero y.
Jules
8
@Jules pow (0, 0) no está definido.
relieve
110

La respuesta de Hans Passant es excelente, pero me gustaría agregar que si bes un entero, entonces a^bse puede calcular de manera muy eficiente con la descomposición binaria. Aquí hay una versión 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;
    }    
}

Señala que esta operación es óptima (hace el número mínimo de operaciones aritméticas o lógicas) para todos b <15. Además, no existe una solución conocida para el problema general de encontrar una secuencia óptima de factores para calcular a^bpara cualquier b que no sea un extenso buscar. Es un problema NP-Hard. Básicamente, eso significa que la descomposición binaria es tan buena como se pone.

Michael Graczyk
fuente
11
Este algoritmo ( cuadrado y multiplicar ) también se aplica si aes un número de coma flotante.
CodesInChaos
14
En la práctica, es posible hacer un poco mejor que el cuadrado y multiplicar nativo. Por ejemplo, preparar tablas de búsqueda para exponentes pequeños para que pueda cuadrar varias veces y solo luego multiplicar, o construir cadenas de suma cuadrada optimizadas para exponentes fijos. Este tipo de problema es parte integral de algoritmos criptográficos importantes, por lo que se ha trabajado bastante para optimizarlo. La dureza de NP solo se trata de los asintóticos en el peor de los casos , a menudo podemos producir soluciones óptimas o casi óptimas para casos del problema que surge en la práctica.
CodesInChaos
El texto no menciona aser un número entero, pero el código sí. Como consecuencia de eso, me pregunto acerca de la precisión del resultado del cálculo "muy eficiente" del texto.
Andrew Morton
69

Si la versión C de disponibilidad gratuitapow es una indicación, no se parece a nada de lo que cabría esperar. No sería de mucha ayuda para usted encontrar la versión .NET, porque el problema que está resolviendo (es decir, el que tiene números enteros) es un orden de magnitud más simple y puede resolverse en unas pocas líneas de código C # con la exponenciación mediante el algoritmo de cuadratura .

dasblinkenlight
fuente
Gracias por tu respuesta. El primer enlace me sorprendió, ya que no esperaba una implementación técnica tan masiva de la función Pow (). Aunque la respuesta de Hans Passant confirma que también es lo mismo en el mundo .Net. Creo que puedo resolver el problema en cuestión haciendo uso de algunas de las técnicas enumeradas en el enlace del algoritmo de cuadratura. Gracias de nuevo.
Pawan Mishra
2
No creo que este código sea eficiente. 30 variables locales solo deberían superar todos los registros. Solo supongo que es la versión ARM, pero en x86 30 variables locales en el método es increíble.
Alex Zhukovskiy