Язык программирования C++. Вводный курс

Соображения эффективности A


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

bool sufficient_funds( Account acct, double );

то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct

значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур:

bool sufficient_funds( Account *pacct, double );

bool sufficient_funds( Account &acct, double );

то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).

Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения:



// задача решается, но для больших матриц эффективность может

// оказаться неприемлемо низкой

Matrix

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix result;

   // выполнить арифметические операции ...

   return result;

}

Этот перегруженный оператор позволяет пользователю писать

Matrix a, b;

// ...

// в обоих случаях вызывается operator+()

Matrix c = a + b;

a = b + c;

Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix

представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.

Следующая пересмотренная реализация намного увеличивает скорость:

// более эффективно, но после возврата адрес оказывается недействительным

// это может привести к краху программы

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix result;

   // выполнить сложение ...

   return result;

}

но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)


Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается:



// нет возможности гарантировать отсутствие утечки памяти

// поскольку матрица может быть большой, утечки будут весьма заметными

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix *result = new Matrix;

   // выполнить сложение ...

   return *result;
}

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

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



// это обеспечивает нужную эффективность,

// но не является интуитивно понятным для пользователя

void

mat_add( Matrix &result,

         const Matrix& m1, const Matrix& m3 )

{

   // вычислить результат
}

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



// более не поддерживается
Matrix c = a + b;

и использовать их в выражениях:



// тоже не поддерживается
if ( a + b > c ) ...

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



Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

name result

{

   Matrix result;

   // ...

   return result;
}

Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку:



// переписанная компилятором функция

// в случае принятия предлагавшегося расширения языка

void

operator+( Matrix &result, const Matrix& m1, const Matrix& m2 )

name result

{

   // вычислить результат
}

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



Matrix c = a + b;

было бы трансформировано в



Matrix c;
operator+(c, a, b);

Это расширение так и не стало частью языка, но предложенная оптимизация прижилась. Компилятор в состоянии распознать, что возвращается объект класса и выполнить трансформацию его значения и без явного расширения языка. Если дана функция общего вида:



classType

functionName( paramList )

{

   classType namedResult;

   // выполнить какие-то действия ...

   return namedResult;
}

то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней:



void

functionName( classType &namedResult, paramList )

{

   // вычислить результат и разместить его по адресу namedResult
}

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

И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида

Matrix c = a + b;

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



Matrix c;
c = a + b;

но объем требуемых вычислений значительно больше. Аналогично эффективнее писать:



for ( int ix = 0; ix < size-2; ++ix ) {

     Matrix matSum = mat[ix] + mat[ix+1];

     // ...
}

чем



Matrix matSum;

for ( int ix = 0; ix < size-2; ++ix ) {

     matSum = mat[ix] + mat[ix+1];

     // ...
}

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

Point3d p3 = operator+( p1, p2 );

можно безопасно трансформировать:



// Псевдокод на C++

Point3d p3;
operator+( p3, p1, p2 );

преобразование



Point3d p3;
p3 = operator+( p1, p2 );

в



// Псевдокод на C++

// небезопасно в случае присваивания
operator+( p3, p1, p2 );



небезопасно.

Преобразованная функция требует, чтобы переданный ей объект представлял собой неформатированную область памяти. Почему? Потому что к объекту сразу применяется конструктор, который уже был применен к именованному локальному объекту. Если переданный объект уже был сконструирован, то делать это еще раз с семантической точки зрения неверно.

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

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



Point3d p3;
p3 = operator+( p1, p2 );

трансформируется в такой:



// Псевдокод на C++

Point3d temp;

operator+( temp, p1, p2 );

p3.Point3d::operator=( temp );
temp.Point3d::~Point3d();

Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением

(return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге “Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем.

15


Содержание раздела