Restar / sumar saturación para bytes sin firmar

83

Imagina que tengo dos bytes sin firmar by x. Necesito calcular bsubcomo b - xy baddcomo b + x. Sin embargo, no quiero que se produzca subdesbordamiento / desbordamiento durante estas operaciones. Por ejemplo (pseudocódigo):

b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254

y

b = 250; x = 10;
badd = b + x; // badd must be 255, not 4

La forma obvia de hacer esto incluye la ramificación:

bsub = b - min(b, x);
badd = b + min(255 - b, x);

Me pregunto si hay mejores formas de hacer esto, es decir, mediante algunas manipulaciones de bits hacky.

ovk
fuente
13
y ^ ((x ^ y) & -(x < y))para inttipos se evalúa min(x, y)sin ramificar. Esto podría formar parte de una solución eventual, basada en lo que tiene hasta ahora.
Betsabé
3
¿Quizás entero incremental fijo? es útil.
Shafik Yaghmour
8
¿Es esta una pregunta de C o C ++? Por favor elige uno.
fuz
9
@AlanCampbell se llama Aritmética de saturación .
Shafik Yaghmour
7
¿Necesitas que sea portátil? Porque si está mirando una arquitectura específica, probablemente haya una buena instrucción única. Sé que ARM tiene sumas y restas vectoriales de saturación de bytes. En X86, el _mm_adds_epi8intrínseco hará una suma saturada de 16 bytes en una sola instrucción.
porglezomp

Respuestas:

86

El artículo Aritmética de saturación sin ramas proporciona estrategias para esto:

Su solución de adición es la siguiente:

u32b sat_addu32b(u32b x, u32b y)
{
    u32b res = x + y;
    res |= -(res < x);

    return res;
}

modificado para uint8_t:

uint8_t  sat_addu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x + y;
    res |= -(res < x);

    return res;
}

y su solución de resta es:

u32b sat_subu32b(u32b x, u32b y)
{
    u32b res = x - y;
    res &= -(res <= x);

    return res;
}

modificado para uint8_t:

uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x - y;
    res &= -(res <= x);

    return res;
}
Shafik Yaghmour
fuente
2
@ user1969104 ese puede ser el caso, pero como indica el comentario en el artículo, eso se resuelve lanzando a unsigned antes de aplicar unary minus. En la práctica , es poco probable que tenga que lidiar con otra cosa que no sea el complemento de dos .
Shafik Yaghmour
2
Esta puede ser una buena respuesta en C, pero no es una muy buena respuesta en C ++.
Yakk - Adam Nevraumont
4
@Yakk ¿Qué hace que esta sea una "mala" respuesta de C ++? Estas son operaciones matemáticas básicas, y no veo cómo se interpretaría como solo C o como C ++ incorrecto.
JPhi1618
4
@ JPhi1618 ¿Una mejor respuesta de C ++ podría ser template<class T>struct sat{T t;};con operadores sobrecargados que saturan? Uso adecuado de los espacios de nombres. Sobre todo azúcar.
Yakk - Adam Nevraumont
6
@Yakk, Ah, está bien. Solo vi esto como un ejemplo mínimo de que el OP podría adaptarse según sea necesario. No esperaría ver una implementación tan completa. Gracias por aclararlo.
JPhi1618
40

Un método simple es detectar el desbordamiento y restablecer el valor en consecuencia como se muestra a continuación

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

GCC puede optimizar la verificación de desbordamiento en una asignación condicional al compilar con -O2.

Medí la cantidad de optimización en comparación con otras soluciones. Con más de 1000000000 operaciones en mi PC, esta solución y la de @ShafikYaghmour promediaron 4.2 segundos, y la de @chux promedió 4.8 segundos. Esta solución también es más legible.

user1969104
fuente
5
@ user694733 No está optimizado, está optimizado en una asignación condicional dependiendo de la bandera de acarreo.
Salida el
2
Sí, user694733 tiene razón. Está optimizado en una asignación condicional.
user1969104
Esto no funcionaría para todos los casos, por ejemplo badd: b = 155 x = 201, que badd = 156, y eso es mayor que b. Debería comparar el resultado con el mínimo () o máximo () de las dos variables, dependiendo de la operación
Cristian F
@CristianF ¿Cómo se calcula 155 + 201 = 156? Creo que debe ser 155 + 201 = 356% 256 = 100. No creo que min (), max () sea necesario en ninguna combinación de valores b, x.
user1969104
16

Para restar:

diff = (a - b)*(a >= b);

Adición:

sum = (a + b) | -(a > (255 - b))

Evolución

// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too

Gracias a @R_Kapp

Gracias a @NathanOliver

Este ejercicio muestra el valor de simplemente codificar.

sum = b + min(255 - b, a);
chux - Restablecer a Monica
fuente
¿Por sumquizás (a + b) | -(a <= (255 - b))?
R_Kapp
Usted podría hacer sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF, asumiendo sizeof(int) > sizeof(unsigned char), pero esto se ve tan compleja que no sé si ganaría nada con ella (que no sea el dolor de cabeza).
user694733
@ user694733 Sí y tal vez incluso (a+b+1)*(a <= (255-b)) - 1.
chux - Restablecer a Monica
@NathanOliver Gracias por la supervisión; el aspecto revelador de esto es que subfue fácil como lo fue el límite 0. Pero otros límites plantean complicaciones y siguen el comentario del usuario2079303 .
chux - Reincorporar a Monica
1
@ user1969104 OP no estaba claro sobre "mejor" (espacio de código frente a rendimiento de velocidad) ni la plataforma de destino ni el compilador. La evaluación de la velocidad tiene más sentido en el contexto de un problema mayor no publicado.
chux - Reincorporar a Monica
13

Si está usando una versión lo suficientemente reciente de gcc o clang (tal vez también algunas otras), puede usar incorporados para detectar desbordamientos.

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}
erebos
fuente
Esta es la mejor respuesta. El uso de componentes integrados del compilador en lugar de la magia de bits no solo es más rápido, también es más claro y hace que el código sea más fácil de mantener.
Cefalópodo
Gracias @erebos. Definitivamente intentaré esto en plataformas donde esté disponible.
ovk
3
No puedo hacer que gcc genere código sin brach con este, lo cual es un poco decepcionante. Lo especialmente desafortunado aquí es que clang usa diferentes nombres para estos .
Shafik Yaghmour
1
@Cephalopod Y es completamente no multiplataforma, diablos, lo más probable es que ni siquiera funcione en otro compilador. No es una buena solución para el siglo XXI.
Ela782
1
@ Ela782 Es exactamente al revés: los dispositivos integrados no son una buena solución para el siglo XX. ¡Bienvenido al futuro!
Cefalópodo
3

Por adición:

unsigned temp = a+b;  // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);

Para restar:

unsigned temp = a-b;  // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);

No se requieren operadores de comparación ni multiplicaciones.

Super gato
fuente
3

Si está dispuesto a usar ensamblaje o intrínsecos, creo que tengo una solución óptima.

Para restar:

Podemos usar la sbbinstrucción

En MSVC podemos usar la función intrínseca _subborrow_u64 (también disponible en otros tamaños de bits).

Así es como se usa:

// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);

Así es como podríamos aplicarlo a su situación.

uint64_t sub_no_underflow(uint64_t a, uint64_t b){
    uint64_t result;
    borrow_flag = _subborrow_u64(0, a, b, &result);
    return result * !borrow_flag;
}

Por adición:

Podemos usar la adcxinstrucción

En MSVC podemos usar la función intrínseca _addcarry_u64 (también disponible en otros tamaños de bits).

Así es como se usa:

// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);

Así es como podríamos aplicarlo a su situación.

uint64_t add_no_overflow(uint64_t a, uint64_t b){
    uint64_t result;
    carry_flag = _addcarry_u64(0, a, b, &result);
    return !carry_flag * result - carry_flag;
}

No me gusta tanto este como el de resta, pero creo que es bastante ingenioso.

Si el complemento se desborda, carry_flag = 1. Not-ing carry_flagproduce 0, por lo que !carry_flag * result = 0cuando hay desbordamiento. Y dado 0 - 1que establecerá el valor integral sin firmar a su máximo, la función devolverá el resultado de la suma si no hay acarreo y devolverá el máximo del valor integral elegido si hay acarreo.

MichaelMitchell
fuente
1
Es posible que desee mencionar que esta respuesta es para una arquitectura de conjunto de instrucciones específica (¿x86?) Y requerirá una reimplementación para cada arquitectura de destino (SPARC, MIPS, ARM, etc.)
Toby Speight
2

que hay de esto:

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

fuente
Arreglé el error tipográfico (¿obvio?), Pero todavía no creo que sea correcto.
Betsabé
Esto también incluye la ramificación.
fuz
Eliminaré esta respuesta solo una pregunta rápida en ensamblaje sin optimización, ¿cuál es la diferencia entre el operador ternario y la declaración if / else?
@GRC No hay diferencia.
fuz
@GRC FUZxxl tiene razón, pero, como siempre, pruébelo usted mismo. Incluso si no sabe el montaje (podría hacer una pregunta aquí en SO si algo no le queda claro), simplemente verificando la longitud / instrucciones que sabrá.
edmz
2

Todo se puede hacer en aritmética de bytes sin firmar

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;
Yves Daoust
fuente
1
En realidad, esta es una de las mejores soluciones. Todos los demás que hacen la resta o la suma antes en realidad están creando un comportamiento indefinido en C ++, lo que hace que el compilador pueda hacer lo que quiera. En la práctica, puede predecir principalmente lo que sucederá, pero aún así.
Adrien Hamelin
2

Si desea hacer esto con dos bytes, use el código más simple posible.

Si desea hacer esto con veinte mil millones de bytes, verifique qué instrucciones vectoriales están disponibles en su procesador y si pueden usarse. Puede encontrar que su procesador puede realizar 32 de estas operaciones con una sola instrucción.

gnasher729
fuente
2

También puede usar la biblioteca numérica segura en Boost Library Incubator . Proporciona reemplazos directos para int, long, etc., lo que garantiza que nunca obtendrá un desbordamiento, un desbordamiento, etc., no detectados.

Robert Ramey
fuente
7
Proporcionar un ejemplo de cómo usar la biblioteca haría que esta sea una mejor respuesta. Además, ¿ofrecen una garantía sin brazo?
Shafik Yaghmour
La biblioteca tiene una amplia documentación y ejemplos. Pero al final del día, es tan fácil como incluir el encabezado apropiado y sustituir safe <int> por int.
Robert Ramey
sin ramas? Supongo que eres un hombre sin ramas. La biblioteca utiliza una plantilla de metaprogramación para incluir comprobaciones de tiempo de ejecución solo cuando sea necesario. Por ejemplo, unsigned char times unsigned char dará como resultado unsigned int. Esto nunca puede desbordarse, por lo que no es necesario realizar ninguna verificación. Por otro lado, los tiempos sin firmar sin firmar pueden desbordarse, por lo que debe comprobarse en tiempo de ejecución.
Robert Ramey
1

Si llama mucho a esos métodos, la forma más rápida no sería la manipulación de bits, sino probablemente una tabla de búsqueda. Defina una matriz de longitud 511 para cada operación. Ejemplo de menos (resta)

static unsigned char   maxTable[511];
memset(maxTable, 0, 255);           // If smaller, emulates cutoff at zero
maxTable[255]=0;                    // If equal     - return zero
for (int i=0; i<256; i++)
    maxTable[255+i] = i;            // If greater   - return the difference

La matriz es estática y se inicializa solo una vez. Ahora su resta se puede definir como método en línea o usando el precompilador:

#define MINUS(A,B)    maxTable[A-B+255];

¿Cómo funciona? Bueno, desea calcular previamente todas las posibles sustracciones de caracteres sin firmar. Los resultados varían de -255 a +255, total de 511 resultados diferentes. Definimos una matriz de todos los resultados posibles, pero como en C no podemos acceder a ella desde índices negativos, usamos +255 (en [A-B + 255]). Puede eliminar esta acción definiendo un puntero al centro de la matriz.

const unsigned char *result = maxTable+255;
#define MINUS(A,B)    result[A-B];

úsalo como:

bsub  = MINUS(13,15); // i.e 13-15 with zero cutoff as requested

Tenga en cuenta que la ejecución es extremadamente rápida. Solo una resta y una deferencia de puntero para obtener el resultado. Sin ramificaciones. Las matrices estáticas son muy cortas, por lo que se cargarán completamente en la memoria caché de la CPU para acelerar aún más el cálculo.

Lo mismo funcionaría para la adición pero con una tabla un poco diferente (los primeros 256 elementos serán los índices y los últimos 255 elementos serán iguales a 255 para emular el límite más allá de 255.

Si insiste en la operación de bits, las respuestas que usan (a> b) son incorrectas. Esto todavía podría implementarse como ramificación. Usa la técnica del bit de signo

// (num1>num2) ? 1 : 0
#define        is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)

Ahora puedes usarlo para calcular restas y sumas.

Si desea emular las funciones max (), min () sin ramificar, use:

inline __int32 MIN_INT(__int32 x, __int32 y){   __int32 d=x-y; return y+(d&(d>>31)); }              

inline __int32 MAX_INT(__int32 x, __int32 y){   __int32 d=x-y; return x-(d&(d>>31)); }

Mis ejemplos anteriores usan números enteros de 32 bits. Puede cambiarlo a 64, aunque creo que los cálculos de 32 bits se ejecutan un poco más rápido. Depende de usted

DanielHsH
fuente
2
Es probable que no lo haga, en realidad: primero, por supuesto, cargar la mesa es lento. Las operaciones de bit toman 1 ciclo, la carga desde la memoria toma aproximadamente 80 ns; incluso desde la caché L1 estamos en el rango de 20 ns, que son casi 7 ciclos en una CPU de 3GHz.
edmz
No es del todo correcto. El método LUT tomará algunos sycles, pero la manipulación de bits no es un ciclo único también. Hay algunas acciones secuenciales. Por ejemplo, solo el cálculo de MAX () requiere 2 restas, una operación lógica y un desplazamiento a la derecha. Y no olvide la promoción / degradación de
enteros
1
Quise decir que las operaciones simples bit a bit toman 1 ciclo, asumiendo naturalmente operandos de registro. Con el código que mostró Shafik, clang genera 4 instrucciones elementales. Además (x > y), no tiene ramas.
edmz
Primero, (x> y) podría usar ramificaciones. No sabe en qué arquitectura se está ejecutando. Tiendo a estar de acuerdo en que posiblemente no tenga ramificaciones en la arquitectura Intel. La mayoría de los teléfonos inteligentes no son Intel. Esa es también la razón por la que no puede saber cuántas instrucciones de montaje habrá. Prueba mi solución en tu PC. Estoy interesado en escuchar los resultados.
DanielHsH
1
La caché L1 es mucho más rápida que 20 ns, es del orden de quizás 4 ciclos de procesador. Y probablemente usará una unidad de ejecución que de otro modo no se usaría y, de todos modos, estará completamente canalizada. Mídelo. Y 20ns son 60 ciclos en una CPU de 3 GHz.
gnasher729