Передача розумних покажчиків по константної посиланням. Розтин

    Розумні покажчики часто передаються в інші функції з константної посиланням. Експерти C + +, Андрій Александреску, Скотт Мейерс і Герб Саттер, обговорюють це питання на конференції <a href="http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything">C++ and Beyond 2011 (Дивитися з [4:34] On shared_ptr performance and correctness).
 
По суті, розумний покажчик, який передано по константної посиланням, вже живе в поточній області видимості десь в зухвалій коді. Якщо він зберігається в члені класу, то може статися так, що цей член буде обнулено. Але це не проблема передачі за посиланням, це проблема архітектури та політики володіння.
 
Але цей пост не про коректність. Тут ми розглянемо продуктивність, яку ми можемо отримати при переході на константні посилання. На перший погляд може здатися, що єдина вигода це відсутність атомарних інкрементів / декрементів лічильника посилань при виклику конструктора копіювання і деструктора. Давайте напишемо трохи коду і подивимося уважніше, що ж відбувається під капотом.
 
 
 
 
Переклад статті: blog.linderdaum.com/2014/07/03/smart-pointers-passed-by-const-reference /
 
Для початку, кілька допоміжних функцій:
 
 
const size_t NUM_CALLS = 10000000;

double GetSeconds()
{
	return ( double )clock() / CLOCKS_PER_SEC;
}

void PrintElapsedTime( double ElapsedTime )
{
	printf( "%f s/Mcalls\n", float( ElapsedTime / double( NUM_CALLS / 10000000 ) )  );
}

 
Інтрузівний лічильник посилань:
 
 
class iIntrusiveCounter
{
public:
	iIntrusiveCounter():FRefCounter(0) {};
	virtual ~iIntrusiveCounter() {}
	void    IncRefCount() { FRefCounter++; }
	void    DecRefCount() { if ( --FRefCounter == 0 ) { delete this; } }
private:
	std::atomic<int> FRefCounter;
};

 
Ad hoc розумний покажчик:
 
 
template <class T> class clPtr
{
public:
	clPtr(): FObject( 0 ) {}
	clPtr( const clPtr& Ptr ): FObject( Ptr.FObject ) { FObject->IncRefCount(); }
	clPtr( T* const Object ): FObject( Object ) { FObject->IncRefCount(); }
	~clPtr() { FObject->DecRefCount(); }
	clPtr& operator = ( const clPtr& Ptr )
	{
		T* Temp = FObject;
		FObject = Ptr.FObject;
		Ptr.FObject->IncRefCount();
		Temp->DecRefCount();
		return *this;
	}
	inline T* operator -> () const { return FObject; }
private:
	T*    FObject;
};

 
Поки все досить просто, так?
Оголосимо простий клас, екземпляр якого ми передаватиме в функцію спочатку за значенням, а потім по константної посиланням:
 
 
class clTestObject: public iIntrusiveCounter
{
public:
	clTestObject():FPayload(32167) {}
	// сделаем что-нибудь полезное
	void Do()
	{
		FPayload++;
	}

private:
	int FPayload;
};

 
Тепер можна написати безпосередньо код бенчмарка:
 
 
void ProcessByValue( clPtr<clTestObject> O ) { O->Do(); }
void ProcessByConstRef( const clPtr<clTestObject>& O ) { O->Do(); }

int main()
{
	clPtr<clTestObject> Obj = new clTestObject;
	for ( size_t j = 0; j != 3; j++ )
	{
		double StartTime = GetSeconds();
		for ( size_t i = 0; i != NUM_CALLS; i++ ) { ProcessByValue( Obj ); }
		PrintElapsedTime( GetSeconds() - StartTime );
	}
	for ( size_t j = 0; j != 3; j++ )
	{
		double StartTime = GetSeconds();
		for ( size_t i = 0; i != NUM_CALLS; i++ ) { ProcessByConstRef( Obj ); }
		PrintElapsedTime( GetSeconds() - StartTime );
	}
	return 0;
}

 
Зберемо і подивимося, що відбувається. Спочатку зберемо неоптимізованими версію (я використовую gcc.EXE (GCC) 4.10.0 20140420 (experimental) ):
 
 
gcc -O0 main.cpp -lstdc++ -std=c++11

 
Швидкість роботи 0.375 с / Мвизовов для версії «по-значенням» проти 0.124 с / Mвизовов для версії «по-константної-засланні». Переконлива різниця в 3x на отладочной збірці. Це добре. Давайте подивимося на асемблерний лістинг. Версія «по-значенням»:
 
 
L25:
	leal	-60(%ebp), %eax
	leal	-64(%ebp), %edx
	movl	%edx, (%esp)
	movl	%eax, %ecx
	call	__ZN5clPtrI12clTestObjectEC1ERKS1_		// вызываем конструктор копирования
	subl	$4, %esp
	leal	-60(%ebp), %eax
	movl	%eax, (%esp)
	call	__Z14ProcessByValue5clPtrI12clTestObjectE
	leal	-60(%ebp), %eax
	movl	%eax, %ecx
	call	__ZN5clPtrI12clTestObjectED1Ev			// вызываем деструктор
	addl	$1, -32(%ebp)
L24:
	cmpl	$10000000, -32(%ebp)
	jne	L25

 
Версія «по-константної-засланні». Зверніть увагу на скільки все стало чистіше навіть у отладочном білді:
 
 
L29:
	leal	-64(%ebp), %eax
	movl	%eax, (%esp)
	call	__Z17ProcessByConstRefRK5clPtrI12clTestObjectE	// просто один вызов
	addl	$1, -40(%ebp)
L28:
	cmpl	$10000000, -40(%ebp)
	jne	L29

 
Всі дзвінки на своїх місцях і все що вдалося заощадити це дві досить-таки дорогі атомарні операції. Але налагоджувальні збірки це не те, що нам потрібно, але ж? Давайте все оптимізуємо і подивимося, що станеться:
 
 
gcc -O3 main.cpp -lstdc++ -std=c++11

 
Версія «по-значенням» тепер виконується за 0.168 секунди на 1 млн. викликів. Час виполнянія версії «по-константної-засланні» обустілось в буквальному посиланням до нуля. Це не помилка. Не важливо скільки ітерацій ми зробимо, час виконання цього простого тесту буде нульовим. Давайте подивимося на асемблер, щоб переконатися, чи не помилилися ми де-небудь. Ось оптимізована версія передачі за значенням:
 
 
L25:
	call	_clock
	movl	%eax, 36(%esp)
	fildl	36(%esp)
	movl	$10000000, 36(%esp)
	fdivs	LC0
	fstpl	24(%esp)
	.p2align 4,,10
L24:
	movl	32(%esp), %eax
	lock addl	$1, (%eax)		// заинлайненный IncRefCount()...
	movl	40(%esp), %ecx
	addl	$1, 8(%ecx)			// ProcessByValue() и Do() скомпилированы в 2 строки
	lock subl	$1, (%eax)		// а это DecRefCount(). Впечатляет.
	jne	L23
	movl	(%ecx), %eax
	call	*4(%eax)
L23:
	subl	$1, 36(%esp)
	jne	L24
	call	_clock

 
Добре, але що ще можна зробити при передачі за посиланням, що вона стане працювати на стільки швидко, що ми не можемо це виміряти? Ось вона:
 
 
call	_clock
	movl	%eax, 36(%esp)
	movl	40(%esp), %eax
	addl	$10000000, 8(%eax)		// предвычесленный окончательный результат, никаких циклов, ничего
	call	_clock
	movl	%eax, 32(%esp)
	movl	$20, 4(%esp)
	fildl	32(%esp)
	movl	$LC2, (%esp)
	movl	$1, 48(%esp)
	flds	LC0
	fdivr	%st, %st(1)
	fildl	36(%esp)
	fdivp	%st, %st(1)
	fsubrp	%st, %st(1)
	fstpl	8(%esp)
	call	_printf

 
Ось це так! У цей лістинг помістився весь бенчмарк. Відсутність атомарних операцій дозволило оптимізаторові залізти в цей код і розгорнути цикл в одне перевирахованой значення. Звичайно, цей приклад тривіальний. Однак, він дозволяє чітко говорити про 2-х вигодах передачі розумних указетелей по константної посиланням, які роблять її не передчасні оптимізацією, а серйозним засобом поліпшення продуктивність:
 
1) видалення атомарних операцій дає велику вигоду саме по собі
2) видалення атомарних операцій дозволяє оптимізаторові причесати код
 
Повний ісходник тут .
 
На вашому компіляторі результат може відрізнятися :)
 
P.S. У Герба Саттера є вельми докладний есе на цю тему, яке в найдрібніших подробицях зачіпає мовну сторону передачі розумних покажчиків за посиланням в С + +.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.