Занимает ли неиспользуемая переменная-член память? - PullRequest
88 голосов
/ 08 марта 2019

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

struct Foo {
    int var1;
    int var2;

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

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

Ответы [ 6 ]

104 голосов
/ 08 марта 2019

Золотое правило C ++ "as-if" 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),
  • квалифицируя объект как volatile ,
  • и т. д. .

1)

[intro.abstract]/1

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

2) Подобно тому, как утверждение или пропуск не соответствует.

61 голосов
/ 08 марта 2019

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

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

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


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

По мере того, как вы делаете взаимодействие функции с внешним миром более сложным / неясным для компилятора (принимать / возвращать более сложные структуры данных, например, std::vector<Foo>, скрыть определение функции в другом модуле компиляции, запретить / отменяет включение строк и т. д.), становится все более вероятным, что компилятор не сможет доказать, что неиспользованный элемент не имеет никакого эффекта.

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

21 голосов
/ 08 марта 2019

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

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

7 голосов
/ 08 марта 2019

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

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

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

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

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

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

6 голосов
/ 08 марта 2019

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

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

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

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

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

4 голосов
/ 08 марта 2019

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...