Распределение кучи/динамической и статической памяти для экземпляра класса Single С++ - программирование
Подтвердить что ты не робот

Распределение кучи/динамической и статической памяти для экземпляра класса Single С++

Мой конкретный вопрос заключается в том, что при реализации singleton class в С++ существуют ли существенные различия между двумя приведенными ниже кодами относительно производительности, побочных проблем или что-то:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // allocating on heap
        static singleton* pInstance = new singleton();
        return *pInstance;
    }
    // ...
};

и это:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // using static variable
        static singleton instance;
        return instance;
    }
    // ...
};


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

UPDATE:

У меня есть интересные ответы и комментарии, которые я пытаюсь обобщить здесь. (Чтение подробных ответов рекомендуется для заинтересованных.):

  • В одноэлементном режиме, используя статическую локальную переменную, деструктор класса автоматически вызывается при завершении процесса, тогда как в случае динамического распределения вам нужно как-то уничтожить уничтожение объекта, например. с помощью интеллектуальных указателей:
    static singleton& getInstance() {
        static std::auto_ptr<singleton> instance (new singleton());
        return *instance.get(); 
    }
  • Синглтон, использующий динамическое распределение, является "более ленивым", чем статическая одноэлементная переменная, как в последнем случае, требуемая память для одноэлементного объекта (всегда?) зарезервирована при запуске процесса (как часть целая память, необходимая для загрузки программы), и только вызов одноэлементного конструктора откладывается до getInstance() времени вызова. Это может иметь значение, когда sizeof(singleton) велико.

  • Оба являются потокобезопасными в С++ 11. Но с более ранними версиями С++ он специфичен для реализации.

  • В случае динамического распределения используется один уровень косвенности для доступа к одиночному объекту, тогда как в случае статического одноэлементного объекта прямой адрес объекта определяется и жестко закодирован во время компиляции.


P.S.: Я исправил терминологию, которую я использовал в оригинальной публикации в соответствии с ответом @TonyD.

4b9b3361

Ответ 1

  • версия new, очевидно, должна выделять память во время выполнения, тогда как версия без указателя имеет память, выделенную во время компиляции (но обе они должны выполнять одну и ту же конструкцию)

  • версия new не будет вызывать деструктор объекта при завершении программы, но версия не new: вы можете использовать интеллектуальный указатель для исправления этого

    • вам нужно быть осторожным, чтобы некоторые деструкторы объекта static/namespace-scope не вызывали ваш синглтон после того, как его статический локальный деструктор экземпляра запустился... если вас это беспокоит, вы, возможно, читаете немного больше о Синглтон жизни и подходы к управлению ими. Andrei Alexandrescu Современный дизайн С++ имеет очень читаемое обращение.
  • в С++ 03, он определяется реализацией, будет ли поток безопасен. (Я считаю, что GCC имеет тенденцию быть, в то время как Visual Studio не стремится к тому, чтобы подтвердить/исправить оценку.)

  • в С++ 11, это безопасно: 6.7.4 "Если элемент управления входит в объявление одновременно при инициализации переменной, одновременное выполнение должно ждать завершения инициализации". (без рекурсии).

Обсуждение re compile-time по сравнению с распределением и инициализацией во время выполнения

Из того, как вы сформулировали свое резюме и несколько комментариев, я подозреваю, что вы не полностью понимаете тонкий аспект распределения и инициализации статических переменных....

Скажите, что ваша программа имеет 3 локальных статических 32-разрядных int - a, b и c - в разных функциях: компилятор может скомпилировать двоичный файл, который сообщает загрузчику ОС оставить 3x32-бит = 12 байт памяти для этой статики. Компилятор решает, что делает смещение каждой из этих переменных: он может помещать a в смещение 1000 hex в сегменте данных, b на 1004 и c на 1008. Когда программа выполняется, загрузчик ОС не имеет значения, t нужно выделять память для каждого отдельно - все, что он знает, это общее количество из 12 байтов, которое может быть запрошено или не спрошено специально для 0-инициализации, но может потребоваться сделать так или иначе, чтобы гарантировать, что процесс не может видеть оставил содержимое памяти из других программ пользователей. Команды машинного кода в программе, как правило, жестко кодируют смещения 1000, 1004, 1008 для доступа к a, b и c - поэтому при выполнении во время выполнения не требуется выделение этих адресов.

Динамическое распределение памяти отличается тем, что указатели (например, p_a, p_b, p_c) будут указаны адреса во время компиляции, как описано, но дополнительно:

  • указательная память (каждая из a, b и c) должна быть найдена во время выполнения (обычно, когда статическая функция выполняется первым, но компилятор разрешает делать это раньше, комментарий к другому ответу), и
    • если в процессе работы операционной системой слишком мало памяти для динамического выделения для успеха, тогда библиотека программ запросит ОС для большей памяти (например, с помощью sbreak())), которые ОС обычно будет уничтожать по соображениям безопасности
    • динамические адреса, назначенные для каждого из a, b и c, должны быть скопированы обратно в указатели p_a, p_b и p_c.

Этот динамический подход явно более запутан.

Ответ 2

Основное отличие состоит в том, что при использовании локального static объект будет уничтожен при закрытии программы, а объекты, выделенные кучей, будут просто оставлены без разрушения.

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

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

Программа, которая сработает перед вводом первой команды main или после выполнения последней команды main, сложнее отлаживать.

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