¿Puedo insinuar el optimizador dando el rango de un entero?

173

Estoy usando un inttipo para almacenar un valor. Según la semántica del programa, el valor siempre varía en un rango muy pequeño (0 - 36), y int(no a char) se usa solo debido a la eficiencia de la CPU.

Parece que se pueden realizar muchas optimizaciones aritméticas especiales en un rango tan pequeño de enteros. Muchas llamadas de función en esos enteros pueden optimizarse en un pequeño conjunto de operaciones "mágicas", y algunas funciones pueden incluso optimizarse en búsquedas de tablas.

Entonces, ¿es posible decirle al compilador que esto intsiempre está en ese rango pequeño, y es posible que el compilador haga esas optimizaciones?

rolevax
fuente
44
existen optimizaciones de rango de valor en muchos compiladores, por ejemplo. llvm pero no conozco ninguna pista de lenguaje para declararlo.
Remus Rusanu
2
Tenga en cuenta que si nunca tiene números negativos, puede obtener pequeñas ganancias por usar unsignedtipos, ya que es más fácil para el compilador razonar con ellos.
user694733
44
@RemusRusanu: Pascal le permite definir tipos de subrango , por ejemplo var value: 0..36;.
Edgar Bonet el
77
" int (no es un char) se usa solo porque la eficiencia de la CPU " . Esta vieja pieza de sabiduría convencional generalmente no es muy cierta. Los tipos estrechos a veces deben extenderse por cero o por signos al ancho total del registro, especialmente. cuando se usan como índices de matriz, pero a veces esto sucede de forma gratuita. Si tiene una matriz de este tipo, la reducción en la huella de caché generalmente supera cualquier otra cosa.
Peter Cordes
1
Se olvidó de decir: inty es unsigned intnecesario extender el signo o cero de 32 a 64 bits, también, en la mayoría de los sistemas con punteros de 64 bits. Tenga en cuenta que en x86-64, las operaciones en registros de 32 bits se extienden de cero a 64 bits de forma gratuita (no se extiende la señal, pero el desbordamiento firmado es un comportamiento indefinido, por lo que el compilador solo puede usar matemática firmada de 64 bits si lo desea). Por lo tanto, solo verá instrucciones adicionales para argumentos de función de 32 bits de extensión cero, no resultados de cálculo. Lo haría para tipos sin signo más estrechos.
Peter Cordes

Respuestas:

230

Sí, es posible. Por ejemplo, gccpuede usar __builtin_unreachablepara decirle al compilador acerca de condiciones imposibles, así:

if (value < 0 || value > 36) __builtin_unreachable();

Podemos envolver la condición anterior en una macro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

Y úsalo así:

assume(x >= 0 && x <= 10);

Como puede ver , gccrealiza optimizaciones basadas en esta información:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Produce:

func(int):
    mov     eax, 17
    ret

Sin embargo, una desventaja es que si su código alguna vez rompe tales suposiciones, obtendrá un comportamiento indefinido .

No le avisa cuando esto sucede, incluso en las versiones de depuración. Para depurar / probar / atrapar errores con suposiciones más fácilmente, puede usar una macro híbrida asumir / afirmar (créditos a @David Z), como esta:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

En las compilaciones de depuración ( NDEBUG sin definir), funciona como un assertmensaje de error de impresión ordinario y un abortprograma 'ing', y en las compilaciones de lanzamiento utiliza una suposición, produciendo código optimizado.

Sin embargo, tenga en cuenta que no es un sustituto de la versión regular assert: condpermanece en las versiones de lanzamiento, por lo que no debe hacer algo así assume(VeryExpensiveComputation()).

Peter Mortensen
fuente
55
@Xofo, no lo entendí, en mi ejemplo esto ya está sucediendo, ya que return 2el compilador eliminó la rama del código.
66
Sin embargo, parece que gcc no puede optimizar funciones para operaciones mágicas o búsquedas de tablas como OP esperaba.
jingyu9575
19
@ user3528438, __builtin_expectes una pista no estricta. __builtin_expect(e, c)debe leerse como " ees más probable que evalúe c" y puede ser útil para optimizar la predicción de rama, pero no se limita ea ser siempre c, por lo que no permite que el optimizador descarte otros casos.Mira cómo se organizan las ramas en el ensamblaje .
66
En teoría, cualquier código que causa incondicionalmente un comportamiento indefinido podría usarse en lugar de __builtin_unreachable() .
CodesInChaos
14
A menos que haya algún capricho que no sé sobre eso hace que esta es una mala idea, que podría tener sentido para combinar esto con assert, por ejemplo, definir assumecomo assertcuando NDEBUGno está definido, y que __builtin_unreachable()cuando NDEBUGse define. De esa forma, obtiene el beneficio de la suposición en el código de producción, pero en una compilación de depuración aún tiene una comprobación explícita. Por supuesto, debe realizar suficientes pruebas para asegurarse de que la suposición se cumplirá en la naturaleza.
David Z
61

Hay soporte estándar para esto. Lo que debe hacer es incluir stdint.h( cstdint) y luego usar el tipo uint_fast8_t.

Esto le dice al compilador que solo está usando números entre 0 y 255, pero que es libre de usar un tipo más grande si eso proporciona un código más rápido. Del mismo modo, el compilador puede suponer que la variable nunca tendrá un valor superior a 255 y luego realizar optimizaciones en consecuencia.

Lundin
fuente
2
Estos tipos no se usan tanto como deberían (yo personalmente tiendo a olvidar que existen). Dan un código que es rápido y portátil, bastante brillante. Y han existido desde 1999.
Lundin, el
Esta es una buena sugerencia para el caso general. La respuesta de Deniss muestra una solución más maleable para escenarios específicos.
Carreras de ligereza en órbita el
1
El compilador solo obtiene la información del rango 0-255 en sistemas donde en uint_fast8_trealidad es un tipo de 8 bits (por ejemplo unsigned char) como en x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). A principios de DEC Alpha antes de 21164A , las cargas / tiendas de bytes no eran compatibles, por lo que cualquier implementación sensata lo usaría typedef uint32_t uint_fast8_t. AFAIK, no existe un mecanismo para que un tipo tenga límites de rango adicionales con la mayoría de los compiladores (como gcc), por lo que estoy bastante seguro de que uint_fast8_tse comportaría exactamente igual unsigned into lo que sea en ese caso.
Peter Cordes
( booles especial y tiene un rango limitado a 0 o 1, pero es un tipo incorporado, no definido por los archivos de encabezado en términos de char, en gcc / clang. Como dije, no creo que la mayoría de los compiladores tengan un mecanismo eso lo haría posible.)
Peter Cordes
1
De todos modos, uint_fast8_tes una buena recomendación, ya que usará un tipo de 8 bits en plataformas donde sea tan eficiente como unsigned int. (Estoy realmente no estoy seguro que los fastque se supone tipos que ser rápido para , y si la caché huella de compensación se supone que es parte de ella.). x86 tiene un amplio soporte para operaciones de bytes, incluso para agregar bytes con una fuente de memoria, por lo que ni siquiera tiene que hacer una carga separada de extensión cero (que también es muy barata). gcc crea uint_fast16_tun tipo de 64 bits en x86, que es una locura para la mayoría de los usos (frente a 32 bits). godbolt.org/g/Rmq5bv .
Peter Cordes
8

La respuesta actual es buena para el caso cuando sabe con certeza cuál es el rango, pero si aún desea un comportamiento correcto cuando el valor está fuera del rango esperado, entonces no funcionará.

Para ese caso, descubrí que esta técnica puede funcionar:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

La idea es una compensación de datos de código: está moviendo 1 bit de datos (ya sea x == c) a la lógica de control .
Esto sugiere al optimizador que, xde hecho, es una constante conocida c, lo que lo alienta a alinear y optimizar la primera invocación por fooseparado del resto, posiblemente en gran medida.

Sin fooembargo, asegúrese de factorizar el código en una sola subrutina , no duplique el código.

Ejemplo:

Para que esta técnica funcione, debe ser un poco afortunado: hay casos en los que el compilador decide no evaluar las cosas estáticamente, y son algo arbitrarios. Pero cuando funciona, funciona bien:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Simplemente use -O3y observe las constantes previamente evaluadas 0x20y 0x30een la salida del ensamblador .

usuario541686
fuente
¿No te gustaría if (x==c) foo(c) else foo(x)? Si solo para atrapar constexprimplementaciones de foo?
MSalters
@MSalters: ¡Sabía que alguien iba a preguntar eso! Se me ocurrió esta técnica antes constexpry nunca me molesté en "actualizarla" después (aunque realmente nunca me he molestado en preocuparme constexprincluso después), pero la razón por la que no lo hice inicialmente fue porque quería facilita que el compilador los descomponga como código común y elimine la rama si decide dejarlos como llamadas a métodos normales y no optimizarlos. Esperaba que si ponía que cfuera realmente difícil para el compilador c (lo siento, broma) que los dos son el mismo código, aunque nunca verifiqué esto.
user541686
4

Solo estoy diciendo que si quieres una solución que sea más estándar en C ++, puedes usar el [[noreturn]]atributo para escribir el tuyo unreachable.

Así que volveré a utilizar el excelente ejemplo de Deniss para demostrar:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Lo que, como puede ver , da como resultado un código casi idéntico:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

La desventaja es, por supuesto, que recibe una advertencia de que una [[noreturn]]función, de hecho, regresa.

StoryTeller - Unslander Monica
fuente
Funciona con clang, cuando mi solución original no lo hace , un buen truco y +1. Pero todo depende mucho del compilador (como nos mostró Peter Cordes, ya iccque puede empeorar el rendimiento), por lo que todavía no es de aplicación universal. Además, una nota menor: la unreachabledefinición debe estar disponible para el optimizador y en línea para que esto funcione .