Pregunta

Cuando la compilación de C/C++ códigos usando gcc/g++, si ignora mi registro, ¿que me dicen?Por ejemplo, en este código

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    return 0;
}

j será utilizado como registro, pero en este código

int main()
{
    register int j;
    int k;
    for(k = 0; k < 1000; k++)
        for(j = 0; j < 32000; j++)
            ;
    int * a = &j;
    return 0;
}

j será una variable normal.Puede que me diga si una variable solía registrarse es realmente almacenado en un registro de CPU?

¿Fue útil?

Solución

Usted puede asumir que GCC ignora la register palabras clave excepto tal vez en -O0.Sin embargo, no se debe hacer una diferencia, de una manera o de otra, y si son de tal profundidad, que ya debería estar leyendo el código de la asamblea.

Aquí es un hilo informativo sobre este tema: http://gcc.gnu.org/ml/gcc/2010-05/msg00098.html .De vuelta en los viejos días, register de hecho, ayudó a los compiladores para asignar una variable en registros, pero hoy asignación de registro puede realizarse de forma óptima, de forma automática, sin pistas.La palabra clave no continúa, para servir a dos propósitos en C:

  1. En C, se le impide tomar la dirección de una variable.Dado que los registros no tienen direcciones, esta restricción puede ayudar a un simple compilador de C.(Simples compiladores de C++ no existen.)
  2. Un register objeto no puede ser declarada restrict.Porque restrict corresponde a las direcciones, su intersección es inútil.(C++ todavía no han restrict, y de todos modos, esta regla es un poco trivial.)

Para C++, la palabra clave se ha dejado de utilizar debido a que C++11 y propuesta para la eliminación de la norma de revisión previsto para 2017.

Algunos compiladores han utilizado register en declaraciones de parámetro para determinar la convención de llamada de funciones, con la ABI permitiendo mixto de la pila y registro de los parámetros.Este parece ser el inconformismo, tiende a ocurrir con la sintaxis extendida como register("A1"), y no sé si algún compilador está todavía en uso.

Otros consejos

Con respecto a las técnicas modernas de compilación y optimización, el register La anotación no tiene ningún sentido en absoluto. En su segundo programa, toma la dirección de j, y los registros no tienen direcciones, pero una misma variable local o estática podría almacenarse perfectamente en dos ubicaciones de memoria diferentes durante su vida, o a veces en la memoria y, a veces, en un registro, o no existir en absoluto. De hecho, un compilador optimizante compilaría sus bucles anidados como nada, porque no tienen ningún efecto y simplemente asignarían sus valores finales a k y j. Y luego omita estas tareas porque el código restante no usa estos valores.

No puede obtener la dirección de un registro en C, además del compilador puede ignorarlo totalmente; Estándar C99, Sección 6.7.1 (PDF):

La implementación puede tratar cualquier declaración de registro simplemente como una declaración automática. Sin embargo, si realmente se utiliza el almacenamiento direccionable o no, la dirección de cualquier parte de un objeto declarado con el registro de especificadores de clase de almacenamiento no se puede calcular, ya sea explícitamente (mediante el uso del Operador y el operador como se discute en 6.5.3.2) o implícitamente ( convirtiendo un nombre de matriz en un puntero como se discutió en 6.3.2.1). Por lo tanto, el único operador que se puede aplicar a una matriz declarada con el registro de especificadores de clase de almacenamiento es el tamaño de.

A menos que estés amontonando en AVR o fotos de 8 bits, el compilador probablemente se reirá de ti pensando que sabes mejor e ignoras tus súplicas. Incluso en ellos, pensé que sabía mejor un par de veces y encontré formas de engañar al compilador (con un ASM en línea), pero mi código explotó porque tenía que masajear un montón de otros datos para trabajar en torno a mi terquedad.

Esta pregunta, y algunas de las respuestas, y varias otras discusiones sobre las palabras clave del "registro" que he visto, parecen asumir implícitamente que todos los locales están asignados a un registro específico o a una ubicación de memoria específica en la pila. Esto fue generalmente cierto hasta hace 15-25 años, y es cierto si desactiva la optimización, pero no es cierto en absoluto cuando se realiza la optimización estándar. Los optimizadores ahora visten a los locales como nombres simbólicos que usa para describir el flujo de datos, en lugar de los valores que deben almacenarse en ubicaciones específicas.

Nota: por 'locales' aquí quiero decir: variables escalares, de la clase de almacenamiento automática (o 'registrarse'), que nunca se usan como operando de '&'. Los compiladores a veces pueden dividir las estructuras automáticas, sindicatos o matrices en variables 'locales' individuales también.

Para ilustrar esto: supongo que escribo esto en la parte superior de una función:

int factor = 8;

.. y luego el único uso del factor La variable es multiplicar por varias cosas:

arr[i + factor*j] = arr[i - factor*k];

En este caso, pruébelo si lo desea, no habrá factor variable. El análisis de código mostrará que factor es siempre 8, por lo que todos los cambios se convertirán en <<3. Si hiciste lo mismo en 1985 C, factor obtendría una ubicación en la pila, y habría multiplicidades, ya que los compiladores básicamente trabajaron una declaración a la vez y no recordaban nada sobre los valores de las variables. En aquel entonces, los programadores tendrían más probabilidades de usar #define factor 8 para obtener un mejor código en esta situación, mientras se mantiene ajustable factor.

Si utiliza -O0 (Optimización apagada): de hecho obtendrá una variable para factor. Esto le permitirá, por ejemplo, pasar por encima del factor=8 declaración y luego cambiar factor a 11 con el depurador, y sigue adelante. Para que esto funcione, el compilador no puede mantener cualquier cosa en registros entre declaraciones, excepto las variables asignadas a registros específicos; Y en ese caso, el depurador está informado de esto. Y no puede tratar de "saber" nada sobre los valores de las variables, ya que el depurador podría cambiarlas. En otras palabras, necesita la situación de 1985 si desea cambiar las variables locales mientras se depugga.

Los compiladores modernos generalmente compilan una función de la siguiente manera:

(1) Cuando una variable local se asigna a más de una vez en una función, el compilador crea diferentes 'versiones' de la variable para que cada una solo se asigne en un solo lugar. Todas las 'lecturas' de la variable se refieren a una versión específica.

(2) Cada uno de estos locales se asigna a un registro 'virtual'. Los resultados del cálculo intermedio también se asignan variables/registros; asi que

  a = b*c + 2*k;

se convierte en algo como

       t1 = b*c;
       t2 = 2;
       t3 = k*t2;
       a = t1 + t3;

(3) El compilador luego toma todas estas operaciones y busca subexpresiones comunes, etc. Dado que cada uno de los nuevos registros solo se escribe una vez, es bastante más fácil reorganizarlas mientras mantiene la corrección. Ni siquiera comenzaré en el análisis de bucle.

(4) El compilador luego intenta mapear todos estos registros virtuales en registros reales para generar código. Dado que cada registro virtual tiene una vida útil limitada, es posible reutilizar los registros reales en gran medida: 'T1' en lo anterior solo se necesita hasta que el ADD que genera 'A', por lo que podría mantenerse en el mismo registro que 'A'. Cuando no hay suficientes registros, algunos de los registros virtuales se pueden asignar a la memoria, o, un valor se puede mantener en un determinado registro, almacenarse a la memoria durante un tiempo y cargarse en un registro (posiblemente) diferente más tarde . En una máquina de carga de carga, donde solo se pueden usar valores en los registros en los cálculos, esta segunda estrategia lo acomoda bien.

De lo anterior, esto debería ser claro: es fácil determinar que el registro virtual asignado a factor es lo mismo que la constante '8', por lo que todas las multiplicaciones por factor son multiplicaciones por 8. incluso si factor se modifica más tarde, esa es una 'nueva' variable y no afecta los usos previos de factor.

Otra implicación, si escribes

 vara = varb;

.. puede o no ser el caso de que haya una copia correspondiente en el código. Por ejemplo

int *resultp= ...
int acc = arr[0] + arr[1];
int acc0 = acc;    // save this for later
int more = func(resultp,3)+ func(resultp,-3);
acc += more;         // add some more stuff
if( ...){
    resultp = getptr();
    resultp[0] = acc0;
    resultp[1] = acc;
}

En las dos 'versiones' anteriores de ACC (inicial, y después de agregar 'más') podrían estar en dos registros diferentes, y 'ACC0' sería lo mismo que el 'ACC' inital. Por lo tanto, no se necesitaría ninguna copia de registro para 'ACC0 = ACC'. Otro punto: el 'resultadop' se asigna a dos veces, y dado que la segunda asignación ignora el valor anterior, hay esencialmente dos variables distintas 'resultadop' en el código, y esto se determina fácilmente mediante análisis.

Una implicación de todo esto: no dude en dividir expresiones complejas en las más pequeñas usando locales adicionales para intermedios, si hace que el código sea más fácil de seguir. Básicamente hay cero penalización por tiempo de ejecución para esto, ya que el optimizador ve lo mismo de todos modos.

Si está interesado en aprender más, podría comenzar aquí: http://en.wikipedia.org/wiki/static_single_assignment_form

El punto de esta respuesta es (a) dar una idea de cómo funcionan los compiladores modernos y (b) señalar que pedirle al compilador, si sería tan amable, que ponga una variable local en particular en un registro, no lo hace. Realmente tiene sentido. El optimizador puede ver cada 'variable' como varias variables, algunas de las cuales pueden usarse en gran medida en bucles, y otras no. Algunas variables desaparecerán, por ejemplo, por ser constante; o, a veces, la variable temperada utilizada en un intercambio. O cálculos no utilizados realmente. El compilador está equipado para usar el mismo registro para diferentes cosas en diferentes partes del código, de acuerdo con lo que realmente es mejor en la máquina para la que está compilando.

La noción de insinuar el compilador en cuanto a qué variables deben estar en registros supone que cada variable local se asigna a un registro o a una ubicación de memoria. Esto fue cierto cuando Kernighan + Ritchie diseñó el lenguaje C, pero ya no es cierto.

Con respecto a la restricción de que no puede tomar la dirección de una variable de registro: claramente, no hay forma de implementar tomar la dirección de una variable mantenida en un registro, pero puede preguntar, ya que el compilador tiene discreción para ignorar el 'registro' ' - ¿Por qué esta regla está en su lugar? ¿Por qué el compilador no puede ignorar el 'registro' si tomo la dirección? (Como es el caso en C ++).

Nuevamente, tienes que volver al antiguo compilador. El compilador K+R original analizaría una declaración de variable local, y luego inmediatamente Decide si asignarlo a un registro o no (y si es así, qué registro). Luego procedería a compilar expresiones, emitiendo el ensamblador para cada declaración una a la vez. Si luego descubrió que estaba tomando la dirección de una variable de 'registro', que había sido asignada a un registro, no había forma de manejar eso, ya que la tarea era, en general, irreversible para entonces. Sin embargo, era posible generar un mensaje de error y dejar de compilar.

En pocas palabras, parece que 'registrarse' es esencialmente obsoleto:

  • Los compiladores de C ++ lo ignoran por completo
  • C Los compiladores lo ignoran, excepto para hacer cumplir la restricción sobre & - Y posiblemente no lo ignore en -O0 donde en realidad podría dar lugar a la asignación según lo solicitado. Sin embargo, en -o0 no te preocupa la velocidad del código.

Por lo tanto, básicamente está allí ahora para la compatibilidad atrasada, y probablemente sobre la base de que algunas implementaciones aún podrían estar utilizándolo para 'pistas'. Nunca lo uso, y escribo el código DSP en tiempo real y paso un poco de tiempo mirando el código generado y encontrando formas de hacerlo más rápido. Hay muchas formas de modificar el código para que se ejecute más rápido, y saber cómo funcionan los compiladores es muy útil. Ha pasado mucho tiempo desde la última vez que descubrí que agregar 'registrarse' es una de esas formas.


Apéndice

Excluí anteriormente, de mi definición especial de 'locales', variables a las que & se aplica (estos están, por supuesto, se incluyen en el sentido habitual del término).

Considere el código a continuación:

void
somefunc()
{
    int h,w;
    int i,j;
    extern int pitch;

    get_hw( &h,&w );  // get shape of array

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*pitch + j] = generate_func(i,j);
        }
    }
}

Esto puede verse perfectamente inofensivo. Pero si le preocupa la velocidad de ejecución, considere esto: el compilador está pasando las direcciones de h y w a get_hw, y luego luego llamando generate_func. Supongamos que el compilador no sabe nada sobre lo que hay en esas funciones (que es el caso general). El compilador deber Suponga que la llamada a generate_func podría estar cambiando h o w. Ese es un uso perfectamente legal del puntero pasado a get_hw - Podrías almacenarlo en algún lugar y luego usarlo más tarde, siempre y cuando el alcance contenga h,w todavía está en juego, para leer o escribir esas variables.

Por lo tanto, el compilador debe almacenar h y w en la memoria en la pila y no puede determinar nada por adelantado sobre cuánto tiempo se ejecutará el bucle. Por lo tanto, ciertas optimizaciones serán imposibles, y el bucle podría ser menos eficiente como resultado (en este ejemplo, hay una llamada de función en el bucle interno de todos modos, por lo que puede no marcar una gran diferencia, pero considere el caso donde hay una función cual es ocasionalmente llamado en el bucle interno, dependiendo de alguna condición).

Otro problema aquí es que generate_func podría cambiar pitch, y entonces i*pitch debe hacerse cada vez, en lugar de solo cuando i cambios.

Se puede recodificar como:

void
somefunc()
{
    int h0,w0;
    int h,w;
    int i,j;
    extern int pitch;
    int apit = pitch;

    get_hw( &h0,&w0 );  // get shape of array
    h= h0;
    w= w0;

    for( int i = 0; i < h; i++ ){
        for( int j = 0; j < w; j++ ){
            Arr[i*apit + j] = generate_func(i,j);
        }
    }
}

Ahora las variables apit,h,w son todos locales 'seguros' en el sentido que definí anteriormente, y el compilador puede estar seguro de que no los cambiarán por ninguna llamada de función. Suponiendo que soy no modificando cualquier cosa en generate_func, el código tendrá el mismo efecto que antes pero podría ser más eficiente.

Jens Gustedt ha sugerido el uso de la palabra clave del "registro" como una forma de etiquetar variables clave para inhibir el uso de & En ellos, por ejemplo, por ejemplo, por otros manteniendo el código (no afectará el código generado, ya que el compilador puede determinar la falta de & sin ello). Por mi parte, siempre pienso cuidadosamente antes de aplicar & a cualquier escalar local en un área crítica del tiempo del código, y en mi opinión, usar 'registrarse' para hacer cumplir esto es un poco críptico, pero puedo ver el punto (desafortunadamente no funciona en C ++ ya que el compilador solo lo hará. Ignorar el 'registro').

Por cierto, en términos de eficiencia del código, la mejor manera de que una función devuelva dos valores es con una estructura:

struct hw {  // this is what get_hw returns
   int h,w;
};

void
somefunc()
{
    int h,w;
    int i,j;

    struct hw hwval = get_hw();  // get shape of array
    h = hwval.h;
    w = hwval.w;
    ...

Esto puede parecer engorroso (y es engorroso de escribir), pero generará código más limpio que los ejemplos anteriores. El 'struct hw' en realidad se devolverá en dos registros (en la mayoría de los abis modernos de todos modos). Y debido a la forma en que se usa la estructura 'HWVAL', el optimizador lo dividirá efectivamente en dos 'locales' hwval.h y hwval.w, y luego determine que estos son equivalentes a h y w -- asi que hwval Básicamente desaparecerá en el código. No es necesario pasar punteros, ninguna función está modificando las variables de otra función a través del puntero; Es como tener dos valores de retorno escalar distintos. Esto es mucho más fácil de hacer ahora en C ++ 11 - con std::tie y std::tuple, puede usar este método con menos verbosidad (y sin tener que escribir una definición de estructura).

Su segundo ejemplo no es válido en C., así que ves bien que el register La palabra clave cambia algo (en c). Está ahí para este propósito, para inhibir la toma de una dirección de una variable. Así que no tome su nombre "Regístrese" verbalmente, es un nombre inapropiado, pero apegue a su definición.

Que C ++ parece ignorar register, bueno, deben tener su razón para eso, pero me parece un poco triste encontrar nuevamente una de estas diferencias sutiles donde el código válido para uno no es válido para el otro.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top