¿Cuál es la diferencia de rendimiento entre enteros sin signo y con signo? [cerrado]


42

Soy consciente del éxito en el rendimiento al mezclar entradas firmadas con flotantes.

¿Es peor mezclar ints sin firmar con flotadores?

¿Hay algún golpe al mezclar con signo / sin signo sin flotadores?

¿Los diferentes tamaños (u32, u16, u8, i32, i16, i8) tienen algún efecto en el rendimiento? ¿En qué plataformas?


2
Eliminé el texto / etiqueta específico de PS3, porque esta es una buena pregunta sobre cualquier arquitectura, y la respuesta es válida para todas las arquitecturas que separan registros enteros y de coma flotante, que es prácticamente todos.

Respuestas:


36

La gran penalización de mezclar ints (de cualquier tipo) y flotantes se debe a que están en diferentes conjuntos de registros. Para pasar de un conjunto de registros a otro, debe escribir el valor en la memoria y volver a leerlo, lo que incurre en un bloqueo de carga-golpe-tienda .

Ir entre diferentes tamaños o firmas de entradas mantiene todo en el mismo conjunto de registros, por lo que evita la gran penalización. Puede haber penalizaciones menores debido a las extensiones de señal, etc., pero estas son mucho más pequeñas que una tienda de carga y descarga.


El artículo que vinculó afirma que el procesador de celda PS3 es una excepción a esto porque aparentemente todo se almacena en el mismo conjunto de registros (se puede encontrar aproximadamente en el medio del artículo o busque "Celda").
bummzack

44
@bummzack: Eso se aplica solo a los SPEs, no al PPE; los SPE tienen un entorno de coma flotante muy, muy especial, y el reparto sigue siendo relativamente caro. Además, los costos siguen siendo los mismos para enteros con y sin signo.

Es un buen artículo y es importante saber sobre LHS (y lo votaré por eso), pero mi pregunta es sobre las sanciones relacionadas con los signos. Sé que estos son pequeños y probablemente insignificantes, pero todavía me gustaría ver algunos números reales o referencias sobre ellos.
Luis

1
@Luis: estaba tratando de encontrar documentación pública sobre esto, pero no puedo encontrarla en este momento. Si tiene acceso a la documentación de Xbox360, hay un buen documento de Bruce Dawson que cubre algo de esto (y es muy bueno en general).
celion

@Luis: He publicado un análisis a continuación, pero si te satisface, por favor dale la respuesta a Celion: todo lo que dijo es correcto, todo lo que hice fue ejecutar GCC varias veces.

12

Sospecho que la información sobre Xbox 360 y PS3 específicamente va a estar detrás de muros solo para desarrolladores con licencia, como la mayoría de los detalles de bajo nivel. Sin embargo, podemos construir un programa x86 equivalente y desarmarlo para tener una idea general.

Primero, veamos qué costos de ampliación sin firmar:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

La parte relevante se desmonta en (usando GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Básicamente lo mismo: en un caso, movemos un byte, en el otro, movemos una palabra. Próximo:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Se convierte en:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

Por lo tanto, el costo de la extensión del signo es el costo de movsblmás que el movzblnivel de subinstrucción. Eso es básicamente imposible de cuantificar en los procesadores modernos debido a la forma en que funcionan los procesadores modernos. Todo lo demás, desde la velocidad de la memoria hasta el almacenamiento en caché de lo que estaba en la tubería de antemano, dominará el tiempo de ejecución.

En los ~ 10 minutos que me llevó escribir estas pruebas, pude encontrar fácilmente un error de rendimiento real, y tan pronto como enciendo cualquier nivel de optimización del compilador, el código se vuelve irreconocible para tareas tan sencillas.

Esto no es Stack Overflow, por lo que espero que nadie aquí diga que la microoptimización no importa. Los juegos a menudo trabajan en datos que son muy grandes y muy numéricos, por lo que la atención cuidadosa a la ramificación, los lanzamientos, la programación, la alineación de la estructura, etc. puede proporcionar mejoras muy importantes. Cualquiera que haya pasado mucho tiempo optimizando el código PPC probablemente tenga al menos una historia de terror sobre las tiendas de carga y descarga. Pero en este caso, realmente no importa. El tamaño de almacenamiento de su tipo entero no afecta el rendimiento, siempre que esté alineado y quepa en un registro.


2
(CW porque esto es realmente sólo un comentario sobre la respuesta de celion, y porque soy curioso lo alteraciones del código de la gente podría tener que hacerlo más ilustrativo.)

La información sobre la CPU de PS3 está disponible de forma inmediata y legal, por lo que no es un problema hablar de cosas de CPU relacionadas con PS3. Hasta que Sony eliminó el soporte de OtherOS, cualquiera podía pegar Linux en una PS3 y programarlo. La GPU estaba fuera de los límites, pero la CPU (incluidos los SPE) están bien. Incluso sin el soporte de OtherOS, puede obtener fácilmente el GCC apropiado y ver cómo es el code-gen.
JasonD 01 de

@ Jason: Marqué mi publicación como CW, por lo que si alguien hace esto, puede proporcionar la información. Sin embargo, cualquier persona con acceso al compilador oficial de GameOS de Sony, que es realmente el único que importa, probablemente tenga prohibido hacerlo.

En realidad, el entero firmado es más caro en PPC IIRC. Tiene un pequeño impacto en el rendimiento, pero está ahí ... también muchos de los detalles de PS3 PPU / SPU están aquí: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html y aquí: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . ¿Curioso qué es este compilador de GameOS? ¿Es ese el compilador GCC o el SNC? Además de las cosas mencionadas anteriormente, las comparaciones firmadas tienen una sobrecarga cuando se habla de optimizar los bucles más internos. No tengo acceso a los documentos que describen esto, sin embargo - e incluso si lo hiciera ...
jheriko

4

Las operaciones de enteros firmados pueden ser más caras en casi todas las arquitecturas. Por ejemplo, la división por una constante es más rápida cuando no está firmada, por ejemplo:

unsigned foo(unsigned a) { return a / 1024U; }

se optimizará para:

unsigned foo(unsigned a) { return a >> 10; }

Pero...

int foo(int a) { return a / 1024; }

se optimizará para:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

o en sistemas donde la ramificación es barata,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

Lo mismo vale para el módulo. Esto también es válido para los no poderes de 2 (pero el ejemplo es más complejo). Si su arquitectura no tiene una división de hardware (por ejemplo, la mayoría de ARM), las divisiones sin firma de los no concursos también son más rápidas.

En general, decirle al compilador que los números negativos no pueden resultar ayudará a la optimización de las expresiones, especialmente las utilizadas para la terminación de bucles y otras condiciones.

En cuanto a las entradas de diferentes tamaños, sí, hay un ligero impacto, pero tendrías que sopesar eso frente a mover menos memoria. En estos días, probablemente gane más al acceder a menos memoria de la que pierde con la expansión de tamaño. Estás muy lejos de la microoptimización en ese momento.


Edité su código optimizado para reflejar mejor lo que realmente genera GCC, incluso en -O0. Tener una rama fue engañoso cuando un test + lea le permite hacerlo sin ramas.

2
En x86, tal vez. En ARMv7 solo se ejecuta condicionalmente.
John Ripley

3

Las operaciones con int firmado o sin firmar tienen el mismo costo en los procesadores actuales (x86_64, x86, powerpc, arm). En el procesador de 32 bits, u32, u16, u8 s32, s16, s8 deben ser iguales. Puede tener penalidad con mala alineación.

Pero convertir int a float o float a int es una operación costosa. Puede encontrar fácilmente una implementación optimizada (SSE2, Neon ...).

El punto más importante es probablemente el acceso a la memoria. Si sus datos no caben en el caché L1 / L2, perderá más ciclos que la conversión.


2

Jon Purdy dice anteriormente (no puedo comentar) que sin firmar podría ser más lento porque no puede desbordarse. No estoy de acuerdo, la aritmética sin signo es simple aritmética moular módulo 2 con respecto al número de bits en la palabra. Las operaciones firmadas en principio pueden sufrir desbordamientos, pero generalmente están apagadas.

A veces puede hacer cosas inteligentes (pero no muy legibles) como empacar dos o más elementos de datos en un int y obtener múltiples operaciones por instrucción (aritmética de bolsillo). Pero tienes que entender lo que estás haciendo. Por supuesto, MMX te permite hacer esto naturalmente. Pero a veces, usar el tamaño de palabra compatible con HW más grande y empaquetar manualmente los datos le brinda la implementación más rápida.

Tenga cuidado con la alineación de datos. En la mayoría de las implementaciones de hardware, las cargas y tiendas no alineadas son más lentas. La alineación natural significa que, por ejemplo, una palabra de 4 bytes, la dirección es un múltiplo de cuatro, y las direcciones de palabras de ocho bytes deben ser múltiplos de ocho bytes. Esto se traslada a SSE (128 bits favorece la alineación de 16 bytes). AVX pronto extenderá estos tamaños de registro "vector" a 256 bits y luego a 512 bits. Y las cargas / tiendas alineadas serán más rápidas que las no alineadas. Para geeks HW, una operación de memoria no alineada puede abarcar cosas como la línea de caché e incluso los límites de página, por lo que HW debe tener cuidado.


1

Es un poco mejor usar enteros con signo para los índices de bucle, porque el desbordamiento con signo no está definido en C, por lo que el compilador supondrá que dichos bucles tienen menos casos de esquina. Esto se controla mediante el "-fstrict-overflow" de gcc (habilitado por defecto) y el efecto es probablemente difícil de notar sin leer la salida del ensamblaje.

Más allá de eso, x86 funciona mejor si no mezcla tipos, porque puede usar operandos de memoria. Si tiene que convertir tipos (signo o extensiones cero), eso significa una carga explícita y el uso de un registro.

Siga con int para las variables locales y la mayoría de esto sucederá de forma predeterminada.


0

Como señala Celion, la sobrecarga de convertir entre ints y flotantes tiene que ver en gran medida con la copia y conversión de valores entre registros. La única sobrecarga de entradas sin firmar en sí mismas proviene de su comportamiento envolvente garantizado, que requiere una cierta cantidad de comprobación de desbordamiento en el código compilado.

Básicamente no hay gastos generales en la conversión entre enteros con y sin signo. Los diferentes tamaños de enteros pueden ser (infinitamente) de acceso más rápido o más lento dependiendo de la plataforma. En términos generales, el tamaño del número entero más cercano al tamaño de la palabra de la plataforma será el más rápido de acceder, pero la diferencia de rendimiento general depende de muchos otros factores, especialmente el tamaño de la caché: si lo usa uint64_tcuando todo lo que necesita es uint32_t, puede sea ​​que una menor cantidad de sus datos va a caber en la memoria caché a la vez, y puede incurrir en una sobrecarga de carga.

Sin embargo, es un poco excesivo pensar en esto. Si usa tipos que son apropiados para sus datos, las cosas deberían funcionar perfectamente bien, y la cantidad de energía que se obtendrá al seleccionar tipos basados ​​en la arquitectura es insignificante de todos modos.


¿A qué comprobación de desbordamiento te refieres? A menos que se refiera a un nivel inferior al ensamblador, el código para agregar dos entradas es idéntico en la mayoría de los sistemas, y en realidad no es más largo en los pocos que usan, por ejemplo, magnitud de signo. Sólo diferente.

@ JoeWreschnig: Maldición. Parece que no puedo encontrarlo, pero sé que he visto ejemplos de diferentes resultados de ensamblador que explican el comportamiento envolvente definido, al menos en ciertas plataformas. La única publicación relacionada que pude encontrar: stackoverflow.com/questions/4712315/…
Jon Purdy

La salida de ensamblador diferente para un comportamiento envolvente diferente se debe a que el compilador puede hacer optimizaciones en el caso firmado que, por ejemplo, si b> 0, entonces a + b> a, porque el desbordamiento firmado no está definido (y por lo tanto no se puede confiar en él). Es realmente una situación totalmente diferente.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.