¿Por qué C no tiene carrozas sin signo?

131

Lo sé, la pregunta parece ser extraña. Los programadores a veces piensan demasiado. Por favor sigue leyendo ...

En CI uso signedy unsignedenteros mucho. Me gusta el hecho de que el compilador me advierte si hago cosas como asignar un entero con signo a una variable sin signo. Recibo advertencias si comparo enteros firmados con enteros sin signo y mucho más.

Me gustan estas advertencias. Me ayudan a mantener mi código correcto.

¿Por qué no tenemos el mismo lujo para las carrozas? Una raíz cuadrada definitivamente nunca devolverá un número negativo. También hay otros lugares donde un valor flotante negativo no tiene sentido. Candidato perfecto para una carroza sin firmar.

Por cierto, no estoy realmente interesado en el solo bit extra de precisión que podría obtener al eliminar el bit de signo de los flotadores. Estoy súper feliz con floats como lo están ahora. Solo me gustaría marcar un flotador como sin firmar veces y obtener el mismo tipo de advertencias que obtengo con los enteros.

No conozco ningún lenguaje de programación que admita números de punto flotante sin signo.

¿Alguna idea de por qué no existen?


EDITAR:

Sé que la FPU x87 no tiene instrucciones para lidiar con flotadores sin firmar. Solo usemos las instrucciones de flotación firmadas. El mal uso (por ejemplo, ir por debajo de cero) podría considerarse un comportamiento indefinido de la misma manera que el desbordamiento de enteros con signo no está definido.

Nils Pipenbrinck
fuente
44
Interesante, ¿puede publicar un ejemplo de un caso en el que la comprobación de tipo de firma fue útil?
litb, ¿tu comentario fue dirigido a mí? si es así, no lo entiendo
Iraimbilanja sí :) FABS no puede devolver un número negativo, porque devuelve el valor absoluto de su argumento
Johannes Schaub - litb
Correcto. No pregunté cómo un flotador hipotético sin signo podría ayudar a la centralidad. Lo que pregunté fue: en qué situación Pipenbrinck encontró útil la verificación del tipo de firma Int (lo que lo llevó a buscar el mismo mecanismo para flotadores). con respecto a typesafety
1
Hay una microoptimización sin signo para la verificación de punto en el rango: ((sin signo) (p-min)) <(max-min), que solo tiene una rama, pero, como siempre, es mejor hacer un perfil para ver si realmente ayuda (lo usé principalmente en 386 núcleos, así que no sé cómo se las arreglan las CPU modernas).
Skizz

Respuestas:

114

La razón por la cual C ++ no tiene soporte para flotantes sin firmar es porque no hay operaciones de código de máquina equivalentes para que la CPU las ejecute. Por lo tanto, sería muy ineficiente apoyarlo.

Si C ++ lo admitiera, a veces estaría utilizando un flotador sin signo y sin darse cuenta de que su rendimiento acaba de ser eliminado. Si C ++ lo admitiera, entonces todas las operaciones de punto flotante tendrían que verificarse para ver si está firmado o no. Y para los programas que realizan millones de operaciones de coma flotante, esto no es aceptable.

Entonces, la pregunta sería por qué los implementadores de hardware no lo admiten. Y creo que la respuesta a eso es que no había un estándar flotante sin signo definido originalmente. Dado que a los idiomas les gusta ser compatibles con versiones anteriores, incluso si se agregaran idiomas, no podrían usarlo. Para ver las especificaciones de coma flotante, debe mirar el estándar 754 de coma flotante IEEE .

Sin embargo, puede evitar tener un tipo de coma flotante sin signo creando una clase flotante sin signo que encapsule un flotante o doble y arroje advertencias si intenta pasar un número negativo. Esto es menos eficiente, pero probablemente si no los está usando intensamente, no le importará esa ligera pérdida de rendimiento.

Definitivamente veo la utilidad de tener un flotador sin firmar. Pero C / C ++ tiende a elegir la eficiencia que funciona mejor para todos sobre la seguridad.

Brian R. Bondy
fuente
17
C / C ++ no requiere operaciones específicas de código de máquina para implementar el lenguaje. Los primeros compiladores de C / C ++ podrían generar código de coma flotante para el 386, ¡una CPU sin FPU! El compilador generaría llamadas a la biblioteca para emular las instrucciones de FPU. Por lo tanto, se podría hacer un ufloat sin soporte de CPU
Skizz
10
Skizz, si bien eso es correcto, Brian ya abordó esto: que debido a que no hay un código de máquina equivalente, el rendimiento será horrible en comparación.
Anthony
2
@Brian R. Bondy: Te perdí aquí: "porque no hay operaciones de código de máquina equivalentes para que la CPU ejecute ...". ¿Puedes explicar, en términos más simples?
Lazer
2
La razón por la cual OP quería soporte para flotantes sin firmar era para mensajes de advertencia, por lo que realmente no tiene nada que ver con la fase de generación de código del compilador, solo tiene que ver con cómo realiza la verificación de tipo de antemano, por lo que el soporte para ellos en el código de máquina es irrelevante y (como se ha agregado al final de la pregunta), las instrucciones normales de coma flotante podrían usarse para la ejecución real.
Joe F
2
No estoy seguro de ver por qué esto debería afectar el rendimiento. Al igual que con int's, toda la verificación de tipos relacionada con signos puede ocurrir en tiempo de compilación. OP sugiere que unsigned floatse implementaría regularmente floatcon comprobaciones en tiempo de compilación para garantizar que ciertas operaciones sin sentido nunca se realicen. El código y el rendimiento de la máquina resultante podrían ser idénticos, independientemente de si sus flotadores están firmados o no.
xanderflood
14

Hay una diferencia significativa entre enteros con y sin signo en C / C ++:

value >> shift

los valores con signo dejan el bit superior sin cambios (extensión de signo), los valores sin signo borran el bit superior.

La razón por la que no hay flotante sin signo es que rápidamente se encuentra con todo tipo de problemas si no hay valores negativos. Considera esto:

float a = 2.0f, b = 10.0f, c;
c = a - b;

¿Qué valor tiene c? -8. Pero, ¿qué significaría eso en un sistema sin números negativos? FLOAT_MAX - 8 tal vez? En realidad, eso no funciona ya que FLOAT_MAX - 8 es FLOAT_MAX debido a los efectos de precisión, por lo que las cosas son aún más complicadas. ¿Qué pasaría si fuera parte de una expresión más compleja?

float a = 2.0f, b = 10.0f, c = 20.0f, d = 3.14159f, e;
e = (a - b) / d + c;

Esto no es un problema para los enteros debido a la naturaleza del sistema de complemento de 2.

También considere las funciones matemáticas estándar: sin, cos y tan solo funcionarían para la mitad de sus valores de entrada, no podría encontrar el registro de valores <1, no podría resolver ecuaciones cuadráticas: x = (-b +/- root ( bb - 4.ac)) / 2.a, y así sucesivamente. De hecho, probablemente no funcionaría para ninguna función compleja, ya que tienden a implementarse como aproximaciones polinómicas que usarían valores negativos en alguna parte.

Entonces, los flotadores sin firmar son bastante inútiles.

Pero eso no quiere decir que una clase que verifica el rango de valores flotantes no sea útil, es posible que desee sujetar los valores a un rango dado, por ejemplo, cálculos RGB.

Skizz
fuente
@Skizz: si la representación es un problema, ¿quiere decir que si alguien puede idear un método para almacenar flotadores que sea tan eficiente como 2's complement, no habrá problema con tener flotadores sin signo?
Lazer
3
value >> shift for signed values leave the top bit unchanged (sign extend) ¿Estás seguro de eso? Pensé que era un comportamiento definido por la implementación, al menos para los valores con signo negativo.
Dan
@Dan: Acabo de ver el estándar reciente y, de hecho, indica que su implementación está definida; supongo que es solo en caso de que haya una CPU que no tenga desplazamiento a la derecha con la instrucción de extensión de signo.
Skizz
1
el punto flotante tradicionalmente satura (a - / + Inf) en lugar de envolver. Puede esperar que el desbordamiento de resta sin signo se sature 0.0, o posiblemente Inf o NaN. O simplemente sea Comportamiento indefinido, como el OP sugerido en una edición de la pregunta. Re: funciones trigonométricas: así que no defina versiones de entrada sin signo de siny así sucesivamente, y asegúrese de tratar su valor de retorno como firmado. La pregunta no era proponer reemplazar flotante por flotante sin signo, solo agregar unsigned floatcomo un nuevo tipo.
Peter Cordes
9

(Como comentario aparte, Perl 6 te permite escribir

subset Nonnegative::Float of Float where { $_ >= 0 };

y luego puede usarlo Nonnegative::Floatcomo lo haría con cualquier otro tipo).

No hay soporte de hardware para operaciones de coma flotante sin firmar, por lo que C no lo ofrece. C está diseñado principalmente para ser un "ensamblaje portátil", es decir, lo más cerca posible del metal sin estar atado a una plataforma específica.

[editar]

C es como un ensamblaje: lo que ves es exactamente lo que obtienes. Un implícito "Comprobaré que este flotador no sea negativo para usted" va en contra de su filosofía de diseño. Si realmente lo desea, puede agregar assert(x >= 0)o similar, pero debe hacerlo explícitamente.

efímero
fuente
svn.perl.org/parrot/trunk/languages/perl6/docs/STATUS dice que sí, pero of ...no analiza.
Ephemient
8

Creo que el int sin firmar se creó debido a la necesidad de un margen de valor mayor que el que se puede ofrecer.

Un flotador tiene un margen mucho mayor, por lo que nunca hubo una necesidad 'física' de un flotador sin signo. Y como usted mismo señala en su pregunta, la precisión adicional de 1 bit no es para nada.

Editar: Después de leer la respuesta de Brian R. Bondy , tengo que modificar mi respuesta: Definitivamente tiene razón en que las CPU subyacentes no tenían operaciones flotantes sin firmar. Sin embargo, mantengo mi creencia de que esta fue una decisión de diseño basada en las razones que dije anteriormente ;-)

Treb
fuente
2
Además, la suma y resta de enteros es el mismo con signo o sin signo: coma flotante, no tanto. ¿Quién haría el trabajo extra para soportar flotadores con y sin signo dada la utilidad marginal relativamente baja de tal característica?
Ephemient
7

Creo que Treb está en el camino correcto. Es más importante para los enteros que tenga un tipo correspondiente sin signo. Esos son los que se usan en el desplazamiento de bits y en los mapas de bits. . Un poco de señal se interpone en el camino. Por ejemplo, al desplazar hacia la derecha un valor negativo, el valor resultante es la implementación definida en C ++. Hacer eso con un entero sin signo o desbordamiento de ese tipo tiene una semántica perfectamente definida porque no hay tal cosa en el camino.

Entonces, al menos para los enteros, la necesidad de un tipo separado sin signo es más fuerte que solo dar advertencias. No es necesario tener en cuenta todos los puntos anteriores para las carrozas. Por lo tanto, creo que no hay una necesidad real de soporte de hardware para ellos, y C ya no los admitirá en ese momento.

Johannes Schaub - litb
fuente
5

Una raíz cuadrada definitivamente nunca devolverá un número negativo. También hay otros lugares donde un valor flotante negativo no tiene sentido. Candidato perfecto para una carroza sin firmar.

C99 admite números complejos y un tipo genérico de sqrt, por sqrt( 1.0 * I)lo que será negativo.


Los comentaristas destacaron un ligero brillo anterior, ya que me refería a la sqrtmacro genérica de tipo en lugar de a la función, y devolverá un valor de punto flotante escalar por truncamiento del complejo a su componente real:

#include <complex.h>
#include <tgmath.h>

int main () 
{
    complex double a = 1.0 + 1.0 * I;

    double f = sqrt(a);

    return 0;
}

También contiene un pedo cerebral, ya que la parte real del sqrt de cualquier número complejo es positiva o cero, y sqrt (1.0 * I) es sqrt (0.5) + sqrt (0.5) * I no -1.0.

Pete Kirkham
fuente
Sí, pero llama a una función con un nombre diferente si trabaja con números complejos. También el tipo de retorno es diferente. Buen punto sin embargo!
Nils Pipenbrinck
44
El resultado de sqrt (i) es un número complejo. Y dado que los números complejos no están ordenados, no se puede decir que un número complejo es negativo (es decir, <0)
quinmars
1
quinmars, seguro que no es csqrt? ¿o hablas de matemáticas en lugar de C? Estoy de acuerdo de todos modos que es un buen punto :)
Johannes Schaub - litb
De hecho, estaba hablando de matemáticas. Nunca he tratado con los números complejos en c.
quinmars
1
"raíz cuadrada definitivamente nunca devolverá un número negativo". -> a sqrt(-0.0)menudo produce -0.0. Por supuesto, -0.0 no es un valor negativo .
chux
4

Supongo que depende de que las especificaciones de punto flotante IEEE solo estén firmadas y que la mayoría de los lenguajes de programación las usen.

Artículo de Wikipedia sobre números de punto flotante IEEE-754

Editar: Además, como han señalado otros, la mayoría del hardware no admite flotadores no negativos, por lo que el tipo normal de flotadores es más eficiente ya que existe soporte de hardware.

Tobias Wärre
fuente
C se introdujo mucho antes de que apareciera el estándar IEEE-754
phuclv
@phuclv Ninguno de los dos era un hardware de punto flotante común. Fue adoptado en el estándar C "unos" años más tarde. Probablemente hay algo de documentación flotando en Internet al respecto. (Además, el artículo de wikipedia menciona C99).
Tobias Wärre
No entiendo lo que quieres decir. No hay "hardware" en su respuesta, y IEEE-754 nació después de C, por lo que los tipos de coma flotante en C no pueden depender del estándar IEEE-754, a menos que esos tipos se introdujeran en C mucho más tarde
phuclv
@phuclv C también se conocía como ensamblaje portátil, por lo que puede estar bastante cerca del hardware. Los lenguajes adquieren características a lo largo de los años, incluso si (antes de mi tiempo) el flotador se implementó en C, probablemente fue una operación basada en software y bastante costosa. Al momento de responder a esta pregunta, obviamente tenía una mejor comprensión de lo que estaba tratando de explicar que ahora. Y si observa la respuesta aceptada, puede entender por qué mencioné el estándar IEE754. Lo que no entiendo es que no has escogido una respuesta de 10 años que no es la aceptada.
Tobias Wärre
3

Creo que la razón principal es que los flotadores sin signo tendrían usos realmente limitados en comparación con las entradas sin signo. No creo que sea porque el hardware no lo admite. Los procesadores más antiguos no tenían capacidades de coma flotante, todo se emulaba en software. Si los flotadores no firmados fueran útiles, se habrían implementado primero en software y el hardware habría seguido su ejemplo.

Ferruccio
fuente
44
El PDP-7, la primera plataforma de C, tenía una unidad de punto flotante de hardware opcional. El PDP-11, la próxima plataforma de C, tenía flotadores de hardware de 32 bits. 80x86 llegó una generación más tarde, con algo de tecnología que estaba una generación atrás.
Ephemient
3

Los tipos enteros sin signo en C se definen de tal manera que obedecen las reglas de un anillo algebraico abstracto. Por ejemplo, para cualquier valor X e Y, agregar XY a Y producirá X. Se garantiza que los tipos enteros sin signo obedecerán estas reglas en todos los casos que no impliquen conversión hacia o desde cualquier otro tipo numérico [o tipos sin signo de diferentes tamaños] , y esa garantía es una de las características más importantes de estos tipos. En algunos casos, vale la pena renunciar a la capacidad de representar números negativos a cambio de las garantías adicionales que solo los tipos sin firmar pueden proporcionar. Los tipos de punto flotante, firmados o no, no pueden cumplir con todas las reglas de un anillo algebraico [por ejemplo, no pueden garantizar que X + YY sea igual a X], y de hecho IEEE no ' incluso les permite cumplir con las reglas de una clase de equivalencia [al exigir que ciertos valores se comparen desiguales entre sí]. No creo que un tipo de punto flotante "sin signo" pueda acatar ningún axioma que un tipo de punto flotante común no podría, así que no estoy seguro de las ventajas que ofrecería.

Super gato
fuente
1

IHMO se debe a que soportar los tipos de punto flotante con y sin signo en hardware o software sería demasiado problemático

Para los tipos enteros, podemos utilizar la misma unidad lógica tanto para operaciones enteras con signo como sin signo en la mayoría de las situaciones utilizando la propiedad agradable del complemento de 2, porque el resultado es idéntico en esos casos para las operaciones add, sub, non-widening mul y la mayoría de las operaciones bit a bit. Para las operaciones que diferencian entre versiones firmadas y no firmadas, aún podemos compartir la mayoría de la lógica . Por ejemplo

  • El cambio aritmético y lógico solo necesita un ligero cambio en el relleno para los bits superiores
  • La multiplicación de ampliación puede usar el mismo hardware para la parte principal y luego una lógica separada para ajustar el resultado y cambiar el signo . No es que se use en multiplicadores reales, pero es posible hacerlo
  • La comparación firmada se puede convertir en comparación sin firmar y viceversa fácilmente al alternar el bit superior o agregarINT_MIN . También teóricamente posible, probablemente no se usa en hardware, pero es útil en sistemas que admiten solo un tipo de comparación (como 8080 u 8051)

Los sistemas que usan el complemento 1 también solo necesitan una pequeña modificación a la lógica porque es simplemente el bit de transporte envuelto al bit menos significativo. No estoy seguro acerca de los sistemas de magnitud de signos, pero parece que usan el complemento de 1 internamente, por lo que se aplica lo mismo

Desafortunadamente , no tenemos ese lujo para los tipos de punto flotante. Simplemente liberando el bit de signo tendremos la versión sin firmar. Pero entonces, ¿para qué deberíamos usar ese bit?

  • Aumenta el rango agregándolo al exponente
  • Aumente la precisión agregándola a la mantisa. Esto suele ser más útil, ya que generalmente necesitamos más precisión que rango

Pero ambas opciones necesitan un sumador más grande para acomodar el rango de valores más amplio. Eso aumenta la complejidad de la lógica, mientras que la parte superior del sumador se encuentra allí sin usar la mayor parte del tiempo. Se necesitarán aún más circuitos para multiplicaciones, divisiones u otras operaciones complejas.

En los sistemas que utilizan software de punto flotante, necesita 2 versiones para cada función que no se esperaba durante el tiempo que la memoria era demasiado costosa, o tendría que encontrar una forma "complicada" de compartir partes de las funciones firmadas y sin firmar.

Sin embargo , el hardware de punto flotante existía mucho antes de que se inventara C. , por lo que creo que la elección en C se debió a la falta de soporte de hardware debido a la razón que mencioné anteriormente

Dicho esto, existen varios formatos especializados de punto flotante sin signo, principalmente para fines de procesamiento de imágenes, como el tipo de punto flotante de 10 y 11 bits del grupo Khronos

phuclv
fuente
0

Sospecho que se debe a que los procesadores subyacentes a los que se dirigen los compiladores de C no tienen una buena manera de tratar con números de coma flotante sin signo.

Brian Ensink
fuente
¿Los procesadores subyacentes tenían una buena forma de tratar con números de punto flotante con signo? C se estaba volviendo popular cuando los procesadores auxiliares de punto flotante eran idiosincrásicos y apenas universales.
David Thornley
1
No conozco todas las líneas de tiempo históricas, pero hubo un soporte de hardware emergente para flotadores firmados, aunque es raro como usted señala. Los diseñadores de idiomas podrían incorporar soporte para él, mientras que los backends del compilador tenían diferentes niveles de soporte dependiendo de la arquitectura de destino.
Brian Ensink
0

Buena pregunta.

Si, como usted dice, es solo para advertencias en tiempo de compilación y ningún cambio en su comportamiento, de lo contrario, el hardware subyacente no se ve afectado y, como tal, solo sería un cambio de C ++ / Compilador.

Me he preguntado lo mismo anteriormente, pero la cosa es: no ayudaría mucho. En el mejor de los casos, el compilador puede encontrar asignaciones estáticas.

unsigned float uf { 0 };
uf = -1f;

O minimalistamente más largo

unsigned float uf { 0 };
float f { 2 };
uf -= f;

Pero eso es todo. Con los tipos enteros sin signo también obtienes un entorno definido, es decir, se comporta como aritmética modular.

unsigned char uc { 0 };
uc -= 1;

después de esto 'uc' tiene el valor de 255.

Ahora, ¿qué haría un compilador con el mismo escenario dado un tipo flotante sin signo? Si los valores no se conocen en el momento de la compilación, necesitaría generar un código que primero ejecute los cálculos y luego realice una verificación de signos. Pero, ¿qué sucede cuando el resultado de tal cálculo sería "-5.5", qué valor debería almacenarse en un flotante declarado sin signo? Uno podría probar la aritmética modular como para los tipos integrales, pero eso viene con sus propios problemas: el valor más grande es indiscutiblemente infinito ... eso no funciona, no puede tener "infinito - 1". Ir por el mayor valor distintivo que puede contener tampoco funcionará realmente, ya que allí se encuentra con la precisión. "NaN" sería un candidato.

Por último, esto no sería un problema con los números de punto fijo ya que el módulo está bien definido.

ABaumstumpf
fuente