Насколько полезен «истинный» размер переменных в C?

9

Одна вещь, которая всегда интуитивно поражала меня как положительная особенность C (ну, собственно, его реализаций, таких как gcc, clang, ...), это то, что он не хранит скрытую информацию рядом с вашими переменными во время выполнения. Под этим я подразумеваю, что если вы, например, хотите переменную «x» типа «uint16_t», вы можете быть уверены, что «x» будет занимать только 2 байта пространства (и не будет содержать никакой скрытой информации, такой как ее тип и т. Д.). .). Точно так же, если вы хотите получить массив из 100 целых чисел, вы можете быть уверены, что он равен 100 целым числам.

Однако чем больше я пытаюсь придумать конкретные варианты использования этой функции, тем больше мне интересно, имеет ли она вообще какие-либо практические преимущества. Единственное, что я мог придумать, так это то, что ему явно требуется меньше оперативной памяти. Для ограниченных сред, таких как микросхемы AVR и т. Д., Это, безусловно, огромный плюс, но для повседневных случаев использования настольных ПК / серверов это, скорее всего, не имеет значения. Еще одна возможность, о которой я думаю, это то, что она может быть полезной / важной для доступа к аппаратному обеспечению или может отображать области памяти (например, для выхода VGA и т. П.) ...?

Мой вопрос: есть ли конкретные домены, которые либо не могут, либо могут быть очень громоздкими без этой функции?

PS Пожалуйста, скажите мне, если у вас есть лучшее название для этого! ;)

Томас Олтманн
источник
@gnat Я думаю, я понимаю, в чем твоя проблема. Это потому, что может быть несколько ответов, верно? Что ж, я понимаю, что этот вопрос может не подходить к тому, как работает stackexchange, но я, честно говоря, не знаю, куда обратиться в противном случае ...
Томас Олтманн
1
@lxrec RTTI хранится в виртуальной таблице, а объекты хранят только указатель на виртуальную таблицу. Кроме того, типы имеют RTTI, только если они уже имеют vtable, потому что у них есть virtualфункция-член. Таким образом, RTTI никогда не увеличивает размер каких-либо объектов, он только увеличивает двоичный файл на постоянную.
3
@ThomasOltmann Каждый объект , имеющий виртуальные методы, нуждается в указателе vtable. Вы не можете иметь функциональные виртуальные методы без этого. Более того, вы явно хотите иметь виртуальные методы (и, следовательно, vtable).
1
@ThomasOltmann Вы, кажется, очень смущены. Это не указатель на объект, который несет указатель vtable, это сам объект. Т.е. T *всегда имеет одинаковый размер и Tможет содержать скрытое поле, указывающее на vtable. И никакой компилятор C ++ никогда не вставлял vtables в объекты, которые им не нужны.

Ответы:

5

Есть несколько преимуществ, очевидное - во время компиляции, чтобы гарантировать, что такие вещи, как параметры функции, соответствуют передаваемым значениям.

Но я думаю, что вы спрашиваете о том, что происходит во время выполнения.

Имейте в виду, что компилятор создаст среду выполнения, которая встраивает знания о типах данных в выполняемые им операции. Каждый кусок данных в памяти не может быть самоописуемым, но сам по себе код знает, что это за данные (если вы правильно выполнили свою работу).

Во время выполнения все немного по-другому, чем вы думаете.

Например, не думайте, что при объявлении uint16_t используются только два байта. В зависимости от процессора и выравнивания слов он может занимать 16, 32 или 64 бита в стеке. Вы можете обнаружить, что ваш массив шорт потребляет гораздо больше памяти, чем вы ожидали.

Это может быть проблематично в определенных ситуациях, когда вам нужно ссылаться на данные с определенными смещениями. Это происходит при обмене данными между двумя системами с разной архитектурой процессора, либо по беспроводной связи, либо через файлы.

C позволяет вам определять структуры с уровнем детализации на уровне битов:

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

Эта структура имеет длину в три байта, с коротким определением, начинающимся с нечетного смещения. Он также должен быть упакован, чтобы быть точно таким, как вы его определили. В противном случае компилятор выравнивает слова по словам.

Компилятор сгенерирует код за кулисами, чтобы извлечь эти данные и скопировать в регистр, чтобы вы могли делать с ним полезные вещи.

Теперь вы можете видеть, что каждый раз, когда моя программа обращается к члену структуры myMessage, она будет знать, как именно ее извлечь и работать с ней.

Это может стать проблематичным и трудным для управления при обмене данными между различными системами с разными версиями программного обеспечения. Вы должны тщательно спроектировать систему и код, чтобы обе стороны имели одно и то же определение типов данных. Это может быть довольно сложным в некоторых условиях. Здесь вам нужен лучший протокол, который содержит данные с самоописанием, такие как буфер протокола Google .

Наконец, вы должны спросить, насколько это важно в среде настольных компьютеров и серверов. Это действительно зависит от того, сколько памяти вы планируете использовать. Если вы занимаетесь чем-то вроде обработки изображений, вы можете использовать большой объем памяти, что может повлиять на производительность вашего приложения. Это определенно всегда проблема во встроенной среде, где память ограничена и нет виртуальной памяти.

Тереус Скотт
источник
2
«Вы можете обнаружить, что ваш массив шорт потребляет гораздо больше памяти, чем вы ожидали». Это неправильно в C: массивы гарантированно содержат свои элементы без пробелов. Да, массив должен быть правильно выровнен, как и одиночный short. Но это единовременное требование для начала массива, остальные автоматически выровнены правильно благодаря последовательности.
cmaster - восстановить монику
Кроме того, синтаксис для заполнения является неправильным, uint8_t padding: 6;как и первые два бита. Или, точнее, просто комментарий //6 bits of padding inserted by the compiler. Структура, как вы ее написали, имеет размер как минимум девять байтов, а не три.
cmaster - восстановить монику
9

Вы столкнулись с одной из причин, по которой это полезно: отображение внешних структур данных. К ним относятся отображаемые в памяти видеобуферы, аппаратные регистры и т. Д. Они также включают в себя данные, передаваемые без изменений за пределы программы, такие как SSL-сертификаты, IP-пакеты, изображения JPEG, и почти любая другая структура данных, которая постоянно существует вне программы.

Росс Паттерсон
источник
5

C является языком низкого уровня, почти переносимым ассемблером, поэтому его структуры данных и языковые конструкции близки к металлу (структуры данных не имеют скрытых затрат - кроме дополнения, выравнивания и ограничений размера, наложенных аппаратным обеспечением и ABI ). Так что C действительно не имеет встроенной динамической типизации. Но если вам это нужно, вы можете принять соглашение, согласно которому все ваши значения являются агрегатами, начиная с некоторой информации о типе (например, с некоторого enum...); Использование union-s и (для массива-подобных вещей) гибкого элемента массива в structсодержащий также размер массива.

(при программировании на C вы несете ответственность за определение, документирование и выполнение полезных соглашений - в частности, предварительных и постусловий и инвариантов; также динамическое выделение памяти в C требует freeобъяснения соглашений о том, кто должен иметь mallocзону с кучей памяти)

Таким образом, для представления значений, которые представляют собой целые числа в виде блоков, или строки, или какой-либо символ , подобный Схеме , или векторы значений, вы концептуально будете использовать теговое объединение (реализованное как объединение указателей) - всегда начиная с типа type - например:

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

Чтобы получить динамический тип некоторого значения

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

Вот «динамическое приведение» к векторам:

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

и «безопасный метод доступа» внутри векторов:

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

Обычно вы определяете большинство коротких функций, описанных выше, как static inlineв некотором заголовочном файле.

Кстати, если вы можете использовать сборщик мусора от Boehm, вы сможете довольно легко кодировать его в каком-то высокоуровневом (но небезопасном) стиле, и таким образом выполняется несколько интерпретаторов Scheme. Вариативный векторный конструктор может быть

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

и если у вас есть три переменные

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

вы можете построить вектор из них, используя make_vector(3,v1,v2,v3)

Если вы не хотите использовать сборщик мусора Boehm (или проектировать свой собственный), вы должны быть очень осторожны с определением деструкторов и документированием того, кто, как и когда память должна быть free-d; посмотрите этот пример. Таким образом, вы могли бы использовать malloc(но затем проверить его на отказ) вместо GC_MALLOCвышеупомянутого, но вам нужно тщательно определить и использовать некоторую функцию деструктораvoid destroy_value(value_t)

Сила C в том, чтобы быть достаточно низкоуровневым, чтобы сделать возможным выполнение кода, подобного описанному выше, и определить ваши собственные соглашения (особенно для вашего программного обеспечения).

Василий Старынкевич
источник
Я думаю, что вы неправильно поняли мой вопрос. Я не хочу динамической типизации на C. Мне было любопытно, имеет ли это конкретное свойство C какое-либо практическое применение.
Томас Олтманн
Но на какое именно свойство C вы ссылаетесь? Структуры данных C близки к металлу, поэтому не имеют скрытых затрат (кроме ограничений по выравниванию и размеру)
Василий Старынкевич
Именно это: /
Томас Олтманн
C был изобретен как низкоуровневый язык, но когда оптимизация включена на компиляторах, таких как gcc, обрабатывает язык, который использует низкоуровневый синтаксис, но не обеспечивает надежного низкоуровневого доступа к предоставленным платформой поведенческим гарантиям. Чтобы использовать malloc и memcpy, нужно sizeof, но использование для более точных вычислений адресов может не поддерживаться в «современном» C.
Суперкат