L'uso delle variabili del puntatore non è un sovraccarico di memoria?


29

In linguaggi come C e C ++, mentre utilizziamo i puntatori alle variabili abbiamo bisogno di un'altra posizione di memoria per memorizzare quell'indirizzo. Quindi questo non è un sovraccarico di memoria? Come viene compensato? I puntatori vengono utilizzati in applicazioni con memoria insufficiente nel tempo?


11
I vantaggi dell'allocazione dinamica della memoria superano di gran lunga il costo del puntatore.
James McLeod,

32
Come pensi che gli altri linguaggi (Java, C #, ...) memorizzino i riferimenti agli oggetti? (Suggerimento: usano i puntatori).
Erik Eidt,

6
Un puntatore potrebbe trovarsi nei registri o essere passato come argomento. In entrambi i casi non vi è alcun evidente sovraccarico di memoria. E il puntatore potrebbe essere calcolato (ad es. Tramite aritmetica del puntatore, funzioni che restituiscono puntatori, ecc.)
Basile Starynkevitch,

9
Che ne dite di passare un (grande) struct argomento per indirizzo? Se lo consideri come una variabile puntatore, è inevitabile per molti algoritmi e utilizza molto meno spazio del passare la struttura per valore!
PJTraill,

4
Questo ha la sensazione di alcuni compiti a casa. La domanda è progettata per esplorare se la risposta comprende i puntatori e come vengono utilizzati.
Michael Shaw,

Risposte:


34

In realtà, l'overhead non risiede davvero nei 4 o 8 byte extra necessari per memorizzare il puntatore. La maggior parte delle volte i puntatori vengono utilizzati per l'allocazione dinamica della memoria , il che significa che invochiamo una funzione per allocare un blocco di memoria, e questa funzione ci restituisce un puntatore che punta a quel blocco di memoria. Questo nuovo blocco in sé e per sé rappresenta un notevole sovraccarico.

Ora, non è necessario impegnarsi nell'allocazione di memoria per utilizzare un puntatore: è possibile avere un array di intdichiarato staticamente o nello stack e è possibile utilizzare un puntatore anziché un indice per visitare la ints, ed è tutto molto bello, semplice ed efficiente. Non è necessaria l'allocazione di memoria e il puntatore di solito occupa esattamente lo stesso spazio in memoria di un indice intero.

Inoltre, come Joshua Taylor ci ricorda in un commento, i puntatori vengono utilizzati per passare qualcosa per riferimento. Ad esempio, struct foo f; init_foo(&f);allocare f nello stack e quindi chiamare init_foo()con un puntatore a quello struct. È molto comune (Fai solo attenzione a non passare quei puntatori "verso l'alto"). In C ++ potresti vedere che questo viene fatto con un "riferimento" ( foo&) invece di un puntatore, ma i riferimenti non sono altro che puntatori che non puoi modificare e occupano il stessa quantità di memoria.

Ma il motivo principale per cui vengono utilizzati i puntatori è per l'allocazione dinamica della memoria, e ciò viene fatto al fine di risolvere problemi che altrimenti non potrebbero essere risolti. Ecco un esempio semplicistico: immagina di voler leggere l'intero contenuto di un file. Dove li conserverai? Se provi con un buffer di dimensioni fisse, sarai in grado di leggere solo file che non sono più lunghi di quel buffer. Ma utilizzando l'allocazione di memoria, è possibile allocare tutta la memoria necessaria per leggere il file e quindi procedere con la lettura.

Inoltre, C ++ è un linguaggio orientato agli oggetti e ci sono alcuni aspetti di OOP come l' astrazione che possono essere raggiunti solo usando i puntatori. Anche linguaggi come Java e C # fanno ampio uso di puntatori, semplicemente non ti permettono di manipolarli direttamente, in modo da impedirti di fare cose pericolose con loro, ma comunque, questi linguaggi iniziano a dare senso solo una volta che hai capito che dietro le quinte tutto è fatto usando i puntatori.

Pertanto, i puntatori non vengono utilizzati solo in applicazioni a bassa memoria e tempi critici, ma vengono utilizzati ovunque .


5
"il motivo principale per cui vengono usati i puntatori è per l'allocazione dinamica della memoria" Questo può essere il caso in C ++, ma non necessariamente in C. In C, i puntatori sono il tuo unico modo per passare qualcosa per riferimento. Se non vuoi copiare un'intera struttura, avrai bisogno di un puntatore ad essa. Ciò ha ancora senso anche se non stai eseguendo alcuna allocazione dinamica. Ad esempio, struct foo f; init_foo(&f);allocerebbe fnello stack e quindi chiamerebbe init_foocon un puntatore a quella struttura. È molto comune (Fai solo attenzione a non passare quei puntatori "verso l'alto").
Joshua Taylor,

@JoshuaTaylor è molto corretto, me ne ero dimenticato. Posso modificarlo alla mia risposta? (Questa è considerata una buona pratica per i programmatori SE perché i commenti sono effimeri, mentre le risposte no).
Mike Nakis,

Aggiungilo sicuramente la tua risposta. :)
Joshua Taylor,

2
Ciò rappresenta un notevole sovraccarico in sé e per sé, poiché questo blocco avrà un'intestazione (nascosta) che di solito sarà grande come alcuni puntatori. => In realtà, le implementazioni moderne mallochanno un sovraccarico di intestazione MOLTO BASSO in quanto raggruppano i blocchi allocati in "bucket". D'altra parte, questo si traduce in un'eccessiva allocazione in generale: chiedi 35 byte e ottieni 64 (a tua insaputa) sprecando così 29 ...
Matthieu M.

1
@MikeNakis: sembra migliore, grazie per essere rimasto con me :)
Matthieu M.

35

Quindi questo non è un sovraccarico di memoria?

Certo, un indirizzo aggiuntivo (generalmente 4/8 byte a seconda del processore).

Come viene compensato?

Non è. Se hai bisogno dell'indirizzamento necessario per i puntatori, puoi pagarlo.

I puntatori vengono utilizzati in applicazioni con memoria insufficiente nel tempo?

Non ho fatto molto lavoro lì, ma lo suppongo. L'accesso al puntatore è un aspetto elementare della programmazione degli assiemi. Ci vogliono quantità insignificanti di memoria e le operazioni del puntatore sono veloci, anche nel contesto di questo tipo di applicazioni.


1
@DavidGrinberg Suppone solo che non ci sia ottimizzazione del valore di ritorno .
Darkhogg,

3
Ho usato i puntatori durante la scrittura di applicazioni TSR per DOS che dovevano rientrare nel 15k negli anni ottanta. Quindi sì, sono utilizzati in applicazioni a memoria insufficiente.
Gort il robot il

1
@StevenBurnap Hai chiamato 15k di memoria insufficiente per un'app TSR? Una volta ho scritto uno strumento TSR che ha consumato solo 16 byte di memoria.
Kasperd,

22
@kasperd: E ai miei tempi, avevamo solo zero
Jack


11

Non ho lo stesso effetto su Telastyn.

I globi di sistema in un processore incorporato potrebbero essere indirizzati con indirizzi specifici e codificati.

I globi in un programma saranno indirizzati come offset da un puntatore speciale che punta al posto in memoria in cui sono memorizzati i globi e la statica.

Le variabili locali vengono visualizzate quando si immette una funzione e vengono indirizzate come offset da un altro puntatore speciale, spesso chiamato "puntatore frame". Ciò include gli argomenti della funzione. Se stai attento ai push e ai pop con il puntatore dello stack, puoi eliminare il puntatore al frame e accedere alle variabili locali direttamente dal puntatore dello stack.

Quindi paghi per l'indirizzamento indiretto dei puntatori, indipendentemente dal fatto che tu stia avanzando attraverso un array o semplicemente prendendo qualche variabile locale o globale non significativa. È solo basato su un puntatore diverso, a seconda di che tipo di variabile sia. Il codice compilato bene manterrà quel puntatore in un registro CPU, invece di ricaricarlo ogni volta che viene utilizzato.


6

Sì, naturalmente. Ma è un atto di bilanciamento.

Generalmente, le applicazioni a memoria insufficiente verrebbero costruite tenendo presente il compromesso tra il sovraccarico di alcune variabili del puntatore rispetto al sovraccarico di quello che sarebbe un programma enorme (che deve essere memorizzato in memoria, ricordate!) Se i puntatori non potessero essere utilizzati .

Questa considerazione vale per tutti i programmi, perché nessuno vuole creare un disastro orribile, non mantenibile con codice duplicato a destra e al centro, che è venti volte più grande di quanto deve essere.


5

In linguaggi come C e C ++, mentre utilizziamo i puntatori alle variabili abbiamo bisogno di un'altra posizione di memoria per memorizzare quell'indirizzo. Quindi questo non è un sovraccarico di memoria?

Si presuppone che il puntatore debba essere memorizzato. Questo non è sempre il caso. Ogni variabile è memorizzata in qualche indirizzo di memoria. Di che hai un longdichiarato come long n = 5L;. Questo alloca memoria per un ncerto indirizzo. Possiamo usare quell'indirizzo per fare cose fantasiose come *((char *) &n) = (char) 0xFF;manipolare parti di n. L'indirizzo di nnon viene memorizzato da nessuna parte come sovraccarico aggiuntivo.

Come viene compensato?

Anche se i puntatori vengono archiviati in modo esplicito (ad esempio in strutture di dati come gli elenchi), la struttura di dati risultante è spesso più elegante (più semplice, più facile da comprendere, più facile da gestire, ecc.) Di una struttura di dati equivalente senza puntatori.

I puntatori vengono utilizzati in applicazioni con memoria insufficiente nel tempo?

Sì. I dispositivi che utilizzano microcontrollori spesso contengono pochissima memoria ma il firmware potrebbe utilizzare i puntatori per gestire i vettori di interruzione o la gestione del buffer, ecc.


Alcune variabili non sono archiviate in memoria, ma solo nei registri
Basile Starynkevitch

@BasileStarynkevitch: puoi suggerire che le variabili rimangano nei registri, ma il compilatore non è tenuto a farlo. A meno che tu non stia programmando in assembler, non hai davvero quel livello di controllo sulla memoria immediata. E anche nell'assemblatore, qualsiasi subroutine che invocherai probabilmente riverserà i registri nello stack in modo che possa usarli per le sue variabili. Quindi, per qualsiasi programma non banale, è quasi garantito che le variabili trascorrano almeno un po 'di tempo in memoria.
TMN,

Al compilatore può essere consentito di memorizzare alcune variabili locali solo nei registri, e la maggior parte dei compilatori di ottimizzazione lo sta facendo (prova gcc -fverbose-asm -S -O2a compilare un codice C)
Basile Starynkevitch

@BasileStarynkevitch Non sono sicuro del punto che stai cercando di sottolineare con l'osservazione che le variabili possono essere memorizzate esclusivamente nei registri. Potresti per favore elaborare?
Lawrence,

5

Avere un puntatore consuma sicuramente un certo sovraccarico, ma puoi anche vedere il lato positivo: il puntatore è come un indice. In C è possibile utilizzare strutture dati complesse come stringhe e strutture solo a causa di puntatori.

Supponiamo infatti che tu voglia passare una variabile per riferimento, quindi è facile mantenere un puntatore anziché replicare l'intera struttura e sincronizzare le modifiche tra di loro (anche per copiarle avrai bisogno di un puntatore). Come gestiresti allocazioni di memoria non contigue e de-allocazioni senza puntatore?

Anche le tue variabili normali hanno una voce nella tabella dei simboli che memorizza l'indirizzo verso cui punta la tua variabile. Quindi, non penso che crei molto sovraccarico in termini di memoria (solo 4 o 8 byte). Anche i linguaggi come Java usano i puntatori internamente (riferimento), semplicemente non ti permettono di manipolarli in quanto renderanno JVM meno sicuro.

Dovresti usare i puntatori solo quando non hai altra scelta come tipi di dati mancanti, strutture (in c) poiché l'uso dei puntatori può portare a errori se non gestito correttamente e relativamente più difficile da eseguire il debug.


3

Quindi questo non è un sovraccarico di memoria?

Sì no forse?

Questa è una domanda imbarazzante perché immagina l'intervallo di indirizzamento della memoria sulla macchina e un software che deve tenere costantemente traccia di dove sono le cose in memoria in un modo che non può essere legato allo stack.

Ad esempio, immagina un lettore musicale in cui il file musicale viene caricato su un pulsante premuto dall'utente e scaricato dalla memoria volatile quando l'utente tenta di caricare un altro file musicale.

Come tenere traccia di dove sono archiviati i dati audio? Ci serve un indirizzo di memoria. Il programma non deve solo tenere traccia del blocco di dati audio in memoria, ma anche dove si trova in memoria. Pertanto, dobbiamo mantenere un indirizzo di memoria (ovvero un puntatore). E la dimensione della memoria richiesta per l'indirizzo di memoria corrisponderà all'intervallo di indirizzamento della macchina (es: puntatore a 64 bit per un intervallo di indirizzamento a 64 bit).

Quindi è un po '"sì", richiede memoria per tenere traccia di un indirizzo di memoria, ma non è che possiamo evitarlo per una memoria allocata dinamicamente di questo tipo.

Come viene compensato?

Parlando solo della dimensione di un puntatore stesso, è possibile evitare il costo in alcuni casi utilizzando lo stack, ad esempio In questo caso, i compilatori possono generare istruzioni che codificano effettivamente l'indirizzo di memoria relativo, evitando il costo di un puntatore. Tuttavia, questo ti rende vulnerabile agli overflow dello stack se lo fai per allocazioni di grandi dimensioni di dimensioni variabili e tende anche a essere poco pratico (se non addirittura impossibile) per una serie complessa di rami guidata dall'input dell'utente (come nell'esempio audio sopra).

Un altro modo è utilizzare strutture dati più contigue. Ad esempio, è possibile utilizzare una sequenza basata su array anziché un elenco doppiamente collegato che richiede due puntatori per nodo. Possiamo anche usare un ibrido di questi due come un elenco non srotolato che memorizza solo i puntatori tra ogni gruppo contiguo di N elementi.

I puntatori vengono utilizzati in applicazioni con memoria insufficiente nel tempo?

Sì, molto comunemente, poiché molte applicazioni critiche per le prestazioni sono scritte in C o C ++ che sono dominate dall'uso del puntatore (potrebbero essere dietro un puntatore intelligente o un contenitore come std::vectoro std::string, ma la meccanica sottostante si riduce a un puntatore che viene utilizzato per tenere traccia dell'indirizzo su un blocco di memoria dinamica).

Ora torniamo a questa domanda:

Come viene compensato? (Seconda parte)

I puntatori sono in genere sporchi a meno che non li si memorizzi come un milione di essi (che è ancora miseramente * 8 megabyte su una macchina a 64 bit).

* Notare come Ben ha sottolineato che un "misero" 8 mega è ancora la dimensione della cache L3. Qui ho usato "misericordioso" di più nel senso dell'uso totale della DRAM e le dimensioni relative tipiche ai blocchi di memoria indicheranno un uso sano dei puntatori.

Dove i puntatori diventano costosi non sono i puntatori stessi ma:

  1. Allocazione dinamica della memoria. L'allocazione dinamica della memoria tende ad essere costosa poiché deve passare attraverso una struttura di dati sottostante (es: buddy o slab allocator). Anche se questi sono spesso ottimizzati fino alla morte, sono di uso generale e progettati per gestire blocchi di dimensioni variabili che richiedono che facciano almeno un po 'di lavoro simile a una "ricerca" (anche se leggera e forse anche a tempo costante) per trova un set gratuito di pagine contigue in memoria.

  2. Accesso alla memoria. Questo tende ad essere il più grande overhead di cui preoccuparsi. Ogni volta che accediamo alla memoria allocata in modo dinamico per la prima volta, c'è un errore di pagina obbligatorio, così come i mancati errori della cache che spostano la memoria nella gerarchia della memoria e in un registro.

Accesso alla memoria

L'accesso alla memoria è uno degli aspetti più critici delle prestazioni oltre agli algoritmi. Molti settori critici per le prestazioni come i motori di gioco AAA concentrano gran parte della loro energia verso ottimizzazioni orientate ai dati che si riducono a schemi e layout di accesso alla memoria più efficienti.

Una delle maggiori difficoltà prestazionali dei linguaggi di livello superiore che vogliono allocare separatamente ogni tipo definito dall'utente attraverso un garbage collector, ad esempio, è che possono frammentare un po 'la memoria. Ciò può essere particolarmente vero se non tutti gli oggetti vengono allocati contemporaneamente.

In questi casi, se si memorizza un elenco di un milione di istanze di un tipo di oggetto definito dall'utente, l'accesso a tali istanze in sequenza in un ciclo potrebbe essere piuttosto lento poiché è analogo a un elenco di un milione di puntatori che puntano a regioni di memoria disparate. In quei casi, l'architettura vuole recuperare la memoria dai livelli superiori, più lenti e più grandi della gerarchia in blocchi grandi e allineati con la speranza che i dati circostanti in tali blocchi possano essere raggiunti prima dello sfratto. Quando ogni oggetto in un tale elenco viene allocato separatamente, spesso finiamo per pagarlo con cache miss quando ogni iterazione successiva potrebbe essere caricata da un'area completamente diversa in memoria senza l'accesso ad oggetti adiacenti prima dello sfratto.

Molti compilatori per tali lingue stanno facendo davvero un ottimo lavoro in questi giorni nella selezione delle istruzioni e nell'allocazione dei registri, ma la mancanza di un controllo più diretto sulla gestione della memoria qui può essere killer (sebbene spesso meno soggetto a errori) e rendere ancora linguaggi come C e C ++ abbastanza popolari.

Ottimizzazione indiretta dell'accesso al puntatore

Negli scenari più critici per le prestazioni, le applicazioni utilizzano spesso pool di memoria che raggruppano la memoria da blocchi contigui per migliorare la località di riferimento. In tali casi, anche una struttura collegata come un albero o un elenco collegato può essere resa cache-friendly a condizione che il layout di memoria dei suoi nodi sia di natura contigua. Ciò sta effettivamente rendendo più economico il dereferenziamento del puntatore, anche se indirettamente migliorando la località di riferimento coinvolta nel dereferenziarlo.

Inseguendo puntatori intorno

Supponiamo di avere un elenco collegato singolarmente come:

Foo->Bar->Baz->null

Il problema è che se assegniamo tutti questi nodi separatamente a un allocatore per scopi generici (e forse non tutti in una volta), la memoria effettiva potrebbe essere dispersa in questo modo (diagramma semplificato):

inserisci qui la descrizione dell'immagine

Quando iniziamo a inseguire i puntatori e accediamo al Foonodo, iniziamo con un miss obbligatorio (e forse un errore di pagina) spostando un pezzo dalla sua regione di memoria da regioni più lente di memoria a regioni più veloci di memoria, in questo modo:

inserisci qui la descrizione dell'immagine

Questo ci fa memorizzare nella cache (possibilmente anche una pagina) un'area di memoria solo per accedere a una parte di essa ed eliminare il resto mentre inseguiamo i puntatori in questo elenco. Prendendo il controllo sull'allocatore di memoria, tuttavia, possiamo allocare un tale elenco in modo contiguo in questo modo:

inserisci qui la descrizione dell'immagine

... e quindi migliorare significativamente la velocità con cui possiamo dereferenziare questi puntatori ed elaborare le loro punte. Quindi, sebbene molto indiretto, possiamo velocizzare l'accesso del puntatore in questo modo. Ovviamente se li avessimo archiviati in modo contiguo in un array, non avremmo questo problema in primo luogo, ma l'allocatore di memoria qui che ci dà il controllo esplicito sul layout della memoria può salvare il giorno in cui è richiesta una struttura collegata.

* Nota: questo è un diagramma molto semplificato e una discussione sulla gerarchia della memoria e sulla località di riferimento, ma si spera sia appropriato per il livello della domanda.


1
"measly 8 megabytes" è la dimensione tipica dell'intera cache L3 su una CPU moderna
Ben Voigt,

1
@BenVoigt Proverò a chiarire un po 'questo. Naturalmente sarebbe orribile se ogni puntatore indicasse un pezzo di memoria a 32 bit, ad esempio

@BenVoigt Aggiunta una nota a piè di pagina per cercare di chiarire quella parte!

Questo chiarimento sembra buono.
Ben Voigt,

1

Quindi questo non è un sovraccarico di memoria?

È davvero un sovraccarico di memoria, ma molto piccolo (fino al punto di insignificanza).

Come viene compensato?

Non è compensato. È necessario rendersi conto che l'accesso ai dati tramite un puntatore (dereferenziazione di un puntatore) è estremamente veloce (se ricordo bene, utilizza solo un'istruzione di assemblaggio per dereferenza). È abbastanza veloce che in molti casi sarà l'alternativa più veloce che hai.

I puntatori vengono utilizzati in applicazioni con memoria insufficiente nel tempo?

Sì.


0

È necessario solo l'utilizzo di memoria aggiuntiva (in genere 4-8 byte per puntatore) mentre è necessario quel puntatore. Ci sono molte tecniche che rendono questo più conveniente.

La tecnica più fondamentale che rende potenti i puntatori è che non è necessario mantenere ogni puntatore. A volte puoi usare un algoritmo per costruire un puntatore da un puntatore a qualcos'altro. L'esempio più banale di questo è l'aritmetica dell'array. Se si assegna una matrice di 50 numeri interi, non è necessario mantenere 50 puntatori, uno per ogni numero intero. In genere si tiene traccia di un puntatore (il primo) e si utilizza l'aritmetica del puntatore per generare gli altri al volo. A volte è possibile mantenere temporaneamente uno di quei puntatori a un elemento specifico dell'array, ma solo quando è necessario. Una volta terminato, puoi scartarlo, a condizione che tu abbia conservato abbastanza informazioni per rigenerarlo in un secondo momento, se necessario. Questo può sembrare banale, ma è esattamente il tipo di strumenti di conservazione che

In situazioni di memoria estremamente ridotte, questo può essere utilizzato per ridurre al minimo i costi. Se stai lavorando in uno spazio di memoria molto ristretto, di solito hai un buon senso di quanti oggetti devi manipolare. Invece di allocare un gruppo di numeri interi uno alla volta e mantenere puntatori completi su di essi, puoi trarre vantaggio dalla conoscenza del tuo sviluppatore che non avrai mai più di 256 numeri interi in questo particolare algoritmo. In tal caso, è possibile mantenere un puntatore al primo intero e tenere traccia di un indice utilizzando un carattere (1 byte) anziché utilizzare un puntatore completo (4/8 byte). È inoltre possibile utilizzare trucchi algoritmici per generare al volo alcuni di questi indici.

Questo tipo di coscienza della memoria era molto popolare in passato. Ad esempio, i giochi NES si baserebbero ampiamente sulla loro capacità di stipare dati e generare algoritmi in modo algoritmico piuttosto che doverli archiviare tutti all'ingrosso.

Le situazioni di memoria estreme possono anche portare a fare cose come l'allocazione di tutti gli spazi su cui operi in fase di compilazione. Quindi il puntatore che devi archiviare in quella memoria viene archiviato nel programma anziché nei dati. In molte situazioni con problemi di memoria, hai memoria di programma e dati separata (spesso ROM vs RAM), quindi potresti essere in grado di regolare il modo in cui usi l'algoritmo per inserire i puntatori nella memoria di quel programma.

Fondamentalmente, non puoi liberarti di tutto il sovraccarico. Tuttavia, puoi controllarlo. Utilizzando tecniche algoritmiche, è possibile ridurre al minimo il numero di puntatori che è possibile memorizzare. Se ti capita di usare i puntatori alla memoria dinamica, non scenderai mai al di sotto del costo di mantenere 1 puntatore in quel punto di memoria dinamica, perché questa è la minima quantità minima di informazioni necessarie per accedere a qualsiasi cosa in quel blocco di memoria. Tuttavia, negli scenari di vincolo di memoria ultra-stretto, questo tende ad essere il caso speciale (la memoria dinamica e i vincoli di memoria ultra-stretto tendono a non apparire nelle stesse situazioni).


0

In molte situazioni i puntatori salvano effettivamente la memoria. Un'alternativa comune all'utilizzo dei puntatori è quella di creare una copia di una struttura di dati. Una copia completa di una struttura di dati sarà più grande di un puntatore.

Un esempio di applicazione time-critical è uno stack di rete. Un buon stack di rete sarà progettato per essere "zero copia" - e per fare questo richiede un uso intelligente dei puntatori.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.