2010-10-27 11 views
6

¿Hay alguna forma de optimizar la siguiente línea de código C (para evitar la bifurcación)?¿Cómo optimizar esta línea de código C (rango de comprobación)?

if ((i < -threshold) || (i > threshold)) 
{ 
    counter++; 
} 

Todas las variables son enteros con signo de 16 bits. Una versión optimizada debería ser altamente portátil.

+5

dices "ambos" pero hay tres variables. – McKay

+0

no puedo recordar si esto funciona con seguridad, pero intento 'if ((unsigned int) i> threshold)' – zdav

+0

@zdav Definitivamente no funciona para la mayoría de los compiladores. Tales conversiones están definidas al menos por implementación y generalmente te dan un complemento de 2. –

Respuesta

12

¿Qué tal:

counter += (i < -threshold) | (i > threshold); 

Suponiendo que el código original era válido, entonces esto debería funcionar también, de una manera portátil. El estándar dice que los operadores relacionales (<, > y así sucesivamente) devuelven un int igual a 1 en caso de éxito, o 0 en caso de falla.

ACTUALIZACIÓN

Para contestar el comentario de Sheen a continuación, el siguiente código:

int main() 
{ 
    short threshold = 10; 
    short i = 20; 
    short counter = 0; 

    counter += (i < -threshold) | (i > threshold); 

    return 0; 
} 

resultados en la siguiente desensamblador en x86 utilizando GCC, sin optimizaciones:

push %rbp 
    mov %rsp,%rbp 
    movw $0xa,-6(%rbp) 
    movw $0x14,-4(%rbp) 
    movw $0x0,-2(%rbp) 
    movswl -4(%rbp),%edx 
    movswl -6(%rbp),%eax 
    neg %eax 
    cmp %eax,%edx 
    setl %dl 
    movzwl -4(%rbp),%eax 
    cmp -6(%rbp),%ax 
    setg %al 
    or  %edx,%eax 
    movzbw %al,%dx 
    movzwl -2(%rbp),%eax 
    lea (%rdx,%rax,1),%eax 
    mov %ax,-2(%rbp) 
    mov $0x0,%eax 
    leaveq 
    retq 
+0

no entiende cómo esto impide la bifurcación. ¿Puedes pegar aquí el código ensamblado generado? – Sheen

+3

Esto agregaría 2 al contador si el umbral es el umbral < 0, i >, yi <-el umbral. Puede ser seguro suponer que el umbral> = 0, pero si es así, el OP debería editarse para agregar esta suposición. –

+0

@Sheen En x86, las evaluaciones de las condiciones como enteros se pueden realizar con las instrucciones 'setl' y' setg', un poco caras porque son poco frecuentes pero mucho más baratas que las derivadas mal predefinidas. –

1

Comparar el absoluto de ambos

short imask = i >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0 
short tmask = threshold >> sizeof(short) * 8 - 1; //compute the sign bit 1 or 0 

short iabsolute = (i + imask)^imask; // compute i absolute 
short tabsolute = (threshold + tmask)^tmask; // compute threshold absolute 

counter += iabsolute > tabsolute; 
+0

Desplazar a la derecha un número negativo es UB. El que pregunta preguntó por "portátil". –

+0

Agradable. C99 tiene 'CHAR_BIT' en limits.h en lugar de' 8' para que funcione en arquitecturas inusuales (pero aún complemento a 2). Además, quiere decir "umbral> absoluto", probablemente. –

+2

@Oli Charlesworth No, está definido por la implementación. 6.5.7.5. –

1

Usted puede utilizar el siguiente truco que reduce las ramas de una misma rama:

if (((unsigned) (i + threshold)) > (threshold << 1)) 
{ 
    counter++; 
} 

o, por el pedante:

if (((unsigned) i + (unsigned) threshold) > ((unsigned) threshold << 1)) 
{ 
    counter++; 
} 
+0

La adición (y el desplazamiento a la izquierda) podría desbordarse. Además, supongo que solo habrá una rama en el código original (bueno, depende del conjunto de instrucciones, supongo). –

+0

@Oli: no es posible que se desborde si el original no se desbordó. Si el shift izquierdo se desbordó, entonces la prueba original '(i <-threshold) || (i> umbral) 'no tendría sentido. Esto funciona. Lo he usado mucho. Es un ajuste no obvio. – Skizz

+0

@Skizz: Estoy de acuerdo en que esto funciona en la práctica en la aritmética de dos complementos. Pero técnicamente, el comportamiento en el desbordamiento de enteros no está definido. Y esto puede suceder en tu código si, por ejemplo, umbral = 'INT_MAX'. –

1

Esto se basa en bit twiddling hacks, (muy recomendable)

#define CHAR_BIT 8 

int main() 
{ 
    int i=-3; // example input 
    int treshold=2; // example treshold 
    int count=0; 
    // step 1: find the absolute value of i 
    unsigned int r; // the result goes here 
    int const mask = i >> (sizeof(int) * CHAR_BIT - 1); 
    r = (i + mask)^mask; 
    // step 2: compute the sign of the difference 
    // sign becomes 0 (if r<=treshold) 
    // sign becomes 1 otherwise 
    int sign = 1^((unsigned int)(r-treshold-1) >> (sizeof(int) * CHAR_BIT - 1)); 
    count+=sign; 
    return count; 
} 

Esto funciona para 32 bits enteros, la adaptación a 16 bits debe ser fácil. Compila con g ++.

La velocidad depende de la procesador utilizado. La ramificación podría ser más rápida después de todo.

+1

Los números negativos de desplazamiento a la derecha están definidos por la implementación. –

+0

Del sitio web bit twiddling hacks: El 7 de marzo de 2003, Angus Duggan señaló que la especificación ANSI C de 1989 deja el resultado de la definición definida de ejecución del cambio a la derecha, por lo que en algunos sistemas este hack podría no funcionar. He leído que ANSI C no requiere que los valores se representen como complemento a dos, por lo que puede no funcionar también por esa razón (en un número cada vez menor de máquinas antiguas que todavía usan el complemento de uno). Por lo tanto, depende de cuán portátil OP desee que responda la pregunta. – mirk

+2

@Oli, tienes razón en que los números negativos que se desplazan a la derecha están definidos por la implementación. Si encuentra un compilador que no implementa esto como una réplica de bits significativos (por ejemplo, lo que todos esperan) le enviaré un paquete de vinos ... (no, los compiladores escritos por usted no se aplican) –

1

Oli Charlesworth, creo, tiene la idea correcta. Sin embargo, sospecho que se puede optimizar aún más (a expensas de la legibilidad).

El umbral se puede normalizar a cero para eliminar una comparación.

Es decir, ...

counter += ((unsigned) (i + threshhold) < (unsigned) (threshhold + threshhold)); 
+0

Cualquiera de estas adiciones podría desbordarse. –

+0

Oli tiene razón, pero se soluciona fácilmente. Transmitir a 'unsigned' antes de la adición y luego está bien. Dado que los valores originales caben en 'signed int', funcionará bien. –

+0

@R: en sistemas que usan aritmética de dos complementos, la conversión de un negativo int a unsigned agregará (UINT_MAX + 1) a él, pero creo que el estándar explícitamente permite que los sistemas usen formato de signo + magnitud, en cuyo caso el elenco restar el valor de ((UINT_MAX + 1)/2). Desafortunadamente, no conozco ninguna forma segura y portátil de agregar un valor posiblemente negativo a un valor sin signo cuando la suma puede estar entre INT_MAX y UINT_MAX. – supercat

9

Hay un lenguaje estándar para la gama de comprobación con una sola instrucción de comparación.Dice así:

(unsigned)x - a <= (unsigned)b - a /* a <= x <= b */ 
(unsigned)x - a < (unsigned)b - a /* a <= x < b */ 

Como un ejemplo común (esta versión si isdigit se garantiza que sea correcta por la norma):

(unsigned)ch - '0' < 10 

Si el tipo de original es mayor que int (por ejemplo long long) luego deberá usar tipos sin firmar más grandes (por ejemplo, unsigned long long). Si a y b son constantes o ya tiene tipo sin signo, o si conoce b-a no se desbordará, se puede omitir el elenco de b.

Para que este método funcione, naturalmente debe tener a<=b y los tipos/valores debe ser tal que la expresión original (es decir a <= x && x <= b o similar) se comporta matemáticamente correctamente. Por ejemplo, si x fueron firmados y sin firmar b, x<=b podría evaluar en falso cuando x=-1 y b=UINT_MAX-1. Siempre que sus tipos originales estén todos firmados o sean más pequeños que el tipo sin firmar al que se lanza, esto no es un problema.

En cuanto a cómo este "truco" funciona, es puramente determinar, después de la reducción de módulo UINT_MAX+1, si x-a se encuentra en el rango de 0 a b-a.

En su caso, creo que el siguiente debería funcionar bien:

(unsigned)i + threshold > 2U * threshold; 

Si threshold no cambia entre las iteraciones del bucle, el compilador probablemente puede mantener tanto threshold y 2U*threshold en los registros.

Hablando de optimizaciones, un buen compilador debe optimizar su prueba original gama de usar la aritmética sin signo donde se sabe que se cumplan las restricciones. Sospecho que muchos lo hacen con a y b constante, pero tal vez no con expresiones más complejas. Incluso si el compilador puede optimizar, sin embargo, el lenguaje (unsigned)x-a<b-a sigue siendo de gran utilidad en las macros en las que desea asegurarse de que x se evalúa exactamente una vez.

+0

Esta es la respuesta correcta IMO. Igual que http://stackoverflow.com/questions/17095324/fastest-way-in-c-to-determine-if-an-integer-is-between-two-integers-inclusive – netigger

3

Oh, qué mal la pregunta ya ha sido contestada. Parafraseando la respuesta de Oli, el código

#include <stdint.h> 
int main() 
{ 
    int32_t threshold_square = 100; 
    int16_t i = 20; 
    int16_t counter = 0; 

    counter += ((int32_t) i * i > threshold_square); 

    return 0; 
} 

se obtiene la siguiente ensamblador x86 usando GCC sin optimizaciones

pushq %rbp 
movq %rsp, %rbp 
movl $100, -8(%rbp) 
movw $20, -2(%rbp) 
movw $0, -4(%rbp) 
movswl -2(%rbp),%edx 
movswl -2(%rbp),%eax 
imull %edx, %eax 
cmpl -8(%rbp), %eax 
setg %al 
movzbl %al, %edx 
movzwl -4(%rbp), %eax 
leal (%rdx,%rax), %eax 
movw %ax, -4(%rbp) 
movl $0, %eax 
leave 
ret 

que es cuatro instrucciones a menos que el uso de (i < -threshold) | (i > threshold).

Si esto es mejor o no, por supuesto, depende de la arquitectura.

(El uso de stdint.h a efectos ilustrativos, por estricta C89 reemplazar con lo que es relevante para el sistema de destino.)

+0

+1: No lo logré piensa en esto Buen enfoque (y en retrospectiva, obvio)! –

+0

Por mucho que esto sea correcto y más óptimo que el método de Oli, una ventaja de su método (y sus variantes que aparecen en otras respuestas) es que es fácil extenderlo para verificar el rango asimétrico, mientras que aquí el rango es siempre simétrico. – ysap

-1

¿Qué hay de malo en el código original? ¿Realmente necesita optimizar la mano?

Cualquier compilador decente debería ser capaz de optimizarlo muy bien. Cualquier optimización de la mano probablemente solo conduzca a la ofuscación.

1

Este código no tiene una rama altamente portátil (sin embargo, la implementación de abs puede tener uno).

#include <stdlib.h> 
counter += abs(i) > threshold; 

Esa es la expresión estándar más simple.

Si su compilador no utiliza macro optimizado para abs() puede usar su propia función macro/en línea.

que son ejemplos, que la naturaleza uso del formato de complemento a dos usada en la mayoría de las máquinas:

#define ABS(x) ((x)*(((x)>>15)|1)) 

#define ABS(x) ((x)-((x)>>15)^((x)>>15)) 

También es posible reemplazar operador de comparación con la expresión de la siguiente manera:

#define LESS(x, y) (-((x)-(y))>>15)) 

código resultante:

counter -= ((threshold - abs(i)) >> 15); 

Todas esas macros se basan en hechos, que cambian a la derecha al número de bits menos uno de valor positivo o cero se evalúa a cero, y de negativo se evalúa a menos uno. Pero esa implementación está definida.

Cuestiones relacionadas