¿Por qué prefiere iniciar sesión sin firmar en C++?
optimization (13)
Me gustaría entender mejor por qué elegir int
sobre unsigned
?
Personalmente, nunca me han gustado los valores firmados a menos que haya una razón válida para ellos. por ejemplo, el recuento de elementos en una matriz, o la longitud de una cadena, o el tamaño del bloque de memoria, etc., por lo que a menudo estas cosas no pueden ser negativas. Tal valor no tiene ningún significado posible. ¿Por qué preferir int
cuando es engañoso en todos esos casos?
Pregunto esto porque tanto Bjarne Stroustrup como Chandler Carruth dieron el consejo de preferir int
unsigned
aquí (aproximadamente 12:30 '') .
Puedo ver el argumento para usar int
en short
o long
- int
es el ancho de datos "más natural" para la arquitectura de la máquina objetivo.
Pero firmar sin firmar siempre me ha molestado. ¿Los valores firmados son genuinamente más rápidos en las arquitecturas de CPU modernas típicas? ¿Qué los hace mejores?
Use
int
por defecto : se reproduce mejor con el resto del lenguaje- el uso del dominio más común es la aritmética regular, no la aritmética modular
-
int main() {} // see an unsigned?
-
auto i = 0; // i is of type int
Solo use
unsigned
para aritmética de módulo y twiddling de bit (en particular desplazamiento)- tiene una semántica diferente a la aritmética regular, asegúrese de que sea lo que quiere
- los tipos con signo de cambio de bit son sutiles (vea los comentarios de @ChristianRau)
- si necesita un vector> 2Gb en una máquina de 32 bits, actualice su SO / hardware
Nunca mezcle aritmética firmada y sin firmar
- las reglas para eso son complicated y sorprendentes (cualquiera de las dos se puede convertir a la otra, dependiendo de los tamaños de tipo relativos)
- encienda
-Wconversion -Wsign-conversion -Wsign-promo
(gcc es mejor que Clang aquí) - la Biblioteca estándar lo entendió mal con
std::size_t
(cita del video GN13) - use range-for si puede,
-
for(auto i = 0; i < static_cast<int>(v.size()); ++i)
si debe
No use tipos cortos o grandes a menos que realmente los necesite
- el flujo de datos de las arquitecturas actuales se adapta bien a los datos no punteros de 32 bits (pero tenga en cuenta el comentario de @BenVoigt sobre los efectos de caché para los tipos más pequeños)
-
char
yshort
espacio de guardar, pero sufren de promociones integrales - ¿realmente vas a contar para todo
int64_t
?
Da resultados inesperados al hacer operaciones aritméticas simples:
unsigned int i;
i = 1 - 2;
//i is now 4294967295 on a 64bit machine
Da resultados inesperados al hacer una comparación simple:
unsigned int j = 1;
std::cout << (j>-1) << std::endl;
//output 0 as false but 1 is greater than -1
Esto se debe a que al realizar las operaciones anteriores, los datos ingresados se convierten en sin firmar, y se desborda y llega a un número realmente grande.
El tipo int
se asemeja más al comportamiento de los enteros matemáticos que al tipo unsigned
.
Es ingenuo preferir el tipo unsigned
simplemente porque una situación no requiere la representación de valores negativos.
El problema es que el tipo unsigned
tiene un comportamiento discontinuo justo al lado de cero. Cualquier operación que intente calcular un pequeño valor negativo, en cambio produce un gran valor positivo. (Peor: uno que está definido por la implementación).
Las relaciones algebraicas como a < b
implican que a - b < 0
naufragan en el dominio sin firmar, incluso para valores pequeños como a = 3
b = 4
.
Un ciclo descendente como for (i = max - 1; i >= 0; i--)
no termina si se realiza sin signo.
Las peculiaridades sin firmar pueden causar un problema que afectará el código independientemente de si el código espera representar solo cantidades positivas.
La virtud de los tipos sin firmar es que ciertas operaciones que no están definidas de forma portátil en el nivel de bit para los tipos firmados son de esa manera para los tipos sin firmar. Los tipos sin firmar carecen de un bit de signo, por lo que cambiar y enmascarar a través del bit de signo no es un problema. Los tipos sin signo son buenos para las máscaras de bits, y para el código que implementa la aritmética precisa de una manera independiente de la plataforma. Las operaciones sin signo simularán la semántica del complemento de dos, incluso en una máquina complementaria que no sea de dos. Escribir una biblioteca de precisión múltiple (bignum) prácticamente requiere que los arreglos de tipos sin firmar se usen para la representación, en lugar de los tipos firmados.
Los tipos sin firmar también son adecuados en situaciones en las que los números se comportan como identificadores y no como tipos aritméticos. Por ejemplo, una dirección IPv4 se puede representar en un tipo sin signo de 32 bits. No agregaría direcciones IPv4 juntas.
La velocidad es la misma en las arquitecturas modernas. El problema con unsigned int
es que a veces puede generar un comportamiento inesperado. Esto puede crear errores que de lo contrario no aparecerían.
Normalmente, cuando restas 1 de un valor, el valor disminuye. Ahora, con las variables unsigned int
y unsigned int
, habrá un tiempo en que al restar 1 se crea un valor MUCHO MÁS GRANDE. La diferencia clave entre unsigned int
e int
es que con unsigned int
el valor que genera el resultado paradójico es un valor comúnmente utilizado --- 0 --- mientras que con la firma el número está lejos de las operaciones normales.
En cuanto a devolver -1 para un valor de error --- el pensamiento moderno es que es mejor emitir una excepción que probar valores devueltos.
Es verdad que si defiendes adecuadamente tu código no tendrás este problema, y si usas sin firmar religiosamente en todas partes, estarás bien (siempre que solo estés agregando, y nunca restando, y que nunca te acerques a MAX_INT). Yo uso unsigned int en todas partes. Pero se necesita mucha disciplina. Para muchos programas, puedes usar int
y dedicar tu tiempo a otros errores.
Los tipos integrales en C y muchos idiomas que se derivan de él tienen dos casos de uso generales: representar números o representar miembros de un anillo algebraico abstracto. Para aquellos que no están familiarizados con el álgebra abstracta, la noción principal detrás de un anillo es que sumar, restar o multiplicar dos elementos de un anillo debería producir otro elemento de ese anillo: no debería colapsar ni producir un valor fuera del anillo. En una máquina de 32 bits, agregar 0x12345678 sin signo al 0xFFFFFFFF sin signo no "desborda" - simplemente produce el resultado 0x12345677 que se define para el anillo de enteros congruentes mod 2 ^ 32 (porque el resultado aritmético de agregar 0x12345678 a 0xFFFFFFFF , es decir, 0x112345677, es congruente con 0x12345677 mod 2 ^ 32).
Conceptualmente, ambos propósitos (representar números o representar miembros del anillo de enteros congruentes mod 2 ^ n) pueden ser atendidos por tipos firmados y no firmados, y muchas operaciones son las mismas para ambos casos de uso, pero hay algunas diferencias. Entre otras cosas, no se debe esperar que un intento de agregar dos números produzca nada más que la suma aritmética correcta. Si bien es discutible si un lenguaje debe ser requerido para generar el código necesario para garantizar que no lo hará (por ejemplo, que se lanzaría una excepción), se podría argumentar que para el código que usa tipos integrales para representar números , sería preferible dicho comportamiento para obtener un valor aritmético incorrecto y no debe prohibirse a los compiladores comportarse de esa manera.
Los implementadores de los estándares C decidieron usar tipos enteros con signo para representar números y tipos sin signo para representar a los miembros del anillo algebraico de enteros mod congruente 2 ^ n. Por el contrario, Java usa enteros con signo para representar a los miembros de dichos anillos (aunque se interpretan de manera diferente en algunos contextos, las conversiones entre tipos de signo de diferente tamaño, por ejemplo, se comportan de forma diferente entre los no firmados) y Java no tiene números enteros ni ninguno tipos integrales primitivos que se comportan como números en todos los casos no excepcionales.
Si un idioma proporcionó una selección de representaciones firmadas y no firmadas para números y números de anillo algebraico, podría tener sentido utilizar números sin signo para representar cantidades que siempre serán positivas. Sin embargo, si los únicos tipos sin firmar representan a los miembros de un anillo algebraico, y los únicos tipos que representan números son los firmados, incluso si un valor siempre será positivo, debe representarse utilizando un tipo diseñado para representar números.
Incidentalmente, la razón por la que (uint32_t) -1 es 0xFFFFFFFF surge del hecho de que convertir un valor firmado a sin firmar equivale a agregar cero sin signo, y agregar un entero a un valor sin signo se define como sumar o restar su magnitud a / del valor sin signo de acuerdo con las reglas del anillo algebraico que especifican que si X = YZ, entonces X es el único miembro de ese anillo, tal X + Z = Y. En matemática sin signo, 0xFFFFFFFF es el único número que, cuando se agrega al 1 sin signo, produce el cero sin signo.
Muchas rasones:
La aritmética en
unsigned
siempre da como resultado unsigned, que puede ser un problema al restar cantidades enteras que pueden dar como resultado un resultado negativo, piense restar cantidades de dinero para producir saldo, o índices de matriz para ceder la distancia entre los elementos. Si los operandos no están firmados, obtienes un resultado perfectamente definido, pero casi seguro sin sentido, y una comparación deresult < 0
siempre será falsa (de la cual los compiladores modernos te advertirán afortunadamente).unsigned
tiene la desagradable propiedad de contaminar la aritmética donde se mezcla con enteros con signo. Entonces, si agrega un firmado y sin firmar y pregunta si el resultado es mayor que cero, puede ser mordido, especialmente cuando el tipo integral sin signo está oculto detrás de untypedef
.
No hay motivos para preferir la signed
unsigned
, aparte de las puramente sociológicas, es decir, algunas personas creen que los programadores promedio no son lo suficientemente competentes y / o atentos para escribir el código correcto en términos de tipos unsigned
. Este es a menudo el principal razonamiento utilizado por varios "hablantes", independientemente de cuán respetados sean esos hablantes.
En realidad, los programadores competentes desarrollan rápidamente y / o aprenden el conjunto básico de modismos y habilidades de programación que les permiten escribir el código correcto en términos de tipos integrales sin firmar.
Tenga en cuenta también que las diferencias fundamentales entre la semántica con signo y sin signo siempre están presentes (en forma superficialmente diferente) en otras partes del lenguaje C y C ++, como la aritmética del puntero y la aritmética del iterador. Lo que significa que, en general, el programador no tiene la opción de evitar tratar problemas específicos de la semántica sin firmar y los "problemas" que trae consigo. Es decir, lo quieras o no, debes aprender a trabajar con rangos que terminan abruptamente en su extremo izquierdo y terminan aquí (no en algún lugar en la distancia), incluso si evitas firmemente los enteros unsigned
.
Además, como probablemente sepa, muchas partes de la biblioteca estándar ya dependen bastante de los tipos de enteros unsigned
signo. Forzar la aritmética con signo en la mezcla, en lugar de aprender a trabajar con uno sin signo, solo dará como resultado un código desastrosamente malo.
La única razón real para preferir signed
en algunos contextos que viene a la mente es que en enteros mixtos / código de coma flotante, los formatos enteros signed
son soportados directamente por el conjunto de instrucciones FPU, mientras que los formatos unsigned
no son compatibles, haciendo que el compilador genere código adicional para conversiones entre valores de punto flotante y valores unsigned
. En dicho código, los tipos signed
pueden tener un mejor rendimiento.
Pero, al mismo tiempo, en el código puramente entero, los tipos unsigned
pueden tener un mejor rendimiento que los tipos signed
. Por ejemplo, la división entera a menudo requiere un código correctivo adicional para cumplir los requisitos de la especificación del idioma. La corrección solo es necesaria en el caso de operandos negativos, por lo que desperdicia ciclos de CPU en situaciones en las que los operandos negativos no se usan realmente.
En mi práctica, me adhiero sigilosamente a unsigned
siempre que puedo, y uso signed
solo si realmente tengo que hacerlo.
Para mí, además de todos los enteros en el rango de 0 .. + 2,147,483,647 contenidos dentro del conjunto de enteros con y sin signo en arquitecturas de 32 bits, hay una mayor probabilidad de que necesite usar -1 (o más pequeño) que necesita usar +2,147,483,648 (o más).
Para responder a la pregunta real: por la gran cantidad de cosas, realmente no importa. int
puede ser un poco más fácil de tratar con cosas como la resta con el segundo operando más grande que el primero y todavía obtienes un resultado "esperado".
No hay absolutamente ninguna diferencia de velocidad en el 99.9% de los casos, porque las ÚNICAS instrucciones que son diferentes para los números con y sin firma son:
- Haciendo que el número sea más largo (complete con el signo para firmado o cero para sin firmar) - se requiere el mismo esfuerzo para hacer ambas cosas.
- Comparaciones: un número firmado, el procesador debe tener en cuenta si alguno de los números es negativo o no. Pero, una vez más, es la misma velocidad para hacer una comparación con números firmados o no firmados: solo se usa un código de instrucción diferente para decir "los números que tienen el bit más alto establecido son más pequeños que los números con el bit más alto no establecido" (esencialmente). [Pedagógicamente, casi siempre es la operación que utiliza el RESULTADO de una comparación que es diferente, el caso más común es un salto condicional o instrucción de bifurcación, pero de cualquier manera, es el mismo esfuerzo, solo que las entradas se toman para significar cosas ligeramente diferentes ]
- Multiplicar y dividir. Obviamente, la conversión de signo del resultado debe suceder si se trata de una multiplicación firmada, donde un signo sin signo no debe cambiar el signo del resultado si se establece el bit más alto de una de las entradas. Y nuevamente, el esfuerzo es (tan cerca como nos importa) idéntico.
(Creo que hay uno o dos casos más, pero el resultado es el mismo; no importa si está firmado o no, el esfuerzo para realizar la operación es el mismo para ambos).
Permítanme parafrasear el video, como lo dijeron los expertos de manera sucinta.
Andrei Alexandrescu :
- No hay una guía simple.
- En la programación de sistemas, necesitamos enteros de diferentes tamaños y firmados.
- Muchas conversiones y reglas arcanas gobiernan la aritmética (como para el
auto
), así que debemos ser cuidadosos.Chandler Carruth :
- Aquí hay algunas pautas simples:
- Use enteros con signo a menos que necesite una aritmética de complemento de dos o un patrón de bits
- Usa el entero más pequeño que sea suficiente.
- De lo contrario, use
int
si cree que puede contar los elementos, y un entero de 64 bits si es incluso más de lo que desea contar.- Deje de preocuparse y use herramientas para decirle cuándo necesita un tipo o tamaño diferente.
Bjarne Stroustrup :
- Use
int
hasta que tenga una razón para no hacerlo.- Use sin signo solo para patrones de bits.
- Nunca mezcle firmado y sin firmar
La cautela sobre las reglas de firma se deja de lado, mi frase de una frase quita a los expertos:
Use el tipo apropiado, y cuando no lo sepa, use un
int
hasta que lo sepa.
Según solicitudes en comentarios: prefiero int
lugar de unsigned
porque ...
es más corto (¡lo digo en serio!)
es más genérico y más intuitivo (es decir, me gusta suponer que
1 - 2
es -1 y no un gran número oscuro)¿Qué sucede si quiero señalar un error devolviendo un valor fuera de rango?
Por supuesto que hay contraargumentos, pero estos son los motivos principales por los que me gusta declarar mis enteros como int
lugar de unsigned
. Por supuesto, esto no siempre es así, en otros casos, un unsigned
es solo una mejor herramienta para una tarea, solo estoy respondiendo la pregunta "¿por qué alguien preferiría fallar al firmar?" Específicamente.
Una buena razón por la que puedo pensar es en caso de detectar desbordamiento.
Para los casos de uso, como el recuento de elementos en una matriz, la longitud de una cadena o el tamaño del bloque de memoria, puede desbordar una int sin firmar y es posible que no note una diferencia incluso cuando eche un vistazo a la variable. Si es un int firmado, la variable será menor que cero y claramente incorrecta.
Simplemente puede verificar si la variable es cero cuando desea usarla. De esta manera, no es necesario comprobar el desbordamiento después de cada operación aritmética, como es el caso de las notas sin signo.
int
es preferido porque es el más comúnmente utilizado. unsigned
generalmente se asocia con operaciones de bits. Cada vez que veo un unsigned
, asumo que se usa para dar vueltas.
Si necesita un rango mayor, use un entero de 64 bits.
Si está iterando sobre cosas usando índices, los tipos generalmente tienen size_type
, y no debería importar si está firmado o sin firmar.
La velocidad no es un problema.