Есть ли значительная стоимость встроенного объекта в С++?

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

Теперь я сделал это изменение, но это заставило меня задуматься, какова стоимость создания экземпляра на С++? Я знаю, что на управляемых языках есть все затраты на сбор мусора, который был бы значительным. Однако мой объект С++ был просто в стеке, он не содержал каких-либо виртуальных методов, поэтому не было бы затрат на поиск функции времени исполнения. Я использовал новый механизм удаления С++ 11 для удаления операторов копирования/присваивания по умолчанию, поэтому никакого копирования не было. Это был простой объект с конструктором, который выполнял небольшую работу (обязательно в любом случае со статическими методами) и деструктор, который ничего не делал. В любом случае, скажите мне, что такое instation consts? (Рецензент немного запугивает, и я не хочу выглядеть глупо, спрашивая его!); -)

Ответ 1

Короткий ответ - по сути, распределение объектов дешево, но в некоторых случаях может стать дорогостоящим.

Длинный ответ

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

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

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

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

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

Итак, нижняя строка заключается в том, что это зависит от того, как и что объект является тем, что вы статируете.

Ответ 2

Если ваш объект-тип должен вызывать нетривиальный конструктор и деструктор в течение его жизненного цикла, тогда стоимость будет минимальной стоимостью создания любого объекта С++, который имеет нетривиальный конструктор и деструктор. Остальные методы static не уменьшат эту стоимость. "Цена" пространства будет составлять не менее 1 байт, так как ваш класс не является базовым классом производного класса, и единственной экономией при вызове метода класса static будет упущение неявного this указатель передан как скрытый первый аргумент вызова, то, что потребуется для методов нестатического класса.

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

Ответ 3

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

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

Тест-сценарий/пример упростит ответ, категорически, дальше, чем "вы должны его спросить".

Ответ 4

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

  • пользовательский ввод
  • сетевые вызовы
  • доступ к базе данных
  • интенсивное вычисление algos
  • затраты на переключение потоков
  • системные вызовы

Я думаю, что в большинстве случаев инкапсуляция в класс для дизайна превосходит небольшие затраты на создание экземпляра. Конечно, могут быть те 1% случаев, когда это не выполняется, но является вашим одним из них?

Ответ 5

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

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

Ответ 6

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

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

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

#include <algorithm>
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>

bool int_lt(int a, int b)
{
    return a < b;
}

int
main()
{
    size_t const N = 50000000;
    std::vector<int> c1;
    c1.reserve(N);
    for (size_t i = 0; i < N; ++i) {
        int r = rand();
        c1.push_back(r);
    }
    std::vector<int> c2 = c1;
    std::vector<int> c3 = c1;

    clock_t t1 = clock();
    std::sort(c2.begin(), c2.end(), std::less<int>()); 
    clock_t t2 = clock();
    std::sort(c3.begin(), c3.end(), int_lt);
    clock_t t3 = clock();

    std::cerr << (t2 - t1) / double(CLOCKS_PER_SEC) << '\n';
    std::cerr << (t3 - t2) / double(CLOCKS_PER_SEC) << '\n';

    return 0;
}

На моем i7 Linux, потому что g++ не может встроить функцию int_lt, но может inline std:: less:: operator(), версия non member-функции примерно на 50% медленнее.

> g++-4.5 -O2 p3.cc 
> ./a.out 
3.85
5.88

Чтобы понять, почему такая большая разница, вам нужно рассмотреть, какой тип компилятор для компаратора. В случае int_lt он указывает тип bool (*) (int, int), тогда как при std:: less он указывает std:: less. С помощью указателя функции функция, которая будет вызываться, известна только во время выполнения. Это означает, что компилятор не может встроить свое определение во время компиляции. В отличие от std:: less у компилятора есть доступ к типу и его определению во время компиляции, чтобы он мог встроить std:: less:: operator(). Это в значительной степени влияет на производительность в этом случае.

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

#include <iostream>
#include <time.h>
#include <vector>
#include <stdlib.h>

typedef long (*fp_t)(long, long);

inline long add(long a, long b)
{
    return a + b;
}

struct add_fn {
    long operator()(long a, long b) const
    {
        return a + b;
    }
};

long f(std::vector<long> const& x, fp_t const add, long init)
{
    for (size_t i = 0, sz = x.size(); i < sz; ++i)
        init = add(init, x[i]);
    return init;        
}

long g(std::vector<long> const& x, add_fn const add, long init)
{
    for (size_t i = 0, sz = x.size(); i < sz; ++i)
        init = add(init, x[i]);
    return init;        
}

int
main()
{
    size_t const N = 5000000;
    size_t const M = 100;
    std::vector<long> c1;
    c1.reserve(N);
    for (size_t i = 0; i < N; ++i) {
        long r = rand();
        c1.push_back(r);
    }
    std::vector<long> c2 = c1;
    std::vector<long> c3 = c1;

    clock_t t1 = clock();
    for (size_t i = 0; i < M; ++i)
        long s2 = f(c2, add, 0);
    clock_t t2 = clock();
    for (size_t i = 0; i < M; ++i)
        long s3 = g(c3, add_fn(), 0);
    clock_t t3 = clock();

    std::cerr << (t2 - t1) / double(CLOCKS_PER_SEC) << '\n';
    std::cerr << (t3 - t2) / double(CLOCKS_PER_SEC) << '\n';
    return 0;
}

Курсорное тестирование показывает, что свободная функция на 100% медленнее, чем функция-член.

> g++ -O2 p5.cc 
> ./a.out 
0.87
0.32

Bjarne Stroustrup недавно предоставил отличную лекцию на С++ 11, которая затрагивает это. Вы можете посмотреть его по ссылке ниже.

http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style