Неиспользуемая переменная-член занимает память?

92

Инициализация переменной-члена, а не обращение к ней / ее использование, дополнительно занимает оперативную память во время выполнения, или компилятор просто игнорирует эту переменную?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

В приведенном выше примере член var1 получает значение, которое затем отображается в консоли. Однако «Var2» вообще не используется. Поэтому запись его в память во время выполнения будет пустой тратой ресурсов. Учитывает ли компилятор такие ситуации и просто игнорирует неиспользуемые переменные, или объект Foo всегда имеет один и тот же размер, независимо от того, используются ли его члены?

Крисс555888
источник
25
Это зависит от компилятора, архитектуры, операционной системы и используемой оптимизации.
Owl
16
Существует тонна кода низкоуровневого драйвера, который специально добавляет бездействующие элементы структуры для заполнения, чтобы соответствовать размерам аппаратных фреймов данных, и в качестве взлома для получения желаемого выравнивания памяти. Если бы компилятор начал их оптимизировать, это привело бы к большим сбоям.
Энди Браун
2
@ Энди, они на самом деле ничего не делают, поскольку вычисляется адрес следующих членов данных. Это означает, что наличие этих элементов заполнения действительно имеет наблюдаемое поведение в программе. Здесь var2нет.
СМУ
4
Я был бы удивлен, если бы компилятор мог оптимизировать его, учитывая, что любая единица компиляции, обращающаяся к такой структуре, может быть связана с другой единицей компиляции, используя ту же структуру, и компилятор не может знать, обращается ли отдельный модуль компиляции к члену или нет.
Галик
2
@geza sizeof(Foo)не может уменьшаться по определению - если вы печатаете, sizeof(Foo)он должен уступить 8(на обычных платформах). Компиляторы могут оптимизировать пространство, используемое var2(независимо от того, находится ли newон в стеке или в вызовах функций ...) в любом контексте, который они сочтут разумным, даже без LTO или оптимизации всей программы. Там, где это невозможно, они не будут этого делать, как и при любой другой оптимизации. Я считаю, что изменение принятого ответа значительно снижает вероятность того, что он будет введен в заблуждение.
Макс Лангхоф

Ответы:

107

Золотое правило C ++ «как если бы» 1 гласит, что если наблюдаемое поведение программы не зависит от существования неиспользуемого члена-данных, компилятору разрешается его оптимизировать .

Неиспользуемая переменная-член занимает память?

Нет (если он «действительно» не используется).


Теперь возникают два вопроса:

  1. Когда наблюдаемое поведение не зависело бы от существования члена?
  2. Встречаются ли подобные ситуации в реальных программах?

Начнем с примера.

пример

#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 делает нечто подобное ).

Обсуждение

Некоторые могут сказать, что это другое, по двум причинам:

  1. это слишком банальный пример,
  2. структура полностью оптимизирована, это не в счет.

Что ж, хорошая программа - это умная и сложная сборка простых вещей, а не простое сопоставление сложных вещей. В реальной жизни вы пишете множество простых функций, используя простые структуры, которые компилятор не оптимизирует. Например:

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)

[intro.abstract]/1

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

2) Похоже на то, что утверждение прошло успешно или нет.

YSC
источник
Комментарии, предлагающие улучшения к ответу, были заархивированы в чате .
Коди Грей
1
Даже на assert(sizeof(…)…)самом деле не ограничивает компилятор - он должен предоставлять sizeofкод, который позволяет использовать такие вещи, как memcpyработа, но это не означает, что компилятор каким-то образом должен использовать такое количество байтов, если они не могут быть подвергнуты такому воздействию, memcpyчто он может «т переписан в любом случае произвести правильное значение.
Дэвис Херринг,
@ Дэвис Совершенно верно.
YSC
64

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

Хорошо, он также записывает постоянные разделы данных и тому подобное.

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


Суть этого в том, что «если компилятор может доказать в рамках функции (включая функции, которые были встроены в нее), что неиспользуемый член не имеет значения для того, как функция работает (и что она возвращает), то велики шансы, что присутствие члена не вызывает накладных расходов ».

По мере того, как вы делаете взаимодействие функции с внешним миром более сложным / неясным для компилятора (принимать / возвращать более сложные структуры данных, например 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оптимизируется независимо. Некоторые могут использовать больше памяти из-за ненужного элемента, некоторые - нет. Для получения более подробной информации обратитесь к руководству вашего компилятора.

Макс Лангхоф
источник
6
Падение микрофона «За подробностями обратитесь к руководству по компилятору». : D
СМУ
22

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

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

Алан Бертлс
источник
1
И тем не менее: godbolt.org/z/UJKguS + ни один компилятор не предупредит о неиспользуемом элементе данных.
СМУ
@YSC clang ++ предупреждает о неиспользуемых элементах данных и переменных.
Максим Егорушкин
3
@YSC Я думаю, что это немного другая ситуация, он полностью оптимизировал структуру и просто печатает 5 напрямую
Алан Бертлс
4
@AlanBirtles Я не понимаю, чем это отличается. Компилятор оптимизировал все, начиная с объекта, что не влияет на наблюдаемое поведение программы. Итак, ваше первое предложение «вряд ли компилятор оптимизирует неиспользуемую переменную-член» неверно.
СМУ
2
@YSC в реальном коде, где структура фактически используется, а не просто построена для ее побочных эффектов, вероятно, более маловероятно, что она будет оптимизирована,
Алан Бертлс
7

В общем, вы должны предположить, что получили то, о чем просили, например, «неиспользуемые» переменные-члены присутствуют.

Поскольку в вашем примере оба члена являются членами public, компилятор не может знать, будет ли какой-либо код (особенно из других единиц перевода = другие файлы * .cpp, которые компилируются отдельно, а затем связываются) обращаться к «неиспользуемому» члену.

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

Если у вас есть интерфейсы между функциями, определенными в разных единицах перевода, обычно компилятор ничего не знает. Интерфейсы обычно соответствуют некоторому предопределенному ABI (например, этому ), так что разные объектные файлы могут быть связаны вместе без каких-либо проблем. Обычно ABI не имеют значения, используется член или нет. Итак, в таких случаях второй член должен физически находиться в памяти (если позже компоновщик не удалит его позже).

И пока вы находитесь в границах языка, вы не можете наблюдать, что происходит какое-либо устранение. Если позвонишь sizeof(Foo), получишь 2*sizeof(int). Если вы создаете массив Foos, расстояние между началом двух последовательных объектов Fooвсегда равно sizeof(Foo)байтам.

Ваш тип является стандартным типом макета , что означает, что вы также можете получить доступ к членам на основе вычисленных смещений во время компиляции (см. offsetofМакрос). Более того, вы можете проверить побайтовое представление объекта, скопировав его в массив charusing std::memcpy. Во всех этих случаях можно наблюдать присутствие второго члена.

Под рукой999
источник
Комментарии не предназначены для расширенного обсуждения; этот разговор был перемещен в чат .
Коди Грей
2
+1: только агрессивная оптимизация всей программы могла бы отрегулировать макет данных (включая размеры и смещения времени компиляции) для случаев, когда локальный объект структуры не оптимизирован полностью,. gcc -fwhole-program -O3 *.cтеоретически может это сделать, но на практике, вероятно, нет. (например, в случае, если программа делает некоторые предположения о том, какое точное значение sizeof()имеет эта цель, и потому что это действительно сложная оптимизация, которую программисты должны делать вручную, если они этого хотят.)
Питер Кордес
6

Примеры, представленные другими ответами на этот вопрос, которые исключены var2, основаны на единственном методе оптимизации: постоянное распространение и последующее исключение всей структуры (а не исключение только var2). Это простой случай, и оптимизирующие компиляторы его реализуют.

Для неуправляемых кодов C / C ++ ответ заключается в том, что компилятор, как правило, не игнорирует var2. Насколько мне известно, такое преобразование структуры C / C ++ не поддерживается в отладочной информации, и если структура доступна как переменная в отладчике, var2ее нельзя исключить. Насколько я знаю, текущий компилятор C / C ++ не может специализировать функции в соответствии с исключением var2, поэтому, если структура передается или возвращается из не встроенной функции, то var2ее нельзя исключить.

Для управляемых языков, таких как C # / Java с JIT-компилятором, компилятор может безопасно исключить, var2потому что он может точно отслеживать, используется ли он и уходит ли он в неуправляемый код. Физический размер структуры в управляемых языках может отличаться от размера, сообщаемого программисту.

Компиляторы C / C ++ 2019 года не могут быть исключены var2из структуры, если не исключена вся переменная структуры. Для интересных случаев исключения var2из структуры ответ: Нет.

Некоторые будущие компиляторы C / C ++ смогут исключить var2из структуры, и экосистема, построенная вокруг компиляторов, должна будет адаптироваться для обработки информации исключения, генерируемой компиляторами.

атомосимвол
источник
1
Ваш параграф об отладочной информации сводится к следующему: «Мы не можем оптимизировать его, если это усложнит отладку», что совершенно неверно. Или я неправильно читаю. Не могли бы вы уточнить?
Макс Лангхоф
Если компилятор выдает отладочную информацию о структуре, он не может исключить var2. Возможны следующие варианты: (1) Не выдавать отладочную информацию, если она не соответствует физическому представлению структуры, (2) Поддерживать исключение члена структуры в отладочной информации и
выдавать
Возможно, более общим является упоминание скалярной замены агрегатов (а затем исключения мертвых хранилищ и т . Д. ).
Дэвис Херринг,
4

Это зависит от вашего компилятора и уровня его оптимизации.

В gcc, если вы укажете -O, будут включены следующие флаги оптимизации :

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdceозначает удаление мертвого кода .

Вы можете использовать, __attribute__((used))чтобы предотвратить удаление gcc неиспользуемой переменной со статическим хранилищем:

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

При применении к статическому элементу данных шаблона класса C ++ атрибут также означает, что создается экземпляр элемента, если создается экземпляр самого класса.

Wonter
источник
Это для статических элементов данных, а не для неиспользуемых элементов для каждого экземпляра (которые не оптимизируются, если не оптимизируется весь объект). Но да, я думаю, это имеет значение. Кстати, устранение неиспользуемых статических переменных - это не исключение мертвого кода , если только GCC не изменит термин.
Питер Кордес