¿Cómo debemos comparar dos enteros?

8

Recientemente escribí un programa que ordena una matriz. Para ello, necesitaba escribir una función de comparación, que pasaré a ella. Mi función de comparación debería haber devuelto 1 (si x> y), -1 (si x <y) o 0 (si x = y). Escribí una función regular (Función 1) usando expresiones condicionales, pero me aconsejaron que escribiera de manera diferente (Función 2). ¿Es mejor escribir así? ¿Una condición booleana siempre devolverá 1 para la verdad? (Quiero decir, si x = 0 e y = 0 siempre tendremos (x == y) == 1?)

Función 1:

int Icmp(void* x, void* y)
{
    int a = *(int*)x;
    int b = *(int*)y;
    if (a > b)
        return 1;
    else if (a < b)
        return -1;
    else
        return 0;
}

Función 2:

int Icmp(void* x, void* y)
{
    return (*(int*)x > * (int*)y) - (*(int*)x < *(int*)y);
}
Skiv Hisink
fuente
55
La ventaja nominal del segundo es que evita los saltos condicionales y puede funcionar mejor. La notación utilizada es espantosa; las variables deben localizarse como en el primero y luego usarse en el cálculo.
Jonathan Leffler
44
El segundo es un truco común para forzarlo sin ramas en computadoras estúpidas. Combina la legibilidad del primero para obtener lo mejor de ambos mundos. Editar: es decir, como Jonathan dijo anteriormente
Antti Haapala
8
Ambas técnicas funcionan. Mida la diferencia de rendimiento con cuidado. Probablemente sea difícil de detectar. Prefiere claridad cuando la diferencia es marginal.
Jonathan Leffler
1
Pregúntale a quien te aconsejó por qué. Pregunte en qué situaciones se producen las ventajas indicadas. Pregunte por qué se espera que esas siutaciones sean prominentes para usted. Si no obtiene respuestas satisfactorias para todas esas preguntas, busque otro asesor. Considere medir las ventajas indicadas, si es posible. Considérese a sí mismo cuán importantes son las ventajas de las que es consciente, sobre todo "Entiendo el código" y "Creo que todos mis compañeros entienden mejor el código". Entonces decide tú mismo.
Yunnosch
2
@ JoëlHecht si no es 1, entonces no es un compilador de C. ¿Por qué querrías usar un compilador que no sea C para compilar código C?
Antti Haapala

Respuestas:

14

La forma preferida de escribir el código no ramificado sería utilizar una variable local para los operandos:

int icmp(const void *x, const void *y)
{
    int a = *(const int *)x;
    int b = *(const int *)y;
    return (a > b) - (a < b);
}

La expresión es un idioma común en las funciones de comparación, y si se escribe usando variables en lugar de desreferencias de puntero en el lugar, también es bastante legible.

El código se basa en el hecho de que el resultado de una comparación usando >, <o incluso== es de tipo int1 y 0. Esto es requerido por el estándar C: cualquier compilador que genere valores como 42 o -1 no es, por definición, un compilador C .

Es fácil ver que max. uno de a > bo a < bpuede ser cierto en un momento dado, y el resultado es 1 - 0,0 - 1 o0 - 0 .

En cuanto a por qué el código sin ramificación, si bien los compiladores pueden generar exactamente el mismo código para ambas funciones, a menudo no lo hacen. Por ejemplo, los últimos GCC e ICC parecen generar una ramificación para la primera función en x86-64, pero un código sin ramificación con ejecución condicional para la última. Y a cualquiera que diga que las ramas no importan, entonces lo remito al control de calidad más votado de la historia en Stack Overflow .

Antti Haapala
fuente
Esta es la forma correcta de hacerlo. Es muy legible, el código máquina compilado es idéntico y evita ramificaciones a favor de un poco de aritmética adicional. El predictor de bifurcación puede causar muchos problemas de rendimiento al ordenar grandes matrices que se encuentran en un alto estado de entropía. Esto evita ese problema.
3ch0
55
Solo una nota de que la ausencia de una ifdeclaración no significa que no se generará ninguna rama, ni que si hay una ifdeclaración de que habrá una rama. De hecho, depende de si el compilador puede optimizarlos.
G. Sliepen
@ G.Sliepen en realidad ICC sería perfectamente capaz de optimizarlos, pero llegó a la conclusión de que la rama probablemente no se tomaría porque fue escrita usando ramas ...
Antti Haapala
3

¿Es mejor escribir así?

Yo diría que no.

Para el rendimiento; o no importa (probablemente para los compiladores modernos), o no debería ser una función separada (y debería estar integrada en el código utilizado para ordenar), o no debería estar ordenando en absoluto (por ejemplo, datos ordenados en la creación y no ordenado después de la creación).

Para facilitar la lectura (mantenimiento del código, posibilidad de ver errores en la versión original, riesgo de introducir errores más tarde) Prefiero su versión original; especialmente cuando se trabaja en un equipo, y especialmente cuando otros miembros del equipo están más familiarizados con otros 10 lenguajes de programación que tienen reglas muy diferentes a las de C.

Específicamente; Me gusta esto (porque los lanzamientos en el código real hacen que las cosas sean más difíciles de leer):

    int a = *(int*)x;
    int b = *(int*)y;

..y reescribiría el resto para que se vea así:

    if (a > b) {
        return 1;
    }
    if (a < b) {
        return -1;
    }
    return 0;
}

..o para verse así:

    if (a > b) return 1;
    if (a < b) return -1;
    return 0;
}

..porque elsees innecesario después de un return; y porque "sin llaves seguidas de una declaración en su propia línea" crea el riesgo de que alguien inserte accidentalmente una nueva línea sin darse cuenta y rompa todo (por ejemplo, consulte https://dwheeler.com/essays/apple-goto- fail.html ).

Brendan
fuente
0

Si está utilizando la función de comparación con qsort, la función solo necesita devolver valores + ve, -ve o cero.

En ese caso, solo puedes restar los números

int Icmp(const void* x, const void* y)
{
    return (*(int*)x - *(int*)y);
}
Rishikesh Raje
fuente
De hecho, esto es más eficiente que (a > b) - (a < b), pero el problema a - bes que la resta puede desbordarse. Por lo tanto, esta es una mala expresión.
Chmike
-2

Enteros

echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1

Carrozas

echo 1.5 <=> 1.5; // 0
echo 1.5 <=> 2.5; // -1
echo 2.5 <=> 1.5; // 1

Instrumentos de cuerda

echo "a" <=> "a"; // 0
echo "a" <=> "b"; // -1
echo "b" <=> "a"; // 1
Milan Maniya
fuente
La pregunta es sobre cómo comparar números enteros en C. Su respuesta claramente no es un código C válido y, por lo tanto, no proporciona información útil a la persona que hizo la pregunta. Asegúrese de que sus respuestas estén en el tema.
G. Sliepen