В прошлом я работал над проектами для встраиваемых систем, где мы изменили порядок объявления переменных стека, чтобы уменьшить размер получаемого исполняемого файла. Например, если бы у нас было:
void func()
{
char c;
int i;
short s;
...
}
Мы бы изменили его порядок на:
void func()
{
int i;
short s;
char c;
...
}
Из-за проблем с выравниванием первый привел к использованию 12 байтов пространства стека, а второй привел только к 8 байтам.
Это стандартное поведение для компиляторов C или просто недостаток компилятора, который мы использовали?
Мне кажется, что компилятор должен иметь возможность переупорядочивать переменные стека в пользу меньшего размера исполняемого файла, если он этого хочет. Мне предлагали, чтобы какой-то аспект стандарта C препятствовал этому, но я так и не смог найти авторитетный источник.
В качестве дополнительного вопроса, относится ли это также к компиляторам C++?
Редактировать
Если ответ положительный, компиляторы C / C++ могут переставлять переменные стека. Не могли бы вы привести пример компилятора, который определенно это делает? Я хотел бы увидеть документацию по компилятору или что-то подобное, подтверждающее это.
Изменить снова
Спасибо всем за вашу помощь. Что касается документации, лучшее, что мне удалось найти, - это документ Оптимальное назначение слотов стека в GCC (pdf), написанный Навином Шармой и Сандживом Кумаром Гуптой, который был представлен на саммите GCC в 2003 году.
Рассматриваемый здесь проект использовал компилятор ADS для разработки ARM. В документации для этого компилятора упоминается, что объявления упорядочения, подобные показанным мною, могут улучшить производительность, а также размер стека из-за того, как архитектура ARM-Thumb вычисляет адреса в кадре локального стека. Этот компилятор не переставлял автоматически локальные переменные, чтобы воспользоваться этим. В документе, ссылка на который приведена здесь, говорится, что с 2003 года GCC также не переставлял фрейм стека для улучшения локальности ссылок для процессоров ARM-Thumb, но это подразумевает, что вы могли.
Я не могу найти ничего, что определенно говорило бы, что это когда-либо было реализовано в GCC, но я думаю, что этот документ считается доказательством того, что вы все правы. Еще раз спасибо.





это специфика компилятора, можно создать свой собственный компилятор, который делал бы обратное, если бы он этого хотел.
Поскольку в стандарте нет ничего, запрещающего это для компиляторов C или C++, да, компилятор может это сделать.
Это отличается от агрегатов (т.е. структур), где должен поддерживаться относительный порядок, но все же компилятор может вставлять байты заполнения для достижения предпочтительного выравнивания.
Новые компиляторы MSVC IIRC используют эту свободу в борьбе с переполнением буфера локальными переменными.
В качестве примечания, в C++ порядок уничтожения должен быть обратным порядку объявления, даже если компилятор переупорядочивает макет памяти.
(Я не могу цитировать главы и стихи, это по памяти.)
Компилятор может даже удалить переменную из стека и зарегистрировать ее, только если анализ показывает, что адрес переменной никогда не берется / не используется.
Он даже может назначить несколько переменных одному и тому же регистру или месту стека, если сможет доказать, что переменные никогда не существуют в одном и том же разделе кода. Это обычная практика, особенно со встроенным кодом, который приводит к короткому сроку жизни переменных.
Компилятор может вообще не использовать стек для данных. Если ваша платформа настолько крошечная, что вас беспокоит размер стека 8 против 12, то вполне вероятно, что будут компиляторы с довольно специализированными подходами. (На ум приходят некоторые компиляторы PIC и 8051)
Для какого процессора вы компилируете?
Это было для проекта, над которым я работал некоторое время назад, где мы использовали старую версию компилятора Arm Developer Suite (ADS) для сборки для нескольких процессоров ARM. Я просто прошу уладить дискуссию о том, как с этим справляются другие компиляторы.
Приличный компилятор поместит локальные переменные в регистры, если сможет. Переменные следует помещать в стек только в том случае, если существует чрезмерное давление в регистре (недостаточно места) или если используется адрес переменной, что означает, что она должна находиться в памяти.
Насколько мне известно, нет ничего, что говорило бы о необходимости размещения переменных в каком-либо конкретном месте или выравнивания в стеке для C / C++; компилятор поместит их туда, где лучше всего для производительности и / или что удобно для разработчиков компилятора.
Местные жители почти всегда должны быть сброшены в стек в какой-то момент. Единственное исключение из этого - если ваша функция никогда вызывает другую функцию, и в этом случае все временное и может обрабатываться без кадра стека.
... или если переменная не связана с вызовом функции. Подобно x = f (x) - старое значение может быть в регистре, а новое значение снова попадает в регистр.
Стек может даже не существовать (на самом деле, в стандарте C99 нет ни одного слова «стек»). Итак, да, компилятор может делать все, что хочет, при условии, что это сохраняет семантику переменных с автоматической продолжительностью хранения.
Что касается примера: я много раз сталкивался с ситуацией, когда я не мог отобразить локальную переменную в отладчике, потому что она хранилась в регистре.
AFAIK нет ничего в определении C или C++, определяющего, как компилятор должен упорядочивать локальные переменные в стеке. Я бы сказал, что полагаться на то, что может сделать компилятор в этом случае, - плохая идея, потому что следующая версия вашего компилятора может сделать это иначе. Если вы потратите время и усилия на то, чтобы ваши локальные переменные сохранили несколько байтов стека, эти несколько байтов должны быть действительно важными для функционирования вашей системы.
Компилятор для цифровых сигнальных процессоров Texas Instruments серии 62xx поддерживает и делает «оптимизация всей программы». (вы можете выключить)
Именно здесь ваш код перестраивается, а не только местные жители. Таким образом, порядок выполнения оказывается не совсем таким, как вы могли ожидать.
C и C++ не обещают на самом деле модели памяти (в смысле, скажем, JVM), поэтому все может быть совершенно по-другому и все еще законно.
Для тех, кто их не знает, семейство 62xx - это ЦСП с 8 командами на такт; на 750 МГц они делают пик на 6e + 9 инструкциях. По крайней мере, иногда. Они выполняют параллельное выполнение, но порядок команд выполняется в компиляторе, а не в процессоре, как Intel x86.
Встраиваемые платы PIC и Rabbit не складывают имеют, если вы не спросите особенно хорошо.
Нет нужды в праздных размышлениях о том, что требует или не требует стандарт C: последние проекты свободно доступны в Интернете по адресу Рабочая группа ANSI / ISO.
Это не ответ на ваш вопрос, но вот мои 2 цента по связанной проблеме ...
У меня не было проблемы оптимизации пространства стека, но у меня была проблема неправильного выравнивания двойных переменных в стеке. Функция может быть вызвана из любой другой функции, и значение указателя стека может иметь любое невыровненное значение. Итак, я придумал идею, представленную ниже. Это не исходный код, я его просто написал ...
#pragma pack(push, 16)
typedef struct _S_speedy_struct{
double fval[4];
int64 lval[4];
int32 ival[8];
}S_speedy_struct;
#pragma pack(pop)
int function(...)
{
int i, t, rv;
S_speedy_struct *ptr;
char buff[112]; // sizeof(struct) + alignment
// ugly , I know , but it works...
t = (int)buff;
t += 15; // alignment - 1
t &= -16; // alignment
ptr = (S_speedy_struct *)t;
// speedy code goes on...
}
Компилятор может не только переупорядочить структуру стека локальных переменных, он может назначать их регистрам, назначать их для жизни иногда в регистрах, а иногда и в стеке, он может назначать двух локальных переменных в один и тот же слот в памяти (если их живые диапазоны не перекрываются), и он может даже полностью исключить переменные.
Это действительно хороший момент. Я не думал об этом, когда обсуждал это с моим коллегой.