Инициализация переменной-члена, а не обращение к ней / ее использование, дополнительно занимает оперативную память во время выполнения, или компилятор просто игнорирует эту переменную?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
В приведенном выше примере член var1 получает значение, которое затем отображается в консоли. Однако «Var2» вообще не используется. Поэтому запись его в память во время выполнения будет пустой тратой ресурсов. Учитывает ли компилятор такие ситуации и просто игнорирует неиспользуемые переменные, или объект Foo всегда имеет один и тот же размер, независимо от того, используются ли его члены?
var2
нет.sizeof(Foo)
не может уменьшаться по определению - если вы печатаете,sizeof(Foo)
он должен уступить8
(на обычных платформах). Компиляторы могут оптимизировать пространство, используемоеvar2
(независимо от того, находится лиnew
он в стеке или в вызовах функций ...) в любом контексте, который они сочтут разумным, даже без LTO или оптимизации всей программы. Там, где это невозможно, они не будут этого делать, как и при любой другой оптимизации. Я считаю, что изменение принятого ответа значительно снижает вероятность того, что он будет введен в заблуждение.Ответы:
Золотое правило C ++ «как если бы» 1 гласит, что если наблюдаемое поведение программы не зависит от существования неиспользуемого члена-данных, компилятору разрешается его оптимизировать .
Нет (если он «действительно» не используется).
Теперь возникают два вопроса:
Начнем с примера.
пример
#include <iostream> struct Foo1 { int var1 = 5; Foo1() { std::cout << var1; } }; struct Foo2 { int var1 = 5; int var2; Foo2() { std::cout << var1; } }; void f1() { (void) Foo1{}; } void f2() { (void) Foo2{}; }
Если мы попросим gcc скомпилировать эту единицу перевода , он выдаст:
f1(): mov esi, 5 mov edi, OFFSET FLAT:_ZSt4cout jmp std::basic_ostream<char, std::char_traits<char> >::operator<<(int) f2(): jmp f1()
f2
то же самоеf1
, и никакая память никогда не используется для хранения фактическогоFoo2::var2
. ( Clang делает нечто подобное ).Обсуждение
Некоторые могут сказать, что это другое, по двум причинам:
Что ж, хорошая программа - это умная и сложная сборка простых вещей, а не простое сопоставление сложных вещей. В реальной жизни вы пишете множество простых функций, используя простые структуры, которые компилятор не оптимизирует. Например:
bool insert(std::set<int>& set, int value) { return set.insert(value).second; }
Это реальный пример
std::pair<std::set<int>::iterator, bool>::first
неиспользования элемента данных (здесь ). Угадай, что? Он оптимизирован ( более простой пример с фиктивным набором, если эта сборка заставляет вас плакать).Сейчас самое время прочитать отличный ответ Макса Лангхофа (проголосуйте за него, пожалуйста). Это объясняет, почему, в конце концов, концепция структуры не имеет смысла на уровне сборки, выводимой компилятором.
«Но если я сделаю X, то факт, что неиспользуемый член будет оптимизирован, станет проблемой!»
Было несколько комментариев, в которых утверждалось, что этот ответ должен быть неправильным, потому что какая-то операция (например
assert(sizeof(Foo2) == 2*sizeof(int))
) что-то сломает.Если X является частью наблюдаемого поведения программы 2 , компилятору не разрешается выполнять оптимизацию. Есть много операций с объектом, содержащим "неиспользуемый" элемент данных, которые могли бы иметь заметный эффект на программу. Если такая операция выполняется или компилятор не может доказать, что ничего не было выполнено, этот «неиспользуемый» элемент данных является частью наблюдаемого поведения программы и не может быть оптимизирован .
Операции, влияющие на наблюдаемое поведение, включают, но не ограничиваются:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Похоже на то, что утверждение прошло успешно или нет.
источник
assert(sizeof(…)…)
самом деле не ограничивает компилятор - он должен предоставлятьsizeof
код, который позволяет использовать такие вещи, какmemcpy
работа, но это не означает, что компилятор каким-то образом должен использовать такое количество байтов, если они не могут быть подвергнуты такому воздействию,memcpy
что он может «т переписан в любом случае произвести правильное значение.Важно понимать, что код, создаваемый компилятором, не имеет фактических сведений о ваших структурах данных (потому что таких вещей не существует на уровне сборки), как и оптимизатор. Компилятор создает только код для каждой функции , но не структуры данных .
Хорошо, он также записывает постоянные разделы данных и тому подобное.
Исходя из этого, мы уже можем сказать, что оптимизатор не будет «удалять» или «исключать» члены, потому что он не выводит структуры данных. Он выводит код , который может использовать или не использовать элементы, и среди его целей экономия памяти или циклов за счет исключения бессмысленного использования (например, записи / чтения) членов.
Суть этого в том, что «если компилятор может доказать в рамках функции (включая функции, которые были встроены в нее), что неиспользуемый член не имеет значения для того, как функция работает (и что она возвращает), то велики шансы, что присутствие члена не вызывает накладных расходов ».
По мере того, как вы делаете взаимодействие функции с внешним миром более сложным / неясным для компилятора (принимать / возвращать более сложные структуры данных, например
std::vector<Foo>
, скрывать определение функции в другом модуле компиляции, запрещать / препятствовать встраиванию и т. Д.) , становится все более и более вероятным, что компилятор не может доказать, что неиспользуемый член не действует.Здесь нет жестких правил, потому что все зависит от оптимизаций, которые делает компилятор, но пока вы делаете тривиальные вещи (например, как показано в ответе YSC), очень вероятно, что никаких накладных расходов не будет, тогда как выполнение сложных вещей (например, возврат a
std::vector<Foo>
из функции, слишком большой для встраивания), вероятно, вызовет накладные расходы.Чтобы проиллюстрировать это, рассмотрим этот пример :
struct Foo { int var1 = 3; int var2 = 4; int var3 = 5; }; int test() { Foo foo; std::array<char, sizeof(Foo)> arr; std::memcpy(&arr, &foo, sizeof(Foo)); return arr[0] + arr[4]; }
Здесь мы делаем нетривиальные вещи (берем адреса, проверяем и добавляем байты из байтового представления ), и все же оптимизатор может выяснить, что результат всегда один и тот же на этой платформе:
test(): # @test() mov eax, 7 ret
Мало того, что участники
Foo
не занимали никакой памяти,Foo
они даже не возникли! Если есть другие варианты использования, которые нельзя оптимизировать, то, например, этоsizeof(Foo)
может иметь значение - но только для этого сегмента кода! Если бы все использования можно было оптимизировать таким образом, то существование, напримерvar3
, не влияет на сгенерированный код. Но даже если он будет использоваться где-то еще,test()
останется оптимизированным!Вкратце: каждое использование
Foo
оптимизируется независимо. Некоторые могут использовать больше памяти из-за ненужного элемента, некоторые - нет. Для получения более подробной информации обратитесь к руководству вашего компилятора.источник
Компилятор оптимизирует неиспользуемую переменную-член (особенно общедоступную) только в том случае, если он сможет доказать, что удаление переменной не имеет побочных эффектов и что никакая часть программы не зависит от ее размера
Foo
.Я не думаю, что какой-либо текущий компилятор выполняет такую оптимизацию, если структура вообще не используется. Некоторые компиляторы могут, по крайней мере, предупреждать о неиспользуемых частных переменных, но обычно не о публичных.
источник
В общем, вы должны предположить, что получили то, о чем просили, например, «неиспользуемые» переменные-члены присутствуют.
Поскольку в вашем примере оба члена являются членами
public
, компилятор не может знать, будет ли какой-либо код (особенно из других единиц перевода = другие файлы * .cpp, которые компилируются отдельно, а затем связываются) обращаться к «неиспользуемому» члену.Ответ YSC дает очень простой пример, где тип класса используется только как переменная с автоматической продолжительностью хранения и где указатель на эту переменную не берется. Там компилятор может встроить весь код, а затем удалить весь мертвый код.
Если у вас есть интерфейсы между функциями, определенными в разных единицах перевода, обычно компилятор ничего не знает. Интерфейсы обычно соответствуют некоторому предопределенному ABI (например, этому ), так что разные объектные файлы могут быть связаны вместе без каких-либо проблем. Обычно ABI не имеют значения, используется член или нет. Итак, в таких случаях второй член должен физически находиться в памяти (если позже компоновщик не удалит его позже).
И пока вы находитесь в границах языка, вы не можете наблюдать, что происходит какое-либо устранение. Если позвонишь
sizeof(Foo)
, получишь2*sizeof(int)
. Если вы создаете массивFoo
s, расстояние между началом двух последовательных объектовFoo
всегда равноsizeof(Foo)
байтам.Ваш тип является стандартным типом макета , что означает, что вы также можете получить доступ к членам на основе вычисленных смещений во время компиляции (см.
offsetof
Макрос). Более того, вы можете проверить побайтовое представление объекта, скопировав его в массивchar
usingstd::memcpy
. Во всех этих случаях можно наблюдать присутствие второго члена.источник
gcc -fwhole-program -O3 *.c
теоретически может это сделать, но на практике, вероятно, нет. (например, в случае, если программа делает некоторые предположения о том, какое точное значениеsizeof()
имеет эта цель, и потому что это действительно сложная оптимизация, которую программисты должны делать вручную, если они этого хотят.)Примеры, представленные другими ответами на этот вопрос, которые исключены
var2
, основаны на единственном методе оптимизации: постоянное распространение и последующее исключение всей структуры (а не исключение толькоvar2
). Это простой случай, и оптимизирующие компиляторы его реализуют.Для неуправляемых кодов C / C ++ ответ заключается в том, что компилятор, как правило, не игнорирует
var2
. Насколько мне известно, такое преобразование структуры C / C ++ не поддерживается в отладочной информации, и если структура доступна как переменная в отладчике,var2
ее нельзя исключить. Насколько я знаю, текущий компилятор C / C ++ не может специализировать функции в соответствии с исключениемvar2
, поэтому, если структура передается или возвращается из не встроенной функции, тоvar2
ее нельзя исключить.Для управляемых языков, таких как C # / Java с JIT-компилятором, компилятор может безопасно исключить,
var2
потому что он может точно отслеживать, используется ли он и уходит ли он в неуправляемый код. Физический размер структуры в управляемых языках может отличаться от размера, сообщаемого программисту.Компиляторы C / C ++ 2019 года не могут быть исключены
var2
из структуры, если не исключена вся переменная структуры. Для интересных случаев исключенияvar2
из структуры ответ: Нет.Некоторые будущие компиляторы C / C ++ смогут исключить
var2
из структуры, и экосистема, построенная вокруг компиляторов, должна будет адаптироваться для обработки информации исключения, генерируемой компиляторами.источник
Это зависит от вашего компилятора и уровня его оптимизации.
В gcc, если вы укажете
-O
, будут включены следующие флаги оптимизации :-fauto-inc-dec -fbranch-count-reg -fcombine-stack-adjustments -fcompare-elim -fcprop-registers -fdce -fdefer-pop ...
-fdce
означает удаление мертвого кода .Вы можете использовать,
__attribute__((used))
чтобы предотвратить удаление gcc неиспользуемой переменной со статическим хранилищем:источник