¿No es el uso de variables de puntero una sobrecarga de memoria?


29

En lenguajes como C y C ++, al usar punteros a variables necesitamos una ubicación de memoria más para almacenar esa dirección. Entonces, ¿no es esto una sobrecarga de memoria? ¿Cómo se compensa esto? ¿Se utilizan punteros en aplicaciones críticas de poca memoria?


11
Los beneficios de la asignación dinámica de memoria superan ampliamente el costo del puntero.
James McLeod

32
¿Cómo crees que otros lenguajes (Java, C #, ...) almacenan referencias a objetos? (Sugerencia: usan punteros).
Erik Eidt

66
Un puntero puede sentarse en registros o pasar como argumento. En ambos casos no hay sobrecarga de memoria obvia . Y el puntero podría calcularse (por ejemplo, a través de aritmética de puntero, funciones que devuelven punteros, etc.)
Basile Starynkevitch

9
¿Qué tal pasar un argumento de estructura (grande) por dirección? Si cuenta eso como una variable de puntero, es inevitable para muchos algoritmos, ¡y usa mucho menos espacio que pasar la estructura por valor!
PJTraill

44
Esto tiene la sensación de la tarea de algunos. La pregunta está diseñada para explorar si la respuesta comprende los punteros y cómo se usan.
Michael Shaw

Respuestas:


34

En realidad, la sobrecarga no reside realmente en los 4 u 8 bytes adicionales necesarios para almacenar el puntero. La mayoría de las veces se utilizan punteros para la asignación dinámica de memoria , lo que significa que invocamos una función para asignar un bloque de memoria, y esta función nos devuelve un puntero que apunta a ese bloque de memoria. Este nuevo bloque en sí mismo representa una sobrecarga considerable.

Ahora, no tiene que comprometerse en la asignación de memoria para usar un puntero: puede tener una matriz intdeclarada estáticamente o en la pila, y puede usar un puntero en lugar de un índice para visitar el ints, y es Todo muy agradable, sencillo y eficiente. No se necesita asignación de memoria, y el puntero generalmente ocupará exactamente tanto espacio en la memoria como lo haría un índice entero.

Además, como Joshua Taylor nos recuerda en un comentario, los punteros se utilizan para pasar algo por referencia. Por ejemplo, struct foo f; init_foo(&f);asignaría f en la pila y luego llamaría init_foo()con un puntero a eso struct. Eso es muy comun. (Solo tenga cuidado de no pasar esos punteros "hacia arriba"). En C ++ puede ver que esto se hace con una "referencia" (foo& ) en lugar de un puntero, pero las referencias no son más que punteros que no puede alterar, y ocupan el misma cantidad de memoria

Pero la razón principal por la que se utilizan punteros es para la asignación dinámica de memoria, y esto se hace para resolver problemas que de otra manera no podrían resolverse. Aquí hay un ejemplo simplista: imagine que desea leer todo el contenido de un archivo. ¿Dónde los vas a guardar? Si intenta con un búfer de tamaño fijo, solo podrá leer archivos que no sean más largos que ese búfer. Pero al usar la asignación de memoria, puede asignar tanta memoria como sea necesaria para leer el archivo y luego proceder a leerlo.

Además, C ++ es un lenguaje orientado a objetos, y hay ciertos aspectos de la POO como la abstracción que solo se pueden lograr con punteros. Incluso los lenguajes como Java y C # hacen un uso extensivo de los punteros, simplemente no le permiten manipular directamente los punteros, para evitar que haga cosas peligrosas con ellos, pero aún así, estos lenguajes solo comienzan a tener sentido una vez que tiene me di cuenta de que detrás de escena todo se hace usando punteros.

Por lo tanto, los punteros no solo se usan en aplicaciones de poca memoria y tiempo crítico, sino que se usan en todas partes .


55
"La razón principal por la que se utilizan los punteros es para la asignación dinámica de memoria". Ese puede ser el caso en C ++, pero no necesariamente en C. En C, los punteros son su única forma de pasar algo por referencia. Si no desea copiar una estructura completa, necesitará un puntero. Eso todavía tiene sentido incluso si no está haciendo ninguna asignación dinámica. Por ejemplo, struct foo f; init_foo(&f);se asignaría fen la pila y luego llamaría init_foocon un puntero a esa estructura. Eso es muy comun. (Solo tenga cuidado de no pasar esos punteros "hacia arriba".)
Joshua Taylor

@JoshuaTaylor eso es muy correcto, me había olvidado de eso. ¿Puedo modificarlo a mi respuesta? (Esto se considera una buena práctica en los programadores SE porque los comentarios son efímeros, mientras que las respuestas no.)
Mike Nakis

Por supuesto, agréguele su respuesta. :)
Joshua Taylor

2
Esto representa una sobrecarga considerable en sí misma, porque este bloque tendrá un encabezado (oculto) que generalmente será tan grande como unos pocos punteros. => En realidad, las implementaciones modernas malloctienen una sobrecarga de encabezado MUY BAJA ya que agrupan bloques asignados en "cubos". Por otro lado, esto se traduce en una sobreasignación en general: pide 35 bytes y obtiene 64 (sin su conocimiento) desperdiciando así 29 ...
Matthieu M.

1
@ MikeNakis: Se ve mejor, gracias por seguir conmigo :)
Matthieu M.

35

Entonces, ¿no es esto una sobrecarga de memoria?

Claro, una dirección adicional (generalmente 4/8 bytes dependiendo del procesador).

¿Cómo se compensa esto?

No lo es. Si necesita la indirección necesaria para los punteros, puede pagarla.

¿Se utilizan punteros en aplicaciones críticas de poca memoria?

No he trabajado mucho allí, pero supongo que sí. El acceso al puntero es un aspecto elemental de la programación del ensamblaje. Se necesitan cantidades triviales de memoria y las operaciones de puntero son rápidas, incluso en el contexto de este tipo de aplicaciones.


1
@DavidGrinberg Eso solo supone que no hay optimización del valor de retorno .
Darkhogg

3
Usé punteros al escribir aplicaciones TSR para DOS que tenían que caber en 15k en los años ochenta. Entonces sí, se usan en aplicaciones con poca memoria.
Gort the Robot

1
@StevenBurnap ¿Llamas 15k con poca memoria para una aplicación TSR? Una vez escribí una herramienta TSR que consumía solo 16 bytes de memoria.
kasperd

22
@kasperd: Y en mi día, solo teníamos ceros
Jack


11

No tengo el mismo giro en esto que Telastyn.

Los globales del sistema en un procesador integrado pueden ser direccionados con direcciones específicas codificadas.

Los globales en un programa se abordarán como un desplazamiento desde un puntero especial que apunta al lugar en la memoria donde se almacenan los globales y las estadísticas.

Las variables locales aparecen cuando se ingresa una función y se direccionan como un desplazamiento desde otro puntero especial, a menudo llamado "puntero de marco". Esto incluye los argumentos de la función. Si tiene cuidado con las pulsaciones y los estallidos con el puntero de la pila, puede eliminar el puntero del marco y acceder a las variables locales directamente desde el puntero de la pila.

Por lo tanto, paga por la indirección de los punteros, ya sea que esté avanzando a través de una matriz o simplemente tomando alguna variable local o global sin importancia. Simplemente se basa en un puntero diferente, dependiendo de qué tipo de variable sea. El código que se compila bien mantendrá ese puntero en un registro de CPU, en lugar de volver a cargarlo cada vez que se usa.


6

Sí, por supuesto. Pero es un acto de equilibrio.

Las aplicaciones con poca memoria normalmente se construirían teniendo en cuenta el equilibrio entre la sobrecarga de algunas variables de puntero en comparación con la sobrecarga de lo que sería un programa masivo (que debe almacenarse en la memoria, ¡recuerde!) Si los punteros no pudieran usarse .

Esta consideración se aplica a todos los programas, porque nadie quiere crear un desastre horrible e imposible de mantener con el código duplicado a la izquierda y al centro, que es veinte veces más grande de lo que debería ser.


5

En lenguajes como C y C ++, al usar punteros a variables necesitamos una ubicación de memoria más para almacenar esa dirección. Entonces, ¿no es esto una sobrecarga de memoria?

Asume que el puntero necesita ser almacenado. Ese no es siempre el caso. Cada variable se almacena en alguna dirección de memoria. Digamos que tiene un longdeclarado como long n = 5L;. Esto asigna almacenamiento para nen alguna dirección. Podemos usar esa dirección para hacer cosas elegantes como *((char *) &n) = (char) 0xFF;manipular partes de n. La dirección de nno se almacena en ninguna parte como una sobrecarga adicional.

¿Cómo se compensa esto?

Incluso si los punteros se almacenan explícitamente (por ejemplo, en estructuras de datos como listas), la estructura de datos resultante a menudo es más elegante (más simple, más fácil de entender, más fácil de manejar, etc.) que una estructura de datos equivalente sin punteros.

¿Se utilizan punteros en aplicaciones críticas de poca memoria?

Sí. Los dispositivos que usan microcontroladores a menudo contienen muy poca memoria, pero el firmware puede usar punteros para manejar vectores de interrupción o gestión de búfer, etc.


Algunas variables no se almacenan en la memoria, sino solo en registros
Basile Starynkevitch

@BasileStarynkevitch: puede sugerir que las variables permanezcan en los registros, pero no se requiere que el compilador lo haga. A menos que esté programando en ensamblador, realmente no tiene ese nivel de control sobre el almacenamiento inmediato. E incluso en el ensamblador, cualquier subrutina que invoque probablemente derrame los registros en la pila para que pueda usarlos para sus variables. Entonces, para cualquier programa no trivial, está casi garantizado que sus variables pasarán al menos algún tiempo en la memoria.
TMN

Se puede permitir que el compilador almacene algunas variables locales solo en registros, y la mayoría de los compiladores optimizadores lo están haciendo (intente gcc -fverbose-asm -S -O2compilar algún código C)
Basile Starynkevitch

@BasileStarynkevitch No estoy seguro del punto que está tratando de hacer con la observación de que las variables pueden almacenarse puramente en registros. ¿Podrías por favor elaborar?
Lawrence

5

Tener un puntero definitivamente consume algo de sobrecarga, pero también puedes ver el lado positivo. El puntero es como un índice. En C puede usar estructuras de datos complejas como cadenas y estructuras debido solo a punteros.

De hecho, suponga que desea pasar una variable por referencia, entonces es fácil mantener un puntero en lugar de replicar toda la estructura y sincronizar los cambios entre ellos (incluso para copiarlos necesitará un puntero). ¿Cómo trataría las asignaciones y desasignaciones de memoria no contiguas sin puntero?

Incluso sus variables normales tienen una entrada en la tabla de símbolos que almacena la dirección hacia donde apunta su variable. Por lo tanto, no creo que genere demasiada sobrecarga en términos de memoria (solo 4 u 8 bytes). Incluso los lenguajes como Java usan punteros internamente (referencia), simplemente no permiten manipularlos, ya que hará que JVM sea menos seguro.

Debe usar punteros solo cuando no tenga otra opción, como los tipos de datos faltantes, las estructuras (en c), ya que el uso de punteros puede generar errores si no se maneja adecuadamente y es relativamente más difícil de depurar.


3

Entonces, ¿no es esto una sobrecarga de memoria?

¿Si no talvez?

Esta es una pregunta incómoda porque imagina el rango de direccionamiento de memoria en la máquina y un software que necesita hacer un seguimiento constante de dónde están las cosas en la memoria de una manera que no pueda vincularse a la pila.

Por ejemplo, imagine un reproductor de música donde el usuario carga el archivo de música con solo presionar un botón y lo descarga de la memoria volátil cuando el usuario intenta cargar otro archivo de música.

¿Cómo hacemos un seguimiento de dónde se almacenan los datos de audio? Necesitamos una dirección de memoria para ello. El programa no solo necesita hacer un seguimiento de la porción de datos de audio en la memoria, sino también de dónde está en la memoria. Por lo tanto, debemos mantener una dirección de memoria (es decir, un puntero). Y el tamaño del almacenamiento requerido para la dirección de memoria coincidirá con el rango de direccionamiento de la máquina (por ejemplo: puntero de 64 bits para un rango de direccionamiento de 64 bits).

Por lo tanto, es un tipo de "sí", requiere almacenamiento para realizar un seguimiento de una dirección de memoria, pero no es que podamos evitarlo para la memoria asignada dinámicamente de este tipo.

¿Cómo se compensa esto?

Hablando solo del tamaño de un puntero en sí mismo, puede evitar el costo en algunos casos utilizando la pila, por ejemplo, en ese caso, los compiladores pueden generar instrucciones que codifican efectivamente la dirección de memoria relativa, evitando el costo de un puntero. Sin embargo, esto lo deja vulnerable a desbordamientos de pila si hace esto para asignaciones grandes y de tamaño variable, y también tiende a ser poco práctico (si no imposible) para una serie compleja de ramas impulsadas por la entrada del usuario (como en el ejemplo de audio encima).

Otra forma es usar estructuras de datos más contiguas. Por ejemplo, se podría usar una secuencia basada en una matriz en lugar de una lista doblemente vinculada que requiere dos punteros por nodo. También podemos usar un híbrido de estos dos como una lista desenrollada que almacena solo punteros entre cada grupo contiguo de N elementos.

¿Se utilizan punteros en aplicaciones críticas de poca memoria?

Sí, muy comúnmente, ya que muchas aplicaciones críticas para el rendimiento están escritas en C o C ++ que están dominadas por el uso del puntero (pueden estar detrás de un puntero inteligente o un contenedor como std::vectoro std::string, pero la mecánica subyacente se reduce a un puntero que se usa para realizar un seguimiento de la dirección a un bloque de memoria dinámica).

Ahora volviendo a esta pregunta:

¿Cómo se compensa esto? (La segunda parte)

Los punteros suelen ser muy baratos a menos que los esté almacenando como un millón de ellos (que todavía es miserable * 8 megabytes en una máquina de 64 bits).

* Tenga en cuenta que Ben señaló que un "miserable" de 8 megas sigue siendo el tamaño de la caché L3. Aquí usé "miserablemente" más en el sentido del uso total de DRAM y el tamaño relativo típico de los fragmentos de memoria que señalará un uso saludable de punteros.

Donde los punteros se vuelven caros no son los punteros en sí, sino:

  1. Asignación de memoria dinámica. La asignación de memoria dinámica tiende a ser costosa, ya que tiene que pasar por una estructura de datos subyacente (por ejemplo, amigo o asignador de losas). Aunque a menudo están optimizados hasta la muerte, son de uso general y están diseñados para manejar bloques de tamaño variable que requieren que hagan al menos un poco de trabajo parecido a una "búsqueda" (aunque ligera y posiblemente incluso en tiempo constante) para encuentre un conjunto gratuito de páginas contiguas en la memoria.

  2. Acceso a la memoria. Esto tiende a ser la mayor sobrecarga de la que preocuparse. Cada vez que accedemos a la memoria asignada dinámicamente por primera vez, hay un error de página obligatorio, así como errores de caché al mover la memoria hacia abajo de la jerarquía de memoria y hacia abajo en un registro.

Acceso a memoria

El acceso a la memoria es uno de los aspectos más críticos del rendimiento más allá de los algoritmos. Muchos campos críticos para el rendimiento, como los motores de juegos AAA, concentran gran parte de su energía en optimizaciones orientadas a datos que se reducen a patrones y diseños de acceso a la memoria más eficientes.

Una de las mayores dificultades de rendimiento de los lenguajes de nivel superior que desean asignar cada tipo definido por el usuario por separado a través de un recolector de basura, por ejemplo, es que pueden fragmentar bastante la memoria. Esto puede ser especialmente cierto si no todos los objetos se asignan a la vez.

En esos casos, si almacena una lista de un millón de instancias de un tipo de objeto definido por el usuario, acceder a esas instancias secuencialmente en un bucle podría ser bastante lento, ya que es análogo a una lista de un millón de punteros que apuntan a regiones dispares de memoria. En esos casos, la arquitectura quiere obtener memoria de niveles superiores, más lentos y más grandes de la jerarquía en grandes fragmentos alineados con la esperanza de que se pueda acceder a los datos circundantes en esos fragmentos antes del desalojo. Cuando cada objeto en una lista de este tipo se asigna por separado, a menudo terminamos pagándolo con errores de caché cuando cada iteración posterior podría tener que cargarse desde un área completamente diferente en la memoria sin que se acceda a objetos adyacentes antes del desalojo.

Muchos de los compiladores para tales idiomas están haciendo un gran trabajo en estos días en la selección de instrucciones y la asignación de registros, pero la falta de un control más directo sobre la administración de memoria aquí puede ser mortal (aunque a menudo menos propenso a errores) y aún hace que los idiomas sean como C y C ++ bastante populares.

Optimización indirecta del acceso al puntero

En los escenarios más críticos para el rendimiento, las aplicaciones a menudo usan agrupaciones de memoria que agrupan la memoria de fragmentos contiguos para mejorar la localidad de referencia. En tales casos, incluso una estructura vinculada, como un árbol o una lista vinculada, puede hacerse compatible con la memoria caché siempre que el diseño de memoria de sus nodos sea de naturaleza contigua. Esto efectivamente está haciendo más barata la desreferenciación de punteros, aunque indirectamente al mejorar la localidad de referencia involucrada al desreferenciarlos.

Persiguiendo punteros alrededor

Supongamos que tenemos una lista de enlaces individuales como:

Foo->Bar->Baz->null

El problema es que si asignamos todos estos nodos por separado contra un asignador de propósito general (y posiblemente no todos a la vez), la memoria real podría dispersarse de esta manera (diagrama simplificado):

ingrese la descripción de la imagen aquí

Cuando comenzamos a perseguir punteros y accedemos al Foonodo, comenzamos con una falta obligatoria (y posiblemente una falla de página) moviendo un fragmento de su región de memoria de regiones de memoria más lentas a regiones de memoria más rápidas, así:

ingrese la descripción de la imagen aquí

Esto hace que almacenemos en caché (posiblemente también la página) una región de memoria solo para acceder a una parte de ella y desalojar el resto mientras buscamos punteros alrededor de esta lista. Al tomar el control sobre el asignador de memoria, sin embargo, podemos asignar una lista contigua de esta manera:

ingrese la descripción de la imagen aquí

... y así mejorar significativamente la velocidad a la que podemos desreferenciar estos punteros y procesar sus punteros. Entonces, aunque sea muy indirecto, podemos acelerar el acceso del puntero de esta manera. Por supuesto, si solo los almacenamos contiguamente en una matriz, no tendríamos este problema en primer lugar, pero el asignador de memoria aquí, que nos da un control explícito sobre el diseño de la memoria, puede salvar el día en que se requiere una estructura vinculada.

* Nota: este es un diagrama y una discusión muy simplificados sobre la jerarquía de memoria y la localidad de referencia, pero es de esperar que sea apropiado para el nivel de la pregunta.


1
"miserable 8 megabytes" es el tamaño típico de todo el caché L3 en una CPU moderna
Ben Voigt

1
@BenVoigt Trataré de desambiguarlo un poco. Por supuesto, eso sería horrible si cada puntero apuntara a un fragmento de memoria de 32 bits, por ejemplo

@BenVoigt ¡Se agregó una nota al pie para tratar de aclarar esa parte!

Esa aclaración se ve bien.
Ben Voigt

1

Entonces, ¿no es esto una sobrecarga de memoria?

De hecho, es una sobrecarga de memoria, pero muy pequeña (hasta el punto de insignificancia).

¿Cómo se compensa esto?

No es compensado. Debe darse cuenta de que el acceso a los datos a través de un puntero (desreferenciar un puntero) es extremadamente rápido (si no recuerdo mal, usa solo una instrucción de ensamblaje por desreferencia). Es lo suficientemente rápido como para que en muchos casos sea la alternativa más rápida que tenga.

¿Se utilizan punteros en aplicaciones críticas de poca memoria?

Sí.


0

Solo necesita el uso de memoria adicional (4-8 bytes por puntero, normalmente) mientras necesita ese puntero. Hay muchas técnicas que hacen que esto sea más asequible.

La técnica más fundamental que hace que los punteros sean potentes es que no es necesario mantener todos los punteros. A veces puede usar un algoritmo para construir un puntero desde un puntero a otra cosa. El ejemplo más trivial de esto es la aritmética de matrices. Si asigna una matriz de 50 enteros, no necesita mantener 50 punteros, uno para cada entero. Normalmente realiza un seguimiento de un puntero (el primero) y utiliza la aritmética del puntero para generar los otros sobre la marcha. A veces puede mantener uno de esos punteros en un elemento específico de la matriz temporalmente, pero solo mientras lo necesite. Una vez que haya terminado, puede descartarlo, siempre y cuando haya guardado suficiente información para regenerarlo más tarde, si lo necesita. Esto puede sonar trivial, pero es exactamente el tipo de herramientas de conservación que usted '

En situaciones de memoria extremadamente ajustadas, esto se puede usar para minimizar el costo. Si está trabajando en un espacio de memoria muy limitado, generalmente tiene una buena idea de cuántos objetos necesita manipular. En lugar de asignar un grupo de enteros uno a la vez y mantener punteros completos para ellos, puede aprovechar su conocimiento del desarrollador de que nunca tendrá más de 256 enteros en este algoritmo en particular. En ese caso, puede mantener un puntero al primer entero y realizar un seguimiento de un índice utilizando un carácter (1 byte) en lugar de utilizar un indicador completo (4/8 bytes). También puede usar trucos algorítmicos para generar algunos de estos índices sobre la marcha.

Este tipo de conciencia de memoria fue muy popular en el pasado. Por ejemplo, los juegos de NES dependerían ampliamente de su capacidad para acumular datos y generar punteros algorítmicamente en lugar de tener que almacenarlos al por mayor.

Las situaciones extremas de memoria también pueden llevarlo a hacer cosas como asignar todos los espacios en los que opera en tiempo de compilación. Luego, el puntero que tiene que almacenar en esa memoria se almacena en el programa en lugar de los datos. En muchas situaciones de memoria limitada, tiene memoria de programa y datos separada (a menudo ROM vs RAM), por lo que puede ajustar la forma en que usa su algoritmo para insertar los punteros en la memoria de ese programa.

Básicamente, no puedes deshacerte de todos los gastos generales. Sin embargo, puedes controlarlo. Mediante el uso de técnicas algorítmicas, puede minimizar la cantidad de punteros que puede almacenar. Si está utilizando punteros para la memoria dinámica, nunca se encontrará por debajo del costo de mantener 1 puntero en ese punto de memoria dinámica, porque esa es la mínima cantidad de información necesaria para acceder a cualquier cosa en ese bloque de memoria. Sin embargo, en escenarios de restricción de memoria ultra apretada, este tiende a ser el caso especial (la memoria dinámica y las restricciones de memoria ultra apretada tienden a no aparecer en las mismas situaciones).


0

En muchas situaciones, los punteros realmente ahorran memoria. Una alternativa común al uso de punteros es hacer una copia de una estructura de datos. Una copia completa de una estructura de datos será más grande que un puntero.

Un ejemplo de una aplicación de tiempo crítico es una pila de red. Una buena pila de red estará diseñada para ser "copia cero", y para hacer esto se requiere un uso inteligente de los punteros.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.