¿Qué tan útil es el tamaño "verdadero" de las variables de C?

9

Una cosa que siempre me pareció intuitivamente una característica positiva de C (bueno, en realidad de sus implementaciones como gcc, clang, ...) es el hecho de que no almacena ninguna información oculta junto a sus propias variables en tiempo de ejecución. Con esto quiero decir que si, por ejemplo, desea una variable "x" del tipo "uint16_t", puede estar seguro de que "x" solo ocupará 2 bytes de espacio (y no llevará ninguna información oculta como su tipo, etc. .). Del mismo modo, si desea una matriz de 100 enteros, puede estar seguro de que es tan grande como 100 enteros.

Sin embargo, cuanto más intento encontrar casos de uso concretos para esta característica, más me pregunto si realmente tiene alguna ventaja práctica. Lo único que se me ocurrió hasta ahora es que obviamente necesita menos RAM. Para entornos limitados, como chips AVR, etc., esta es definitivamente una gran ventaja, pero para los casos de uso cotidiano de escritorio / servidor, parece ser bastante irrelevante. Otra posibilidad en la que estoy pensando es que podría ser útil / crucial para acceder al hardware, o tal vez mapear regiones de memoria (por ejemplo, para salida VGA y similares) ...?

Mi pregunta: ¿Hay dominios concretos que no pueden o pueden implementarse muy engorrosamente sin esta función?

PD ¡Por favor dime si tienes un nombre mejor para él! ;)

Thomas Oltmann
fuente
@gnat Creo que entiendo cuál es tu problema. Es porque podría haber múltiples respuestas, ¿verdad? Bueno, entiendo que esta pregunta podría no adaptarse a la forma en que funciona stackexchange, pero honestamente no sé dónde preguntar de otra manera ...
Thomas Oltmann
1
@lxrec RTTI se almacena en vtable y los objetos solo almacenan un puntero a vtable. Además, los tipos solo tienen RTTI si ya tienen una vtable porque tienen una virtualfunción miembro. Por lo tanto, RTTI nunca aumenta el tamaño de ningún objeto, solo hace que el binario sea más grande por una constante.
3
@ThomasOltmann Cada objeto que tiene métodos virtuales necesita un puntero vtable. No puede tener la funcionalidad de métodos virtuales sin eso. Además, explícitamente opta por tener métodos virtuales (y, por lo tanto, una vtable).
1
@ThomasOltmann Pareces muy confundido. No es un puntero a un objeto que lleva un puntero vtable, es el objeto mismo. Es decir, T *siempre es del mismo tamaño y Tpuede contener un campo oculto que apunta a la tabla. Y ningún compilador de C ++ insertó vtables en objetos que no los necesitan.

Respuestas:

5

Hay varios beneficios, el obvio es en tiempo de compilación para garantizar que cosas como los parámetros de función coincidan con los valores que se pasan.

Pero creo que estás preguntando qué está sucediendo en tiempo de ejecución.

Tenga en cuenta que el compilador creará un tiempo de ejecución que integra el conocimiento de los tipos de datos en las operaciones que realiza. Es posible que cada fragmento de datos en la memoria no se describa por sí mismo, pero el código inherentemente sabe cuáles son esos datos (si ha realizado su trabajo correctamente).

En tiempo de ejecución las cosas son un poco diferentes de lo que piensas.

Por ejemplo, no asuma que solo se usan dos bytes cuando declara uint16_t. Dependiendo del procesador y la alineación de palabras, puede ocupar 16, 32 o 64 bits en la pila. Puede descubrir que su conjunto de cortos consume mucha más memoria de la que esperaba.

Esto puede ser problemático en ciertas situaciones en las que necesita hacer referencia a datos en desplazamientos específicos. Esto sucede cuando se comunica entre dos sistemas que tienen arquitecturas de procesador diferentes, ya sea a través de un enlace inalámbrico o mediante archivos.

C le permite especificar estructuras con granularidad de nivel de bits:

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

Esta estructura tiene una longitud de tres bytes, con un corto definido para comenzar en un desplazamiento impar. También deberá empacarse para que sea exactamente como lo definió. De lo contrario, el compilador alineará las palabras con los miembros.

El compilador generará código detrás de escena para extraer estos datos y copiarlos en un registro para que pueda hacer cosas útiles con ellos.

Ahora puede ver que cada vez que mi programa accede a un miembro de la estructura myMessage, sabrá exactamente cómo extraerlo y operarlo.

Esto puede volverse problemático y difícil de administrar cuando se comunica entre diferentes sistemas con diferentes versiones de software. Debe diseñar cuidadosamente el sistema y el código para garantizar que ambas partes tengan exactamente la misma definición de los tipos de datos. Esto puede ser bastante desafiante en algunos entornos. Aquí es donde necesita un mejor protocolo que contenga datos autodescriptivos, como los Buffers de protocolo de Google .

Por último, hace un buen punto para preguntar qué tan importante es esto en el entorno de escritorio / servidor. Realmente depende de la cantidad de memoria que planea usar. Si está haciendo algo como el procesamiento de imágenes, puede terminar usando una gran cantidad de memoria que puede afectar el rendimiento de su aplicación. Definitivamente, esto siempre es una preocupación en el entorno integrado donde la memoria está restringida y no hay memoria virtual.

Tereus Scott
fuente
2
"Puede descubrir que su conjunto de cortos consume mucha más memoria de la que esperaba". Esto está mal en C: se garantiza que las matrices contengan sus elementos de una manera libre de espacios. Sí, la matriz debe alinearse correctamente, al igual que una sola short. Pero este es un requisito único para el inicio de la matriz, el resto se alinea automáticamente correctamente en virtud de ser consecutivo.
cmaster - reinstalar a monica el
Además, la sintaxis para el relleno es incorrecta, debería ser uint8_t padding: 6;, al igual que los primeros dos bits. O, más claramente, solo el comentario //6 bits of padding inserted by the compiler. La estructura, como la ha escrito, tiene un tamaño de al menos nueve bytes, no tres.
cmaster - reinstalar a monica el
9

Llegas a una de las únicas razones por las que esto es útil: mapear estructuras de datos externas. Estos incluyen memorias intermedias de video mapeadas en memoria, registros de hardware, etc. También incluyen datos transmitidos intactos fuera del programa, como certificados SSL, paquetes IP, imágenes JPEG y casi cualquier otra estructura de datos que tenga una vida persistente fuera del programa.

Ross Patterson
fuente
5

C es un lenguaje de bajo nivel, casi un ensamblador portátil, por lo que sus estructuras de datos y construcciones de lenguaje están cerca del metal (las estructuras de datos no tienen costos ocultos, excepto el relleno, la alineación y las restricciones de tamaño impuestas por el hardware y ABI ). Por lo tanto, C no tiene una escritura dinámica nativa. Pero si lo necesita, podría adoptar una convención de que todos sus valores son agregados comenzando con alguna información de tipo (por ejemplo, algunos enum...); use union-s y (para cosas similares a una matriz) miembro de matriz flexible al structcontener también el tamaño de la matriz.

(cuando programe en C, es su responsabilidad definir, documentar y seguir convenciones útiles, especialmente condiciones previas y posteriores e invariantes; también la asignación dinámica de memoria en C requiere convenciones explícitas sobre quién debería tener freealguna malloczona de memoria activada )

Por lo tanto, para representar los valores que son números enteros en caja, o cadenas, o algún tipo de esquema -como símbolo , o vectores de valores, que va a utilizar conceptualmente una unión etiquetada (implementado como una unión de punteros) -siempre empezando por el tipo de tipo -, p.ej:

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.
};

Para obtener el tipo dinámico de algún valor

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

Aquí hay un "reparto dinámico" de vectores:

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

y un "acceso seguro" dentro de los vectores:

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;
}

Por lo general, definirá la mayoría de las funciones cortas anteriores como static inlineen algún archivo de encabezado.

Por cierto, si puede usar el recolector de basura de Boehm, puede codificar con bastante facilidad en un estilo de nivel superior (pero inseguro), y varios intérpretes de Scheme se hacen de esa manera. Un constructor de vectores variados podría ser

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};
}

y si tienes tres variables

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

podrías construir un vector a partir de ellos usando make_vector(3,v1,v2,v3)

Si no desea utilizar el recolector de basura de Boehm (o diseñar el suyo propio), debe tener mucho cuidado al definir los destructores y documentar quién, cómo y cuándo la memoria debe ser free-d; Mira este ejemplo. Por lo tanto, puede usar malloc(pero luego probar contra su falla) en lugar de lo GC_MALLOCanterior, pero necesita definir cuidadosamente y usar alguna función destructoravoid destroy_value(value_t)

La fortaleza de C es ser lo suficientemente bajo como para hacer posible un código como el anterior y definir sus propias convenciones (en particular para su software).

Basile Starynkevitch
fuente
Creo que entendiste mal mi pregunta. No quiero escribir dinámicamente en C. Tenía curiosidad por saber si esta propiedad específica de C es útil.
Thomas Oltmann
¿Pero a qué propiedad exacta de C te refieres? Las estructuras de datos C están cerca del metal, por lo que no tienen costos ocultos (excepto restricciones de alineación y tamaño)
Basile Starynkevitch
Exactamente eso: /
Thomas Oltmann
C se inventó como un lenguaje de bajo nivel, pero cuando las optimizaciones se activan en compiladores como el proceso gcc, un lenguaje que utiliza la sintaxis de bajo nivel pero no proporciona de manera confiable acceso de bajo nivel a las garantías de comportamiento proporcionadas por la plataforma. Uno necesita sizeof para usar malloc y memcpy, pero el uso para cálculos de direcciones más sofisticados puede no ser compatible con la "moderna" C.
supercat