c++ - studio - programacion android pdf 2018
¿Cómo se sabe que las variables están en los registros o en la pila? (8)
Estoy leyendo esta pregunta sobre inline
en las preguntas frecuentes de isocpp , el código se da como
void f()
{
int x = /*...*/;
int y = /*...*/;
int z = /*...*/;
// ...code that uses x, y and z...
g(x, y, z);
// ...more code that uses x, y and z...
}
entonces dice que
Asumiendo una implementación típica de C ++ que tiene registros y una pila, los registros y los parámetros se escriben en la pila justo antes de la llamada a
g()
, luego los parámetros se leen de la pila dentro deg()
y se leen de nuevo para restaurar los registros mientrasg()
vuelve af()
. Pero eso es un montón de lecturas y escrituras innecesarias, especialmente en los casos en que el compilador puede usar registros para las variablesx
,y
,z
: cada variable puede escribirse dos veces (como registro y también como parámetro) y leer dos veces (cuando se utiliza dentro deg()
y para restaurar los registros durante el retorno af()
).
Tengo una gran dificultad para entender el párrafo anterior. Intento enumerar mis preguntas de la siguiente manera:
- Para que una computadora realice algunas operaciones en algunos datos que residen en la memoria principal, ¿es cierto que los datos deben cargarse primero en algunos registros y luego la CPU puede operar con los datos? (Sé que esta pregunta no está particularmente relacionada con C ++, pero entender esto será útil para entender cómo funciona C ++).
- Creo que
f()
es una función de la misma manera queg(x, y, z)
es una función. ¿Por quéx, y, z
antes de llamar ag()
están en los registros, y los parámetros pasados eng()
están en la pila? - ¿Cómo se sabe que las declaraciones para
x, y, z
hacen almacenadas en los registros? ¿Dónde se almacenan los datos dentro deg()
, se registran o se apilan?
PD
Es muy difícil elegir una respuesta aceptable cuando las respuestas son todas muy buenas (por ejemplo, las proporcionadas por @MatsPeterson, @TheodorosChatzigiannakis y @superultranova), creo. Personalmente me gusta el de @Potatoswatter un poco más ya que la respuesta ofrece algunas pautas.
Para que una computadora realice algunas operaciones en algunos datos que residen en la memoria principal, ¿es cierto que los datos deben cargarse primero en algunos registros y luego la CPU puede operar con los datos?
Esto depende de la arquitectura y el conjunto de instrucciones que ofrece. Pero en la práctica, sí, es el caso típico.
¿Cómo se sabe que las declaraciones para x, y, z las hacen almacenadas en los registros? ¿Dónde se almacenan los datos dentro de g (), se registran o se apilan?
Suponiendo que el compilador no elimine las variables locales, preferirá colocarlas en registros, porque los registros son más rápidos que la pila (que reside en la memoria principal, o en un caché).
Pero esto está lejos de ser una verdad universal: depende del funcionamiento interno (complicado) del compilador (cuyos detalles se detallan a mano en ese párrafo).
Creo que f () es una función de la misma manera que g (x, y, z) es una función. ¿Por qué x, y, z antes de llamar a g () están en los registros, y los parámetros pasados en g () están en la pila?
Incluso si asumimos que las variables están, de hecho, almacenadas en los registros, cuando llama a una función, se activa la convención de llamada . Es una convención que describe cómo se llama una función, dónde se pasan los argumentos, quién limpia la pila, lo que los registros se conservan.
Todas las convenciones de llamadas tienen algún tipo de sobrecarga. Una fuente de esta sobrecarga es el argumento que pasa. Muchas convenciones de llamada intentan reducir eso, prefiriendo pasar argumentos a través de registros, pero como el número de registros de CPU es limitado (en comparación con el espacio de la pila), finalmente recurren a empujar a través de la pila después de varios argumentos.
El párrafo en su pregunta asume una convención de llamada que pasa todo a través de la pila y, en base a esa suposición, lo que está tratando de decirle es que sería beneficioso (para la velocidad de ejecución) si pudiéramos "copiar" (en tiempo de compilación) el Cuerpo de la función llamada dentro de la persona que llama (en lugar de emitir una llamada a la función). Esto produciría los mismos resultados lógicamente, pero eliminaría el costo de tiempo de ejecución de la llamada a la función.
Para que una computadora realice algunas operaciones en algunos datos que residen en la memoria principal, ¿es cierto que los datos deben cargarse primero en algunos registros y luego la CPU puede operar con los datos?
Ni siquiera esta afirmación es siempre cierta. Probablemente sea cierto para todas las plataformas con las que trabajará, pero seguramente puede haber otra arquitectura que no haga uso de los registros del procesador .
Sin embargo, su computadora x86_64 lo hace.
Creo que f () es una función de la misma manera que g (x, y, z) es una función. ¿Por qué x, y, z antes de llamar a g () están en los registros, y los parámetros pasados en g () están en la pila?
¿Cómo se sabe que las declaraciones para x, y, z las hacen almacenadas en los registros? ¿Dónde se almacenan los datos dentro de g (), se registran o se apilan?
Estas dos preguntas no pueden responderse de forma única para ningún compilador y sistema en el que se compilará su código. Ni siquiera se pueden dar por sentado, ya que los parámetros de g
podrían no estar en la pila, todo depende de varios conceptos que explicaré a continuación.
Primero debe conocer las llamadas convenciones de llamada que definen, entre otras cosas, cómo se pasan los parámetros de la función (por ejemplo, empujados en la pila, colocados en registros o una combinación de ambos). El estándar C ++ no lo impone y las convenciones de llamada forman parte de la ABI , un tema más amplio relacionado con los problemas del programa de código de máquina de bajo nivel.
En segundo lugar, la asignación de registros (es decir, qué variables se cargan realmente en un registro en un momento dado) es una tarea compleja y un NP-complete . Los compiladores tratan de hacer todo lo posible con la información que tienen. En general, las variables de acceso menos frecuente se colocan en la pila, mientras que las variables de acceso más frecuente se guardan en los registros. Por lo tanto, la parte Where the data inside g() is stored, register or stack?
no se puede responder de una vez por todas, ya que depende de muchos factores, incluida la presión de registro .
Sin mencionar las optimizaciones del compilador que incluso podrían eliminar la necesidad de que existan algunas variables.
Finalmente la pregunta que has enlazado ya dice
Naturalmente, su millaje puede variar, y hay miles de variables que están fuera del alcance de este FAQ en particular, pero lo anterior sirve como un ejemplo del tipo de cosas que pueden suceder con la integración de procedimientos.
es decir, el párrafo que publicaste hace algunas suposiciones para configurar las cosas para un ejemplo. Esas son solo suposiciones y debes tratarlas como tales.
Como una pequeña adición: con respecto a los beneficios de la inline
en una función, recomiendo echar un vistazo a esta respuesta: https://.com/a/145952/1938163
Con respecto a su pregunta # 1, sí, las instrucciones sin carga / almacenamiento operan en los registros.
Con respecto a su pregunta # 2, si asumimos que los parámetros se pasan en la pila, entonces tenemos que escribir los registros en la pila, de lo contrario g () no podrá acceder a los datos, ya que el código en g () no "sabe" en qué registros están los parámetros.
Con respecto a su pregunta # 3, no se sabe si x, y, z se almacenarán en los registros en f (). Uno podría usar la palabra clave de register
, pero eso es más una sugerencia. Basándose en la convención de llamada, y suponiendo que el compilador no realiza ninguna optimización que implique el paso de parámetros, puede predecir si los parámetros están en la pila o en los registros.
Debe familiarizarse con las convenciones de llamadas. Las convenciones de llamada tratan sobre la forma en que los parámetros se pasan a las funciones y, por lo general, implican pasar parámetros en la pila en un orden específico, colocando parámetros en registros o una combinación de ambos.
stdcall
, cdecl
y fastcall
son algunos ejemplos de convenciones de llamada. En términos de paso de parámetros, stdcall y cdecl son los mismos, en los parámetros se insertan de derecha a izquierda en la pila. En este caso, si g()
era cdecl
o stdcall
la persona que llama presionaría z, y, x en ese orden:
mov eax, z
push eax
mov eax, x
push eax
mov eax, y
push eax
call g
En FastBall de 64 bits, se utilizan registros, Microsoft usa RCX, RDX, R8, R9 (más la pila para funciones que requieren más de 4 parámetros), Linux usa RDI, RSI, RDX, RCX, R8, R9. Para llamar a g () usando MS 64bit fastcall uno haría lo siguiente (asumimos que z
, x
, y y
no están en los registros)
mov rcx, x
mov rdx, y
mov r8, z
call g
Así es como los humanos escriben el ensamblaje y, a veces, los compiladores. Los compiladores utilizarán algunos trucos para evitar pasar parámetros, ya que generalmente reduce el número de instrucciones y puede reducir el número de veces que se accede a la memoria. Tome el siguiente código, por ejemplo (estoy ignorando intencionalmente las reglas de registro no volátiles):
f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
call g
mov rcx, rax
ret
g:
mov rax, rsi
add rax, rcx
add rax, rdx
ret
Para fines ilustrativos, rcx ya está en uso, y x se ha cargado en rsi. El compilador puede compilar g de tal manera que use rsi en lugar de rcx, por lo que los valores no tienen que intercambiarse entre los dos registros cuando llega el momento de llamar a g. El compilador también podría en línea g, ahora que f y g comparten el mismo conjunto de registros para x, y y z. En ese caso, la instrucción de la call g
se reemplazaría con el contenido de g, excluyendo la instrucción ret
.
f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
mov rax, rsi
add rax, rcx
add rax, rdx
mov rcx, rax
ret
Esto será incluso más rápido, porque no tenemos que lidiar con la instrucción de call
, ya que g se ha alineado en f.
Depende completamente del compilador (junto con el tipo de procesador) si una variable se almacena en la memoria o en un registro [o en algunos casos más de un registro] (y qué opciones le da al compilador, asumiendo que tiene opciones para decidir) tales cosas - la mayoría de los "buenos" compiladores hacen). Por ejemplo, el compilador LLVM / Clang utiliza un paso de optimización específico llamado "mem2reg" que mueve las variables de la memoria a los registros. La decisión de hacerlo se basa en cómo se usan las variables, por ejemplo, si toma la dirección de una variable en algún momento, debe estar en la memoria.
Otros compiladores tienen una funcionalidad similar, pero no necesariamente idéntica.
Además, al menos en los compiladores que tienen cierta apariencia de portabilidad, TAMBIÉN habrá una fase de código de máquina generatinc para el objetivo real, que contiene optimizaciones específicas del objetivo, que de nuevo pueden mover una variable de la memoria a un registro.
No es posible [sin comprender cómo funciona el compilador en particular] determinar si las variables en su código están en los registros o en la memoria. Uno puede adivinar, pero tal suposición es como adivinar otro "tipo de cosas predecibles", como mirar por la ventana para adivinar si va a llover en unas pocas horas, dependiendo de dónde viva, esto puede ser una suposición completamente aleatoria. , o bastante predecible: en algunos países tropicales, puede configurar su reloj según la lluvia que llega cada tarde, en otros países, rara vez llueve, y en algunos países, como aquí en Inglaterra, no puede saber con certeza más allá ". ahora mismo [no] está lloviendo aquí ".
Para responder a las preguntas reales:
- Esto depende del procesador. Los procesadores RISC adecuados, como ARM, MIPS, 29K, etc., no tienen instrucciones que usen operandos de memoria, excepto las instrucciones de carga y tipo de tienda. Entonces, si necesita agregar dos valores, debe cargar los valores en los registros y usar la operación de agregar en esos registros. Algunos, como x86 y 68K permiten que uno de los dos operandos sea un operando de memoria, y por ejemplo PDP-11 y VAX tienen "libertad total", ya sea que sus operandos estén en la memoria o en el registro, puede usar la misma instrucción, solo Diferentes modos de direccionamiento para los diferentes operandos.
- Su premisa original aquí es errónea: no se garantiza que haya argumentos para
g
en la pila. Esa es solo una de las muchas opciones. Muchas ABI (interfaz binaria de la aplicación, también conocidas como "convenciones de llamada") usan registros para los primeros argumentos de una función. Entonces, nuevamente, depende de qué compilador (hasta cierto punto) y qué procesador (mucho más que el compilador) el compilador apunta Si los argumentos están en memoria o en registros. - Nuevamente, esta es una decisión que toma el compilador; depende de cuántos registros tenga el procesador, cuáles están disponibles, cuál es el costo si "libera" algún registro para
x
,y
yz
, que va desde "sin costo alguno" "a" un poco "- nuevamente, dependiendo del modelo de procesador y el ABI.
Las variables casi siempre se almacenan en la memoria principal. Muchas veces, debido a las optimizaciones del compilador, el valor de su variable declarada nunca se moverá a la memoria principal, pero son las variables intermedias que utiliza en su método que no tienen relevancia antes de que se llame a cualquier otro método (es decir, ocurrencia de la operación de pila).
Esto es así por diseño: para mejorar el rendimiento, ya que es más fácil (y mucho más rápido) que el procesador aborde y manipule los datos en los registros. Los registros arquitectónicos tienen un tamaño limitado, por lo que no se puede colocar todo en los registros. Incluso si ''insinúa'' a su compilador para que lo registre, eventualmente, el sistema operativo puede administrarlo fuera del registro, en la memoria principal, si los registros disponibles están llenos.
Probablemente, una variable estará en la memoria principal porque mantendrá la relevancia en la ejecución cercana y puede depender durante un período más largo de tiempo de CPU. Una variable está en el registro arquitectónico porque tiene relevancia en las próximas instrucciones de la máquina y la ejecución será casi inmediata, pero puede que no sea relevante por mucho tiempo.
No puede saber, sin mirar el lenguaje ensamblador, si una variable está en un registro, pila, montón, memoria global o en otro lugar. Una variable es un concepto abstracto. El compilador puede usar registros u otra memoria que elija, siempre y cuando la ejecución no se modifique .
También hay otra regla que afecta a este tema. Si toma la dirección de una variable y la almacena en un puntero, la variable no se puede colocar en un registro porque los registros no tienen direcciones.
El almacenamiento variable también puede depender de la configuración de optimización del compilador. Las variables pueden desaparecer debido a la simplificación. Las variables que no cambian de valor pueden colocarse en el ejecutable como una constante.
No tomes ese párrafo muy en serio. Parece estar haciendo suposiciones excesivas y luego entrar en detalles excesivos, que en realidad no pueden generalizarse.
Pero, tus preguntas son muy buenas.
- Para que una computadora realice algunas operaciones en algunos datos que residen en la memoria principal, ¿es cierto que los datos deben cargarse primero en algunos registros y luego la CPU puede operar con los datos? (Sé que esta pregunta no está particularmente relacionada con C ++, pero entender esto será útil para entender cómo funciona C ++).
Más o menos, todo necesita ser cargado en los registros. La mayoría de las computadoras se organizan en torno a una ruta de datos , un bus que conecta los registros, los circuitos aritméticos y el nivel superior de la jerarquía de memoria. Generalmente, todo lo que se transmite en la ruta de datos se identifica con un registro.
Puede recordar el gran debate RISC vs CISC. Uno de los puntos clave fue que el diseño de una computadora puede ser mucho más simple si la memoria no tiene permiso para conectarse directamente a los circuitos aritméticos.
En las computadoras modernas, hay registros arquitectónicos , que son una construcción de programación como una variable, y registros físicos , que son circuitos reales. El compilador hace un montón de trabajo pesado para realizar un seguimiento de los registros físicos mientras genera un programa en términos de registros arquitectónicos. Para un conjunto de instrucciones CISC como x86, esto puede implicar la generación de instrucciones que envían operandos en la memoria directamente a operaciones aritméticas. Pero detrás de las escenas, se registra todo el camino hacia abajo.
En pocas palabras: simplemente deja que el compilador haga su trabajo.
- Creo que f () es una función de la misma manera que g (x, y, z) es una función. ¿Por qué x, y, z antes de llamar a g () están en los registros, y los parámetros pasados en g () están en la pila?
Cada plataforma define una forma para que las funciones de C se llamen entre sí. Pasar parámetros en registros es más eficiente. Pero, hay compensaciones y el número total de registros es limitado. Las ABI antiguas a menudo sacrifican la eficiencia por simplicidad y las ponen todas en la pila.
Línea inferior: el ejemplo es asumir arbitrariamente un ABI ingenuo.
- ¿Cómo se sabe que las declaraciones para x, y, z las hacen almacenadas en los registros? ¿Dónde se almacenan los datos dentro de g (), se registran o se apilan?
El compilador tiende a preferir usar registros para los valores de acceso más frecuente. Nada en el ejemplo requiere el uso de la pila. Sin embargo, los valores a los que se accede con menos frecuencia se colocarán en la pila para que haya más registros disponibles.
Solo cuando toma la dirección de una variable, como por &x
o pasando por referencia, y esa dirección se escapa del inliner, el compilador debe usar la memoria y no los registros.
En pocas palabras: evite tomar direcciones y pasarlas o almacenarlas de cualquier manera.
Respuesta corta: No puedes. Depende completamente de tu compilador y de las funciones de optimización habilitadas.
La preocupación del compilador es traducir en ensamblador su programa, pero la forma en que se hace está estrechamente relacionada con el funcionamiento de su compilador. Algunos compiladores le permiten insinuar qué variable de mapa registrar. Ver ejemplo para este ejemplo: https://gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html
Su compilador aplicará transformaciones a su código para obtener algo, puede ser el rendimiento, puede ser un tamaño de código más bajo, y aplica funciones de costo para estimar estas ganancias, por lo que normalmente solo puede ver el resultado desarmando la unidad compilada.