Проблема копирования возвращаемого значения (для улучшения времени отладки) - какое здесь решение? - PullRequest
2 голосов
/ 22 сентября 2011

Самый интересный вопрос C ++, с которым я недавно столкнулся, выглядит следующим образом:

Мы определили (через профилирование), что наш алгоритм тратит много времени в режиме отладки в MS Visual Studio 2005 с функциями следующего типа:

MyClass f(void)
{
      MyClass retval;
      // some computation to populate retval

      return retval;
}

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

Итак, один из наших ребят предложил классное (хотя и несколько ошибочное) решение этой проблемы, а именно создание оператора преобразования:

MyClass::MyClass(MyClass *t)
{
      // construct "*this" by transferring the contents of *t to *this   
      // the code goes something like this
      this->m_dataPtr = t->m_dataPtr;  

      // then clear the pointer in *t so that its destruction still works
      // but becomes 'trivial'
      t->m_dataPtr = 0;
}

, а также изменив указанную выше функцию на:

MyClass f(void)
{
      MyClass retval;
      // some computation to populate retval

      // note the ampersand here which calls the conversion operator just defined
      return &retval;      
}

Теперь, прежде чем вы съежитесь (что я и делаю, когда пишу это), позвольте мне объяснить обоснование. Идея состоит в том, чтобы создать оператор преобразования, который в основном выполняет «перенос содержимого» во вновь созданную переменную. Экономия происходит потому, что мы больше не делаем глубокую копию, а просто переносим память по ее указателю. Код переходит от 10-минутного времени отладки к 30-секундному времени отладки, что, как вы можете себе представить, оказывает огромное положительное влияние на производительность. Конечно, оптимизация возвращаемого значения лучше работает в режиме деблокирования, но за счет невозможности вмешаться и посмотреть наши переменные.

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

  void BigFunction(void)
  {
        MyClass *SomeInstance = new MyClass;

        // populate SomeInstance somehow

        g(SomeInstance);

        // some code that uses SomeInstance later
        ...
  }

, где g определяется как:

  void g(MyClass &m)
  {
      // irrelevant what happens here.
  }

Теперь это произошло случайно, то есть человек, который вызвал g(), не должен был передавать указатель, когда ожидалась ссылка. Тем не менее, не было никакого предупреждения компилятора (конечно). Компилятор точно знал, как конвертировать, и он сделал это. Проблема в том, что вызов g() будет (потому что мы передали ему MyClass *, когда он ожидал MyClass &), вызвал оператор преобразования, что плохо, потому что он установил внутренний указатель в SomeInstance в 0 и делает SomeInstance бесполезным для кода, который произошел после вызова g(). ... и началась отладка.

Итак, мой вопрос: как мы можем получить это ускорение в режиме отладки (который имеет прямое преимущество в отладке) с чистым кодом, который не открывает возможности для того, чтобы такие другие ужасные ошибки проскальзывали через трещины?

Я также собираюсь подсластить банк на этом и предложить свою первую награду на этот, как только он получит право. (50 баллов)

Ответы [ 8 ]

5 голосов
/ 22 сентября 2011

Вам нужно использовать то, что называется «сваптимизация».

MyClass f(void)
{
      MyClass retval;
      // some computation to populate retval

      return retval;
}
int main() {
    MyClass ret;
    f().swap(ret);
}

Это предотвратит копирование и , сохраняющих код чистым во всех режимах.

Вы также можете попробовать тот же трюк, что и auto_ptr, но это больше, чем немного сомнительно.

4 голосов
/ 22 сентября 2011

Если ваше определение g написано так же, как в вашей кодовой базе, я не уверен, как оно скомпилировано, поскольку компилятору не разрешается привязывать безымянные временные ссылки к неконстантным ссылкам. Это может быть ошибка в VS2005.

Если вы сделаете конвертирующий конструктор explicit, вы можете использовать его в своих функциях (вы должны были бы сказать return MyClass(&retval);), но в вашем примере он не будет вызываться, если преобразование не было явно вызвано.

Поочередно перейдите к компилятору C ++ 11 и используйте полную семантику перемещения.

(Обратите внимание, что используемая оптимизация фактическая является именованной оптимизацией возвращаемого значения или NRVO).

2 голосов
/ 23 сентября 2011

Другой подход, учитывая ваш особый сценарий:

Измените MyClass f(void) (или operator+) на что-то вроде следующего:

MyClass f(void)
{
   MyClass c;
   inner_f(c);
   return c;
}

И пусть inner_f(c) содержит фактическую логику:

#ifdef TESTING
#   pragma optimize("", off)
#endif

inline void inner_f(MyClass& c)
{
   // actual logic here, setting c to whatever needed
}

#ifdef TESTING
#   pragma optimize("", on)
#endif

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

Таким образом, вы все равно можете воспользоваться RVO в f(), но реальная логика не будет оптимизирована в вашей тестовой сборке. Обратите внимание, что тестовая сборка может быть либо сборкой выпуска, либо сборкой отладки с включенными оптимизациями. В любом случае, чувствительные части кода не будут оптимизированы (вы можете использовать #pragma optimize, конечно же, и в других местах - в приведенном выше коде это касается только самого inner_f, а не кода звонил с него).

2 голосов
/ 22 сентября 2011

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

Существует шаблон функции std::move, который превращает lvalue в ссылку на rvalue, то есть дает «разрешение» на перемещение с объекта [*]. Я считаю, что вы можете подражать этому для своего класса, хотя я не сделаю это бесплатной функцией:

struct MyClass;
struct MovableMyClass {
    MyClass *ptr;
    MovableMyClass(MyClass *ptr) : ptr(ptr) {}
};

struct MyClass {
    MyClass(const MovableMyClass &tc) {
        // unfortunate, we need const reference to bind to temporary
        MovableMyClass &t = const_cast<MovableMyClass &>(tc);
        this->m_dataPtr = t.ptr->m_dataPtr;
        t.ptr->m_dataPtr = 0;
    }
    MovableMyClass move() {
        return MovableMyClass(this);
    }
};

MyClass f(void)
{
    MyClass retval;
    return retval.move();
}

Я не проверял это, но что-то в этом роде. Обратите внимание на возможность сделать что-то const-unsafe с MovableMyClass объектом, который на самом деле является const, но должно быть проще избежать создания одного из них, чем избегать создания MyClass* (который вы нашли выход довольно сложный!)

[*] На самом деле я почти уверен, что я слишком упростил то, что до такой степени, что на самом деле речь идет о влиянии перегрузки, а не о "превращении" чего-либо в нечто другое. Но для перемещения * вместо копии std::move.

2 голосов
/ 22 сентября 2011

Проблема возникает из-за того, что вы используете MyClass* в качестве магического устройства, иногда , но не всегда. Решение: используйте другое магическое устройство.

class MyClass;
class TempClass { //all private except destructor, no accidental copies by callees
    friend MyClass;

    stuff* m_dataPtr; //unfortunately requires duplicate data
                      //can't really be tricked due to circular dependancies.

    TempClass() : m_dataPtr(NULL) {}
    TempClass(stuff* p) : m_dataPtr(p) {}
    TempClass(const TempClass& p) : m_dataPtr(p) {}
public:
    ~TempClass() {delete m_dataPtr;}
};

class MyClass {
    stuff* m_dataPtr;

    MyClass(const MyClass& b) {
        m_dataPtr = new stuff();
    }
    MyClass(TempClass& b) {
        m_dataPtr = b.m_dataPtr ;
        b.m_dataPtr = NULL;
    }
    ~MyClass() {delete m_dataPtr;}
    //be sure to overload operator= too.
    TempClass f(void) //note: returns hack.  But it's safe
    {
        MyClass retval;
        // some computation to populate retval   
        return retval;
    }
    operator TempClass() {
        TempClass r(m_dataPtr);
        m_dataPtr = nullptr;
        return r;
    }

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

0 голосов
/ 29 сентября 2011

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

MyClass f (void)
{
    MyClass retval;
    MyClass dummy;

    //  ...

    volatile bool b = true;
    if b ? retval : dummy;
}

Что касается того, почему создание копии занимает так много времени в режиме отладки, я понятия не имею.Единственный возможный способ ускорить его, оставаясь в режиме отладки, - это использовать rvalue ссылки и перемещать семантику.Вы уже обнаружили семантику перемещения с помощью вашего конструктора "move", который принимает указатель.C ++ 11 дает правильный синтаксис для этого типа семантики перемещения.Пример:

//  Suppose MyClass has a pointer to something that would be expensive to clone.
//  With move construction we simply move this pointer to the new object.

MyClass (MyClass&& obj) :
    ptr (obj.ptr)
{
    //  We set the source object to some trivial state so it is easy to delete.
    obj.ptr = NULL;
}

MyClass& operator = (MyClass&& obj) :
{
    //  Here we simply swap the pointer so the old object will be destroyed instead of the temporary.
    std::swap(ptr, obj.ptr);
    return *this;
}
0 голосов
/ 28 сентября 2011

Я бы предпочел просто передать объект по ссылке на вызывающую функцию, когда MyClass слишком большой для копирования:

void f(MyClass &retval)  // <--- no worries !
{
  // some computation to populate retval
}

Просто Принцип KISS .

0 голосов
/ 24 сентября 2011

Возможные решения

  • Установить более высокие параметры оптимизации для компилятора, чтобы оптимизировать конструкцию копии
  • Использовать выделение кучи и возвращать указатели или оболочки-указатели, предпочтительно с сборкой мусора
  • Использовать семантику перемещения, представленную в C ++ 11;rvalue ссылки, std :: move, move constructors
  • Используйте некоторые хитрости подкачки, либо в конструкторе копирования, либо так, как это делал DeadMG, но я не рекомендую их с чистой совестью.Несоответствующий конструктор копирования, подобный этому, может вызвать проблемы, а последний немного уродлив и нуждается в легко разрушаемых объектах по умолчанию, что может быть не верно для всех случаев.

  • + 1: проверка и оптимизацияваши конструкторы копий, если они занимают так много времени, значит что-то не так с ними.

...