Petroglif

Заметки по производительности

Анализ производительности возврата функцией структур по значению

Практики современного c++ часто предполагают возврат сложных данных функцией по значению. То есть, если результатом работы функции является какой либо класс или структура, то функция непосредственно возвращает экземпляр этого класса (структуры) на стеке.Для примера, возьмет такой код. Здесь функция ничего важного не делает, просто производит очень малозатратные синтетические действия и потом возвращает результат.


struct Bechmark_ByRefVsByVal_TestStruct {
	const void * Ptr;
	uint32 A;
	uint8  BigChunk[32];
	uint32 B;
};

Bechmark_ByRefVsByVal_TestStruct Bechmark_ByRefVsByVal_Func_ByVal(uint arg) { Bechmark_ByRefVsByVal_TestStruct data; data.Ptr = (arg & 1) ? "abcdef" : "ghijkl"; data.A = (arg << 1); data.B = (arg >> 2); return data; }

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


void Bechmark_ByRefVsByVal_Func_ByRef(uint arg, Bechmark_ByRefVsByVal_TestStruct & rRef)
{
	rRef.Ptr = (arg & 1) ? "abcdef" : "ghijkl";
	rRef.A = (arg << 1);
	rRef.B = (arg >> 2);
}

Предпочтение мое обусловлено моим собственным убеждением, что работа в c/c++ через значения - весьма затратная штука - вместо использования регистров, данные заносятся на стек и с него же забираются вызывающей функцией. Я был прав, но не полностью.Бенчмаркинг обеих функций показывает снижение производительности примерно в 2 раза при возврате сложной структуры по значению. Пара нюансов:

  • Фактически, функция возвращает не значение, а адрес экземпляра структуры на стеке, то есть, копирование со стека в рабочий экземпляр осуществляется только вызывающей функцией.
  • Компилятор достаточно агрессивен в попытках реализовать inline-вариант локальной (static) функции, по-этому для сравнения мне пришлось явно сообщать компилятору, что функция не должны быть inline. В inline-варианте производительность обеих функций одинакова.
  • Если размер возвращаемой структуры находится в пределах десятков байт, то практически не влияет на производительность: копирование вызывающей функцией данных со стека - быстрая процедура не зависящая сильно от размера данных. Если же размер структуры значителен (видимо, надо оценивать по сравнению с размером кэша процессора), то замедление может стать очень значительным. Так, добавив в структуру Bechmark_ByRefVsByVal_TestStruct пустое поле размером 1024 байта, я получил почти 6-кратное снижение скорости работы.
  • Влияние конструкторов/деструкторов

    Я намеренно тестировал функции в применении к структуре без явных конструктора и деструктора. Наличие оных еще сильнее замедляет работу (move-семантика, конечно, сильно нивелирует проблему, но неявные вызовы конструктора/деструктора, пусть и в move-исполнении, все равно усугубляют проблему).

    Реализация теста

    Описанный бенчмаркинг реализован в тестовом суб-проекте Papyrus в виде теста

    
    SLTEST_R(Bechmark_ByRefVsByVal)
    

    OOO "Петроглиф"
    Copyright © 2019