Quanto è utile il dimensionamento "vero" delle variabili di C?


9

Una cosa che mi ha sempre intuitivamente colpito come una caratteristica positiva di C (beh, in realtà delle sue implementazioni come gcc, clang, ...) è il fatto che non memorizza alcuna informazione nascosta accanto alle tue variabili in fase di runtime. Con questo intendo che se per esempio volessi una variabile "x" del tipo "uint16_t", potresti essere sicuro che "x" occuperà solo 2 byte di spazio (e non trasporterà alcuna informazione nascosta come il suo tipo ecc. .). Allo stesso modo, se volevi un array di 100 numeri interi, potresti essere sicuro che sia grande quanto 100 numeri interi.

Tuttavia, più sto cercando di trovare casi d'uso concreti per questa funzione, più mi chiedo se abbia effettivamente dei vantaggi pratici. L'unica cosa che mi è venuta in mente finora è che ovviamente ha bisogno di meno RAM. Per ambienti limitati, come i chip AVR ecc., Questo è sicuramente un grande vantaggio, ma per i casi d'uso quotidiani desktop / server, sembra essere piuttosto irrilevante. Un'altra possibilità a cui sto pensando è che potrebbe essere utile / cruciale per accedere all'hardware o magari mappare le aree di memoria (ad esempio per output VGA e simili) ...?

La mia domanda: ci sono domini concreti che non possono o possono essere implementati solo in modo molto ingombrante senza questa funzione?

PS Per favore dimmi se hai un nome migliore per questo! ;)



@gnat Penso di aver capito qual è il tuo problema. È perché potrebbero esserci più risposte, giusto? Bene, capisco che questa domanda potrebbe non adattarsi al modo in cui stackexchange funziona, ma onestamente non so dove chiedere altrimenti ...
Thomas Oltmann

1
@lxrec RTTI è memorizzato nella vtable e gli oggetti memorizzano solo un puntatore alla vtable. Inoltre, i tipi hanno RTTI solo se hanno già una vtable perché hanno una virtualfunzione membro. Quindi RTTI non aumenta mai le dimensioni di alcun oggetto, ma ingrandisce il binario solo di una costante.

3
@ThomasOltmann Ogni oggetto che ha metodi virtuali ha bisogno di un puntatore vtable. Non puoi avere i metodi virtuali di funzionalità senza quello. Inoltre, si sceglie esplicitamente di avere metodi virtuali (e quindi una vtable).

1
@ThomasOltmann Sembri molto confuso. Non è un puntatore a un oggetto che porta un puntatore vtable, è l'oggetto stesso. Cioè, ha T *sempre le stesse dimensioni e Tpuò contenere un campo nascosto che punta alla vtable. E nessun compilatore C ++ ha mai inserito vtables in oggetti che non ne hanno bisogno.

Risposte:


5

Ci sono molti vantaggi, quello ovvio è in fase di compilazione per garantire che cose come i parametri di funzione corrispondano ai valori passati.

Ma penso che tu stia chiedendo cosa sta succedendo in fase di esecuzione.

Tieni presente che il compilatore creerà un runtime che incorpora la conoscenza dei tipi di dati nelle operazioni che esegue. Ogni blocco di dati in memoria potrebbe non essere auto-descrittivo, ma il codice conosce intrinsecamente quali sono i dati (se hai svolto correttamente il tuo lavoro).

In fase di esecuzione le cose sono leggermente diverse da come pensi.

Ad esempio, non dare per scontato che vengano utilizzati solo due byte quando dichiari uint16_t. A seconda del processore e dell'allineamento delle parole, può occupare 16, 32 o 64 bit nello stack. Potresti scoprire che la tua gamma di cortometraggi consuma molta più memoria del previsto.

Ciò può essere problematico in determinate situazioni in cui è necessario fare riferimento a dati in offset specifici. Ciò accade quando si comunica tra due sistemi con architetture di processori diverse, tramite un collegamento wireless o tramite file.

C consente di specificare le strutture con granularità a livello di bit:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

Questa struttura è lunga tre byte, con un corto definito per iniziare con un offset dispari. Dovrà anche essere imballato per essere esattamente come lo hai definito. In caso contrario, il compilatore allinea le parole dei membri.

Il compilatore genererà codice dietro le quinte per estrarre questi dati e copiarli in un registro in modo da poter fare cose utili con esso.

Ora puoi vedere che ogni volta che il mio programma accede a un membro della struttura myMessage, saprà esattamente come estrarlo e operarci.

Questo può diventare problematico e difficile da gestire quando si comunica tra sistemi diversi con diverse versioni di software. È necessario progettare attentamente il sistema e il codice per garantire che entrambe le parti abbiano esattamente la stessa definizione dei tipi di dati. Questo può essere piuttosto impegnativo in alcuni ambienti. Qui è necessario un protocollo migliore che contenga dati autodescrittivi come i buffer di protocollo di Google .

Infine, fai un buon punto per chiederti quanto sia importante questo in ambiente desktop / server. Dipende davvero da quanta memoria hai intenzione di usare. Se stai facendo qualcosa come l'elaborazione delle immagini, potresti finire con l'uso di una grande quantità di memoria che può influire sulle prestazioni della tua applicazione. Questo è sicuramente sempre un problema nell'ambiente embedded in cui la memoria è limitata e non c'è memoria virtuale.


2
"Potresti scoprire che la tua gamma di cortometraggi consuma molta più memoria di quanto ti aspettassi." Questo è sbagliato in C: gli array sono garantiti per contenere i loro elementi in modo privo di gap. Sì, l'array deve essere allineato correttamente, così come un singolo short. Ma questo è un requisito una tantum per l'avvio dell'array, il resto viene automaticamente allineato correttamente in quanto consecutivo.
cmaster - ripristina monica il

Inoltre, la sintassi per il padding è errata, dovrebbe essere uint8_t padding: 6;, proprio come i primi due bit. O, più chiaramente, solo il commento //6 bits of padding inserted by the compiler. La struttura, come l'hai scritta, ha una dimensione di almeno nove byte, non tre.
cmaster - ripristina monica il

9

Colpisci una delle uniche ragioni per cui è utile: mappare strutture di dati esterne. Questi includono buffer video mappati in memoria, registri hardware, ecc. Includono anche dati trasmessi intatti al di fuori del programma, come certificati SSL, pacchetti IP, immagini JPEG e praticamente qualsiasi altra struttura di dati che ha una vita persistente al di fuori del programma.


5

C è un linguaggio di basso livello, quasi un assemblatore portatile, quindi le sue strutture di dati e costrutti di linguaggio sono vicini al metallo (le strutture di dati non hanno costi nascosti - eccetto i limiti di imbottitura, allineamento e dimensione imposti dall'hardware e dall'ABI ). Quindi C in effetti non ha una digitazione dinamica nativa. Ma se ne hai bisogno, potresti adottare una convenzione secondo cui tutti i tuoi valori sono aggregati a partire da alcune informazioni sul tipo (ad es. Alcuni enum...); use union-s e (per cose simili ad array) membro flessibile dell'array nel structcontenere anche le dimensioni dell'array.

(durante la programmazione in C, è responsabilità dell'utente definire, documentare e seguire utili convenzioni - in particolare pre e post-condizioni e invarianti; anche l'allocazione dinamica della memoria C richiede esplicazioni su chi dovrebbe avere freeuna malloczona di memoria ammassata)

Quindi, per rappresentare valori che sono numeri interi inscatolati, o stringhe, o qualche tipo di simbolo simile allo schema , o vettori di valori, userete concettualmente un'unione taggata (implementata come unione di puntatori) che inizia sempre dal tipo di tipo -, per esempio:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

Per ottenere il tipo dinamico di un valore

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

Ecco un "cast dinamico" per i vettori:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

e un "accessore sicuro" all'interno dei vettori:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

In genere definirai la maggior parte delle funzioni brevi sopra come static inlinein alcuni file di intestazione.

A proposito, se puoi usare il garbage collector di Boehm, allora sei in grado di programmare abbastanza facilmente in uno stile di livello superiore (ma non sicuro), e diversi interpreti Scheme sono fatti in quel modo. Potrebbe essere un costruttore di vettore variadico

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

e se hai tre variabili

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

potresti costruire un vettore da loro usando make_vector(3,v1,v2,v3)

Se non vuoi usare il garbage collector di Boehm (o progettarne uno tuo), dovresti stare molto attento a definire i distruttori e documentare chi, come e quando la memoria dovrebbe essere free-d; vedi questo esempio. Quindi potresti usare malloc(ma poi provare contro il suo fallimento) invece di GC_MALLOCsopra ma devi definire con attenzione e usare alcune funzioni di distruttorevoid destroy_value(value_t)

Il punto di forza di C è di essere abbastanza basso livello da rendere possibile il codice come sopra e definire le proprie convenzioni (particolari per il proprio software).


Penso che tu abbia frainteso la mia domanda. Non voglio scrivere in modo dinamico in C. Ero curioso di sapere se questa specifica proprietà di C è di qualche utilità pratica.
Thomas Oltmann,

Ma a quale esatta proprietà di C ti riferisci? Le strutture di dati C sono vicine al metallo, quindi non hanno costi nascosti (tranne i vincoli di allineamento e dimensione)
Basile Starynkevitch

Esatto che: /
Thomas Oltmann il

C è stato inventato come linguaggio di basso livello, ma quando si attivano le ottimizzazioni, i compilatori come gcc elaborano un linguaggio che utilizza la sintassi di basso livello ma che non fornisce un accesso di basso livello a garanzie comportamentali fornite dalla piattaforma. Uno ha bisogno di dimensioni per usare malloc e memcpy, ma l'uso per i calcoli degli indirizzi più elaborati potrebbe non essere supportato nella "moderna" C.
supercat
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.