Может ли компилятор C изменить порядок переменных стека?

В прошлом я работал над проектами для встраиваемых систем, где мы изменили порядок объявления переменных стека, чтобы уменьшить размер получаемого исполняемого файла. Например, если бы у нас было:

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, но я думаю, что этот документ считается доказательством того, что вы все правы. Еще раз спасибо.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
16
0
5 020
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

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

Ответ принят как подходящий

Поскольку в стандарте нет ничего, запрещающего это для компиляторов C или C++, да, компилятор может это сделать.

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

Новые компиляторы MSVC IIRC используют эту свободу в борьбе с переполнением буфера локальными переменными.

В качестве примечания, в C++ порядок уничтожения должен быть обратным порядку объявления, даже если компилятор переупорядочивает макет памяти.

(Я не могу цитировать главы и стихи, это по памяти.)

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

Это действительно хороший момент. Я не думал об этом, когда обсуждал это с моим коллегой.

ryan_s 26.10.2008 22:27

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

Ben Combee 27.10.2008 00:05

Компилятор может вообще не использовать стек для данных. Если ваша платформа настолько крошечная, что вас беспокоит размер стека 8 против 12, то вполне вероятно, что будут компиляторы с довольно специализированными подходами. (На ум приходят некоторые компиляторы PIC и 8051)

Для какого процессора вы компилируете?

Это было для проекта, над которым я работал некоторое время назад, где мы использовали старую версию компилятора Arm Developer Suite (ADS) для сборки для нескольких процессоров ARM. Я просто прошу уладить дискуссию о том, как с этим справляются другие компиляторы.

ryan_s 26.10.2008 22:41

Приличный компилятор поместит локальные переменные в регистры, если сможет. Переменные следует помещать в стек только в том случае, если существует чрезмерное давление в регистре (недостаточно места) или если используется адрес переменной, что означает, что она должна находиться в памяти.

Насколько мне известно, нет ничего, что говорило бы о необходимости размещения переменных в каком-либо конкретном месте или выравнивания в стеке для C / C++; компилятор поместит их туда, где лучше всего для производительности и / или что удобно для разработчиков компилятора.

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

Daniel Spiewak 26.10.2008 22:53

... или если переменная не связана с вызовом функции. Подобно x = f (x) - старое значение может быть в регистре, а новое значение снова попадает в регистр.

gnasher729 04.04.2014 21:02

Стек может даже не существовать (на самом деле, в стандарте 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...
}

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

Другие вопросы по теме