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

9

Una cosa che mi ha sempre colpito intuitivamente 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 esecuzione . Con questo voglio dire che se per esempio volevi una variabile "x" del tipo "uint16_t", potresti essere sicuro che "x" occuperà solo 2 byte di spazio (e non porterà alcuna informazione nascosta come il suo tipo ecc. .). Allo stesso modo, se si desidera un array di 100 numeri interi, si può essere certi che sia grande quanto 100 interi.

Tuttavia, più sto cercando di trovare casi d'uso concreti per questa funzione, più mi chiedo se in realtà abbia alcun vantaggi pratici. L'unica cosa che ho potuto inventare finora è che ovviamente ha bisogno di meno RAM. Per ambienti limitati, come i chip AVR ecc., Questo è sicuramente un vantaggio enorme, ma per i casi di utilizzo desktop / server di tutti i giorni, sembra essere piuttosto irrilevante. Un'altra possibilità che sto pensando è che potrebbe essere utile / cruciale per accedere all'hardware, o magari mappare regioni di memoria (ad esempio per l'output VGA e simili) ...?

La mia domanda: esistono domini concreti che non possono o non possono essere implementati molto facilmente senza questa funzione?

P.S. Per favore dimmi se hai un nome migliore per questo! ;)

    
posta Thomas Oltmann 16.01.2016 - 21:17
fonte

3 risposte

5

Ci sono diversi vantaggi, l'ovvio è in fase di compilazione per garantire che elementi come i parametri di funzione corrispondano ai valori passati.

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

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

In fase di esecuzione le cose sono un po 'diverse da come pensi.

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

Questo può essere problematico in alcune situazioni in cui è necessario fare riferimento a dati a offset specifici. Questo accade quando si comunica tra due sistemi con architetture di processore 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. Altrimenti il compilatore allineerà le parole ai membri.

Il compilatore genererà il 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 operare su di esso.

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

. >

Infine, è utile chiedere quanto sia importante nell'ambiente desktop / server. Dipende molto dalla quantità di memoria che si prevede di utilizzare. Se stai facendo qualcosa come l'elaborazione delle immagini, potresti finire per utilizzare una grande quantità di memoria che potrebbe influire sulle prestazioni della tua applicazione. Questo è sicuramente sempre un problema nell'ambiente embedded in cui la memoria è limitata e non c'è memoria virtuale.

    
risposta data 31.01.2016 - 01:01
fonte
9

Colpisci uno degli unici motivi per cui è utile: mappare strutture di dati esterni. Questi includono buffer video mappati in memoria, registri hardware, etc. Includono anche dati trasmessi all'esterno del programma, come certificati SSL, pacchetti IP, immagini JPEG e praticamente qualsiasi altra struttura di dati che ha un vita persistente al di fuori del programma.

    
risposta data 17.01.2016 - 04:38
fonte
5

C è un linguaggio di basso livello, quasi un assemblatore portatile, quindi le sue strutture dati e i suoi costrutti del linguaggio sono vicini al metallo (le strutture dati non hanno costi nascosti - eccetto vincoli di padding, allineamento e dimensione imposti dall'hardware e ABI ). C infatti non ha una digitazione dinamica nativamente. Ma se ne hai bisogno, puoi adottare una convenzione che tutti i tuoi valori siano aggregati iniziando con alcune informazioni sul tipo (ad esempio alcuni enum ...); usa union -s e (per cose simili ad array) membro flessibile dell'array in struct contenente anche le dimensioni di l'array.

(quando si programma in C, è responsabilità dell'utente definire, documentare e seguire le convenzioni utili, in particolare pre- e post-condizioni e invarianti, anche L'allocazione di memoria dinamica C richiede convenzioni esplicative su chi dovrebbe free di heap- malloc ated memory zone)

Quindi, per rappresentare valori che sono interi in scatola, o stringhe, o qualche tipo di Schema - come simbolo , o vettori di valori, utilizzerai concettualmente un tag tagged (implementato come unione di puntatori), sempre a partire dal tipo, ad 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 certo 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 "accesso 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 inline in un file di intestazione.

BTW, se puoi usare il garbage collector di Boehm allora sei in grado di codificare abbastanza facilmente in qualche livello superiore (ma stile non sicuro) e diversi interpreti Scheme sono fatti in questo modo. Un costruttore di vettori variadico potrebbe essere

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 creare un vettore da loro usando make_vector(3,v1,v2,v3)

Se non vuoi usare il garbage collector di Boehm (o creane uno tuo) devi 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 testare il suo fallimento) invece di GC_MALLOC sopra, ma devi definire con cura e usare una certa funzione distruttore void destroy_value(value_t)

Il punto di forza di C deve essere di livello abbastanza basso da rendere possibile il codice come sopra possibile e definire le tue convenzioni (in particolare per il tuo software).

    
risposta data 17.01.2016 - 09:33
fonte

Leggi altre domande sui tag