Función "insegura" de C#-*(float*)(& resultado) vs.(float)(resultado)
unsafe type-punning (6)
¿Alguien puede explicar de manera simple los siguientes códigos:
public unsafe static float sample(){
int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);
return *(float*)(&result); //don''t know what for... please explain
}
Nota: el código anterior usa una función insegura
Para el código anterior, estoy teniendo dificultades porque no entiendo cuál es la diferencia entre su valor de retorno en comparación con el valor de retorno a continuación:
return (float)(result);
¿Es necesario usar una función no segura si está regresando *(float*)(&result)
?
Como Sarge Borsch preguntó, aquí está el equivalente de ''Unión'':
[StructLayout(LayoutKind.Explicit)]
struct ByteFloatUnion {
[FieldOffset(0)] internal byte byte0;
[FieldOffset(1)] internal byte byte1;
[FieldOffset(2)] internal byte byte2;
[FieldOffset(3)] internal byte byte3;
[FieldOffset(0)] internal float single;
}
public static float sample() {
ByteFloatUnion result;
result.single = 0f;
result.byte0 = 154;
result.byte1 = 153;
result.byte2 = 25;
result.byte3 = 64;
return result.single;
}
Como otros ya han descrito, se trata de los bytes de un int como si fueran un flotador.
Puede obtener el mismo resultado sin utilizar un código inseguro como este:
public static float sample()
{
int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);
return BitConverter.ToSingle(BitConverter.GetBytes(result), 0);
}
Pero entonces ya no será muy rápido y también podrías usar flotadores / dobles y las funciones matemáticas.
En .NET, un float
se representa utilizando un número flotante de precisión simple IEEE binary32 almacenado con 32 bits. Aparentemente, el código construye este número al ensamblar los bits en un int
y luego lo convierte en un float
usando unsafe
. La conversión es lo que en términos de C ++ se llama reinterpret_cast
donde no se realiza ninguna conversión cuando se realiza la conversión; los bits se reinterpretan como un nuevo tipo.
El número ensamblado es 4019999A
en hexadecimal o 01000000 00011001 10011001 10011010
en binario:
- El bit de signo es 0 (es un número positivo).
- Los bits del exponente son
10000000
(o 128) resultando en el exponente 128 - 127 = 1 (la fracción se multiplica por 2 ^ 1 = 2). - Los bits de fracción son
00110011001100110011010
que, si no más, casi tienen un patrón reconocible de ceros y unos.
El float devuelto tiene exactamente los mismos bits que 2.4 convertidos a punto flotante y la función completa puede simplemente reemplazarse por el literal 2.4f
.
El cero final que el tipo de "rompe el patrón de bits" de la fracción ¿existe tal vez para hacer que el flotante coincida con algo que se puede escribir utilizando un literal de coma flotante?
Entonces, ¿cuál es la diferencia entre un reparto regular y este extraño "reparto inseguro"?
Supongamos el siguiente código:
int result = 0x4019999A // 1075419546
float normalCast = (float) result;
float unsafeCast = *(float*) &result; // Only possible in an unsafe context
El primer lanzamiento toma el número entero 1075419546
y lo convierte a su representación de punto flotante, por ejemplo, 1075419546f
. Esto implica el cálculo de los bits de signo, exponente y fracción requeridos para representar el entero original como un número de punto flotante. Este es un cálculo no trivial que se tiene que hacer.
El segundo lanzamiento es más siniestro (y solo se puede realizar en un contexto inseguro). El &result
toma la dirección del result
devolviendo un puntero a la ubicación donde se almacena el entero 1075419546
. El operador de desreferenciación del puntero *
se puede utilizar para recuperar el valor apuntado por el puntero. El uso de *&result
recuperará el número entero almacenado en la ubicación; sin embargo, al convertir el puntero en un float*
(un puntero a un float
), se obtiene un flotante de la ubicación de la memoria, lo que hace que el flotador 2.4f
se asigne a unsafeCast
. Entonces, la narrativa de *(float*) &result
es un puntero al result
y asume que el puntero es un puntero a un float
y recupera el valor apuntado por el puntero .
A diferencia del primer lanzamiento, el segundo lanzamiento no requiere ningún cálculo. Simplemente empuja los 32 bits almacenados en el result
en unsafeCast
(que afortunadamente también es de 32 bits).
En general, realizar un reparto como ese puede fallar de muchas maneras, pero al usar unsafe
le está diciendo al compilador que sabe lo que está haciendo.
Esto parece un intento de optimización. En lugar de hacer cálculos de punto flotante, está haciendo cálculos de números enteros en la representación Integral de un número de punto flotante.
Recuerde, los flotadores se almacenan como valores binarios al igual que ints.
Una vez que se realiza el cálculo, está utilizando los punteros y la conversión para convertir el entero en el valor flotante.
Esto no es lo mismo que lanzar el valor a un flotador. Eso convertirá el valor int 1 en el float 1.0. En este caso, convierta el valor int en el número de punto flotante descrito por el valor binario almacenado en el int.
Es bastante difícil de explicar adecuadamente. Voy a buscar un ejemplo. :-)
Editar: Mira aquí: http://en.wikipedia.org/wiki/Fast_inverse_square_root
Su código básicamente hace lo mismo que se describe en este artículo.
Si estoy interpretando lo que el método está haciendo correctamente, este es un equivalente seguro:
public static float sample() {
int result = 154 + (153 << 8) + (25 << 16) + (64 << 24);
byte[] data = BitConverter.GetBytes(result);
return BitConverter.ToSingle(data, 0);
}
Como ya se ha dicho, se está reinterpretando el valor int
como un valor float
.