Los tipos de datos numéricos de punto flotante como float y double son útiles para cálculos científicos (coordenadas espaciales, mediciones sobre magnitudes físicas que requieren alta precisión); el tipo float es utilizado para la creación de librerías de gráficos, double para representación de números reales.
Por otro lado, el tipo decimal es útil para cálculo financiero y también para valores resultantes de operaciones hechas por el mismo hombre en lugar de valores obtenidos de mediciones de la naturaleza.
En la Tabla 1 se incluyen algunas de las diferencias importantes de estos tres tipos de datos numéricos.
Tabla 1. Resumen de diferencias de tipos punto flotante y decimal [5].
1. Rendimiento
Entre las consideraciones de rendimiento de los tipos de punto flotante y el tipo decimal se tienen las siguientes:
La aritmética de punto flotante es significativamente mayor, debido a que está soportada directamente por el hardware adyacente.
La aritmética con datos de tipo decimal es tratada en base 10 (decimal) y manejada directamente por software. Esto la hace varias veces más lenta. Esto recae en reducción del rendimiento de la aplicación de forma considerable.
En el siguiente código en lenguaje C# presento una versión rudimentaria de prueba de rendimiento, que consiste en generar números pseudoaleatorios para los tipos double y decimal, luego se calcula la suma de todos los aleatorios generados, y se toma el tiempo en milisegundos, y se presentan estos resultados:
Figura 1. Ejecución prueba no. 1 double vs decimal.
Figura 1. Ejecución prueba no. 2 double vs decimal.
Figura 1. Ejecución prueba no. 3 double vs decimal.
2. Conclusiones
Hemos visto muy someramente las diferencias entre los tipos de datos de punto flotante y decimal En la prueba en código C# que se realizó se manifiesta la diferencia significativa a la hora de realizar aritmética básica de suma de números pseudoaleatorios de ambos tipos. Esta consideración debe ser tenida en cuenta cuando creemos aplicaciones centradas en datos, en particular financieras o que requieran cálculos financieros.
3. Glosario
- Aritmética de punto flotante - decimal - Floating point - Prueba de rendimiento
0. Introducción 1. División por Cero 2. División de Cero Entre Cero y Substracción de Infinitos 3. Uso del Operador de Igualdad (==) con Constantes de Punto Flotantes Especiales 4. Otras Operaciones 5. Conclusiones 6. Glosario 7. Referencias
0. Introducción
En esta entrada vamos a tener la oportunidad de hablar acerca de valores especiales (representados como constantes) para los tipos de datos numéricos de punto flotante (float y double). Estos conceptos son importantes introducirlos en esta etapa temprana de la exploración del lenguaje C#, pues nos permitirá comprender el porqué de resultados extraños cuando operamos con valores de los tipos mencionados anteriormente.
Los tipos de datos numéricos de punto flotante float y double se diferencian también de los enteros por algunos valores que son tratados de manera especial en ciertas operaciones. Estos tipos son compatibles con la especificación de formato de tipos de la IEEE 754 [5], la cual es soportada de de manera nativa por todos los microprocesadores.
Estos valores especiales soportados por los dos tipos de datos numéricos de punto flotante son:
NaN (Not a Number)
+∞
-∞
-0
Además, las clases tanto float como double poseen constantes para representar estos valores, además de otros valores especiales (MaxValue, MinValue, y Epsilon). Así:
La Tabla 1 muestra las constantes que representan estos valores especiales para los tipos float y double.
Table 1. Constantes de los Valores Especiales de Tipos Flotantes [4].
1. División por Cero
Al operar con valores de tipo float o double , en particular con la operación división donde el divisor sea cero el resultado será un valor infinito [4][3].
2. División de Cero Entre Cero y Substracción de Infinitos
Al dividir cero entre cero, o al sustraer infinito de infinito, el resultado es la constante especial NaN (Not a Number, que en español se podría producir como No es un Número) [6]. Así:
Console.WriteLine(0.0/0.0);// NaN Console.WriteLine((1.0/0.0)-(1.0/0.0));// NaN
Este valor en la jerga matemática son conocidos como valores producidos por expresiones indefinidas. En Wikipedia, en el artículo [3], podemos encontrar una explición muy sencilla sobre el problema que genera este tipo de operaciones en situacionese cotidianas, como el asunto de distribuir/repartir diez galletas entre 0 personas.
3. Uso del Operador de Igualdad con las Constantes Especiales de los Tipos de Punto Flotante
Cuando usamos el operador de igualdad ==, NaN no es igual a cualquier otro valor del espacio de valores de los tipos de datos numéricos float y double, e inclusive al comparar éste con otro NaN.
Observemos esta línea de código: Console.WriteLine(0.0/0.0==double.NaN);// False
A pesar de que la expresión 0.0/0.0 genera un valor NaN, y lo comparamos con la constante NaN de double la evaluación de la expresión de igualdad nos da como resultado False.
4. Otras Operaciones
Los tipos de datos numéricos de punto flotante como float y double cuentan con el método IsNaN para comprobar si una expresión o valor es NaN:
Console.WriteLine(double.IsNaN(0.0/0.0));// True
Por otro lado, cuando usamos el método Equals de Object, dos valores NaN son iguales. Por ejemplo:
La constante NaN es útil para representar, por ejemplo, el valor «Automatic» en medidas de componentes en WPF[7].
5. Conclusiones
Hemos explorado la importancia de valores constantes para los tipos flotantes y su utilidad en la aritmética de expresiones representadas en lenguaje C#. Tenemos ±Infinity para representar el valor resultante de una operación que involucra la división de un número distinto de cero entre cero. Por otro lado, NaN es el resultado de intentar calcular la raíz de un número negativo, y también de la división entre 0 y 0, por ejemplo.
6. Glosario
- IEEE 754 - NaN (Not a Number) - Punto Flotante - WPF
0. Introducción 1. Conversión en Sistemas Numéricos 2. Complemento (~) 3. AND (&) 4. OR Inclusivo (|) 5. OR Exclusivo (^) - XOR 6. Desplazamiento a la Izquierda 7. Desplazamiento a la Derecha 8. Conclusiones 9. Glosario 10. Referencias
Introducción
En esta entrada veremos una introducción generalizada acerca de los operadores bitwise (bit a bit) que provee C#. Estos operadores son muy interesantes debido a aspectos como rendimiento y eficiencia en el consumo de poder por parte de los circuitos integrados de un sistema de cómputo pequeño. Además, y de acuerdo con [2]: en procesadores de bajo coste, las operaciones bitwise son mucho más eficiente y rápidas que la división, e inclusive varias veces más rápidas que la adición, la substracción y la multiplicación, debido a que son tratadas directamente por las instrucciones de bajo nivel sobre el procesador adyacente.
A pesar que muchos de los procesadores modernos incluyen diseños más óptimos que igualan o superan con operaciones aritméticas a las operaciones bitwise, muchas veces programadores o diseñadores de circuitos deben enfrentarse a retos de rendimiento sobre dispositivos con procesadores de bajas prestaciones (sinónimo de procesadores para fines específicos, por ejemplo, un sistema embebido que requiera bajo poder de cómputo para poder funcionar); entonces es necesario pensar en operar a nivel de bit con los operadores que le corresponde que conduzcan a bajo de consumo de poder por parte del dispositivo.
En la Tabla 1 vemos el listado de operadores bitwise que soporta el lenguaje de programación C#.
Tabla 1. Operadores bitwise en C#[3].
1. Conversión de Sistemas Numéricos
Antes de pasar a demostrar el uso de los operadores bitwise de C#, es importante introducir el concepto de conversión y las operaciones o pasos necesarios para llevar un número de un sistema de numeración a otros (e.g., decimal) a otro distinto (e.g., binario [1]). Además esto nos ayudará entender la utilidad de estos operadores y lo interesante que resultan para proyectos de hardware.
El sistema de numeración decimal nos resulta muy familiar pues es el universalmente utilizado para muchas aplicaciones de la vida diaria: mercado, academia (involucradas las matemáticas), en las rutinas diarias, el tiempo, etc. Este sistema está compuesto por 10 cifras diferentes (de ahí su nombre) y los números se componen y generan el significado de acuerdo a su posición. Ejemplos de números decimales:
Por otro lado, el sistema de numeración binario es el lenguaje de las computadoras para tomar decisiones y procesar datos. Los programas y operaciones escritas en este lenguaje para los humanos resulta complejo y dispendioso para completarse. Los mismos tres números anterios en base decimal, ahora en binario:
1.1 Decimal a Binario
Este proceso de conversación es muy sencillo, consiste en realizar divisiones sucesivas: empezando por el número en base decimal dividido por 2; el residuo de esta división puede ser 0 ó 1, este valor va formando parte de la cadena de bits de la conversión resultante, el cociente se convertirá en el divisor (debe ser distinto a 0) por 2. El proceso anterior finaliza cuando el cociente de la división iterativa se convierta en cero.
Para ilustrar con más detalle este proceso se presenta la Tabla 2.
Tabla 2. Conversión 495 (base decimal) a binario (base dos).
Entonces asumamos que en C# el número 495 se almacena en una variable tipo Int16[4], luego su equivalente en binario es: 111101111. Para convertir un número decimal negativo (e.g., -495) a binario, seguimos el siguiente proceso:
Tomamos la representación binaria de 495: 111101111.
Lo invertimos: 000010000.
Le sumamos 1: 000010001.
Luego como el tipo de dato es Int16rellenamos los bits restantes con el signo para negativo, es decir 1, así: 1111111000010001.
1.2 Binario a Decimal
Pasemos a convertir un número en base dos a base diez: 0000111100000111 (Int16) . Obsérvese que el primero dígito es 0 por lo tanto el número es negativo). Ahora, este número lo representamos en su modo inverso, así: 1110000011110000. Ahora utilicemos el siguiente método (Tabla 3):
Tabla 3. Conversión Binario a Decimal.
Ahora procedemos a sumar todos los resultados de la tercera fila: 1 + 2 + 4 + 0 + 0 + 0 + 0 + 0 + 256 + 512 + 1024 + 2048 + 0 + 0 + 0 = 3847 Adicionalmente, podemos agregar el proceso de conversión de un número binario negativo a decimal (1111111111010011 (Int16), observemos que se trata de un número negativo, pues el bit más significativo es 1):
Invertimos el número original: 0000000000101100.
El anterior número en decimal es igual a 44.
Sumamos 1 a 44: 45.
Lo convertimos a negativo: -45.
Entonces 1111111111010011 equivale a -45 en decimal.
2. Complemento (~)
El operador bitwise~ -o NOT-, realiza la operación unaria que ejecuta la negación lógica de cada bit del número binario en cuestión. Si en la cadena de bits encontramos un 1 esté será 0, si por el contrario hayamos un 0 se convertirá en 1. Tenamos en cuenta que si el tipo de dato es con signo:
Si el número es negativo se convertirá en positivo:
Si el número es positivo se convertirá en negativo:
En bytenotB=(byte)~b; se declara la variable notB de tipo byte y asignamos el complemento de b. En la salida estándar:
Console.WriteLine("{0:0}",Convert.ToString(b,2)); muestra en pantalla 110101. La clase estática Convert[] posee varios métodos miembro estáticos para la conversión entre tipos numéricos.
3. AND (&)
El operador & corresponde con la operación lógica AND (conjución lógica [6]). Vamos a realizar la siguiente operación utilizando este operador para dejar más claro el concepto: Convirtamos los dos siguientes números a binario, 113 y 121:
Aplicando el operador & a los dos números binarios resultantes, tenemos:
Entonces, cuando operamos 1111001 AND 1110001 obtenemos: 1110001 (113 en decimal). Cuando operemos A & B:
Si A y B son negativos el resultado de operar A & B será negativo,
Para ejecutar una operación de este tipo, también realizamos la conversión de decimal a binario. Aquí tenemos un ejemplo con dos números decimales: 39, y 42. El equivalente en binario:
Cuando operamos bit a bit con este operador, el resultado es 0 cuando ambos operandos son 0, y 1 en para los dos casos restantes. Miremos el resultado de operar 1000111 | 101010:
Entonces, al operar A | B obtenemos 101111 (equivalente a 47 en decimal).
A excepción de del OR inclusivo, este operador genera como resultado 1 cuando ambos operandos son diferentes (de ahí su nombre). Hagamos un ejemplo para demostrar el uso de este operador: Tenemos los números decimales 17 y 29. Los convertimos en binario y tenemos:
Apliquemos el operador XOR (^):
Cuando operamos A ^ B obtenoms 1100 (12 en decimal).
Con esta operación bitwise, podemos mover n cantidad de bits a la izquierda sobre x: x << n. Las posiciones vacías se rellenan con ceros. Ver Figura 1 (tomada desde [7]):
Figura 1. Desplazamiento a la Izquierda [7].
La operación 7 << 2 mueve 2 bits a la izquierda sobre 7 (00000111). Observemos esta operación para diferentes número de desplazamientos a la izquierda en la siguiente tabla:
Obsérvese que a medida que se agregan ceros (0) a la izquierda el número binario puede ocurrir dos casos:
El número aumente de valor como en 7 << 1, 7 << 2, y 7 << 3, o que
El número disminuya su valor como 7 << 6, 7 << 7, y 7 << 8.
Cálculo de potencias de 2 usando desplazamiento a la izquierda
A continuación también se demuestra uno de los beneficios de usar operaciones bitwise a sus homólogas aritméticas. 1 << n calcula 2^n. Esta operación bitwise resulta mucho más rápida que la función estática Math.Pow. Mirémoslo en el siguiente ejemplo en C#: Archivo C# DesplazamientoIzquierdaPotenciasDeDos.cs [enlace alternativo]:
7. Desplazamiento a la Derecha >>
Con esta operación bitwise, podemos mover n cantidad de bits a la derecha sobre x: x << n. Las posiciones vacías se rellenan con ceros. Ver Figura 2 (tomada desde [7]):
Nota
Si el tipo de dato numérico posee signo, éste será conservado a pesar del desplazamiento.
En la siguiente tabla se va representado fila por fila el cambio efectuado a medida que aumenta el número de bits de desplazamiento a la derecha sobre el decimal 160:
Cálculo de x / 2^n usando Desplazamiento a la Derecha
Al usar x >> n es equivalente a x/2^n. Por ejemplo, 3 >> 2 equivale a 3 / 2^2 (3/4). Esta operación también resulta más rápida que 2/Math.Pow(2,3). Veámoslo en el siguiente ejemplo: Archivo C#RendimientoDesplazamientoDerecha.cs [enlace alternativo]:
8. Conclusiones
En este artículo pudimos enunciar algunas de las ventajas (e.g., rendimiento sobre procesadores de bajo poder de procesamiento o de bajo coste) de los operadores de bitwise. También se introdujeron ejemplos sencillo acerca del cómo las operaciones desplazamiento a la izquierda (left shift) y desplazamiento a la derecha (right shift) son mucho más eficientes que las aritméticas.
9. Glosario
- Binario - Decimal - Desplazamiento a la derecha (right shift) - Desplazamiento a la izquierda (left shift) - Operación bitwise - Sistema numérico