Улучшенный макрос LOG () с использованием шаблонного метапрограммирования

Типичное решение для ведения журнала на основе макросов LOG () может выглядеть примерно так:

#define LOG(msg) \
std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl

Это позволяет программистам создавать сообщения с богатыми данными, используя удобные и безопасные по типу потоковые операторы:

string file = "blah.txt";
int error = 123;
...
LOG("Read failed: " << file << " (" << error << ")");

// Outputs:
// test.cpp(5): Read failed: blah.txt (123)

Проблема в том, что это заставляет компилятор встроить несколько операторов ostream ::<< звонки. Это увеличивает сгенерированный код и, следовательно, размер функции, что, как я подозреваю, может ухудшить производительность кэша команд и помешать компилятору оптимизировать код.

Вот «простая» альтернатива, которая заменяет встроенный код вызовом шаблонная функция:

********* РЕШЕНИЕ № 2: ВАРИАДИЧЕСКАЯ ФУНКЦИЯ ШАБЛОНА *********

#define LOG(...) LogWrapper(__FILE__, __LINE__, __VA_ARGS__)

// Log_Recursive wrapper that creates the ostringstream
template<typename... Args>
void LogWrapper(const char* file, int line, const Args&... args)
{
std::ostringstream msg;
Log_Recursive(file, line, msg, args...);
}

// "Recursive" variadic function
template<typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostringstream& msg,
T value, const Args&... args)
{
msg << value;
Log_Recursive(file, line, msg, args...);
}

// Terminator
void Log_Recursive(const char* file, int line, std::ostringstream& msg)
{
std::cout << file << "(" << line << "): " << msg.str() << std::endl;
}

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

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

LOG("Read failed: ", file, " (", error, ")");

********* РЕШЕНИЕ № 3: ШАБЛОНЫ ВЫРАЖЕНИЯ *********

По предложению @ DyP я создал альтернативное решение, которое использует шаблоны выражений:

#define LOG(msg) Log(__FILE__, __LINE__, Part<bool, bool>() << msg)

template<typename T> struct PartTrait { typedef T Type; };

// Workaround GCC 4.7.2 not recognizing noinline attribute
#ifndef NOINLINE_ATTRIBUTE
#ifdef __ICC
#define NOINLINE_ATTRIBUTE __attribute__(( noinline ))
#else
#define NOINLINE_ATTRIBUTE
#endif // __ICC
#endif // NOINLINE_ATTRIBUTE

// Mark as noinline since we want to minimize the log-related instructions
// at the call sites
template<typename T>
void Log(const char* file, int line, const T& msg) NOINLINE_ATTRIBUTE
{
std::cout << file << ":" << line << ": " << msg << std::endl;
}

template<typename TValue, typename TPreviousPart>
struct Part : public PartTrait<Part<TValue, TPreviousPart>>
{
Part()
: value(nullptr), prev(nullptr)
{ }

Part(const Part<TValue, TPreviousPart>&) = default;
Part<TValue, TPreviousPart> operator=(
const Part<TValue, TPreviousPart>&) = delete;

Part(const TValue& v, const TPreviousPart& p)
: value(&v), prev(&p)
{ }

std::ostream& output(std::ostream& os) const
{
if (prev)
os << *prev;
if (value)
os << *value;
return os;
}

const TValue* value;
const TPreviousPart* prev;
};

// Specialization for stream manipulators (eg endl)

typedef std::ostream& (*PfnManipulator)(std::ostream&);

template<typename TPreviousPart>
struct Part<PfnManipulator, TPreviousPart>
: public PartTrait<Part<PfnManipulator, TPreviousPart>>
{
Part()
: pfn(nullptr), prev(nullptr)
{ }

Part(const Part<PfnManipulator, TPreviousPart>& that) = default;
Part<PfnManipulator, TPreviousPart> operator=(const Part<PfnManipulator,
TPreviousPart>&) = delete;

Part(PfnManipulator pfn_, const TPreviousPart& p)
: pfn(pfn_), prev(&p)
{ }

std::ostream& output(std::ostream& os) const
{
if (prev)
os << *prev;
if (pfn)
pfn(os);
return os;
}

PfnManipulator pfn;
const TPreviousPart* prev;
};

template<typename TPreviousPart, typename T>
typename std::enable_if<
std::is_base_of<PartTrait<TPreviousPart>, TPreviousPart>::value,
Part<T, TPreviousPart> >::type
operator<<(const TPreviousPart& prev, const T& value)
{
return Part<T, TPreviousPart>(value, prev);
}

template<typename TPreviousPart>
typename std::enable_if<
std::is_base_of<PartTrait<TPreviousPart>, TPreviousPart>::value,
Part<PfnManipulator, TPreviousPart> >::type
operator<<(const TPreviousPart& prev, PfnManipulator value)
{
return Part<PfnManipulator, TPreviousPart>(value, prev);
}

template<typename TPart>
typename std::enable_if<
std::is_base_of<PartTrait<TPart>, TPart>::value,
std::ostream&>::type
operator<<(std::ostream& os, const TPart& part)
{
return part.output(os);
}

Решение для шаблонов выражений позволяет программисту использовать знакомые удобные и безопасные по типу потоковые операторы:

LOG("Read failed: " << file << " " << error);

Однако когда Part<A, B> создание не указано без оператора<< звонки сделаны, давая нам преимущество обоих миров: удобные и безопасные операторы потоковой передачи + меньше инструкций. ICC13 с -O3 создает следующий код сборки для вышеуказанного:

movl      $.L_2__STRING.3, %edi
movl      $13, %esi
xorl      %eax, %eax
lea       72(%rsp), %rdx
lea       8(%rsp), %rcx
movq      %rax, 16(%rsp)
lea       88(%rsp), %r8
movq      $.L_2__STRING.4, 24(%rsp)
lea       24(%rsp), %r9
movq      %rcx, 32(%rsp)
lea       40(%rsp), %r10
movq      %r8, 40(%rsp)
lea       56(%rsp), %r11
movq      %r9, 48(%rsp)
movq      $.L_2__STRING.5, 56(%rsp)
movq      %r10, 64(%rsp)
movq      $nErrorCode.9291.0.16, 72(%rsp)
movq      %r11, 80(%rsp)
call      _Z3LogI4PartIiS0_IA2_cS0_ISsS0_IA14_cS0_IbbEEEEEENSt9enable_ifIXsr3std10is_base_ofI9PartTraitIT_ESA_EE5valueEvE4typeEPKciRKSA_

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

********* РЕШЕНИЕ № 4: ШАБЛОНЫ ВЫРАЖЕНИЯ КАТО *********

Вот отличное решение Cato с твиком для поддержки потоковых манипуляторов (например, endl):

#define LOG(msg) (Log(__FILE__, __LINE__, LogData<None>() << msg))

// Workaround GCC 4.7.2 not recognizing noinline attribute
#ifndef NOINLINE_ATTRIBUTE
#ifdef __ICC
#define NOINLINE_ATTRIBUTE __attribute__(( noinline ))
#else
#define NOINLINE_ATTRIBUTE
#endif // __ICC
#endif // NOINLINE_ATTRIBUTE

template<typename List>
void Log(const char* file, int line,
LogData<List>&& data) NOINLINE_ATTRIBUTE
{
std::cout << file << ":" << line << ": ";
output(std::cout, std::move(data.list));
std::cout << std::endl;
}

struct None { };

template<typename List>
struct LogData {
List list;
};

template<typename Begin, typename Value>
constexpr LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin,
Value&& value) noexcept
{
return {{ std::forward<Begin>(begin.list), std::forward<Value>(value) }};
}

template<typename Begin, size_t n>
constexpr LogData<std::pair<Begin&&, const char*>> operator<<(LogData<Begin>&& begin,
const char (&value)[n]) noexcept
{
return {{ std::forward<Begin>(begin.list), value }};
}

typedef std::ostream& (*PfnManipulator)(std::ostream&);

template<typename Begin>
constexpr LogData<std::pair<Begin&&, PfnManipulator>> operator<<(LogData<Begin>&& begin,
PfnManipulator value) noexcept
{
return {{ std::forward<Begin>(begin.list), value }};
}

template <typename Begin, typename Last>
void output(std::ostream& os, std::pair<Begin, Last>&& data)
{
output(os, std::move(data.first));
os << data.second;
}

inline void output(std::ostream& os, None)
{ }

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

movb  $0, (%rsp)
movl  $.L_2__STRING.4, %ecx
movl  $.L_2__STRING.3, %edi
movl  $20, %esi
lea   212(%rsp), %r9
call  void Log<pair<pair<pair<pair<None, char const*>, string const&>, char const*>, int const&> >(char const*, int, LogData<pair<pair<pair<pair<None, char const*>, string const&>, char const*>, int const&> > const&)

Пожалуйста, дайте мне знать, если вы можете придумать способ улучшить производительность или удобство использования этого решения.

26

Решение

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

struct None { };

template <typename First,typename Second>
struct Pair {
First first;
Second second;
};

template <typename List>
struct LogData {
List list;
};

template <typename Begin,typename Value>
LogData<Pair<Begin,const Value &>>
operator<<(LogData<Begin> begin,const Value &value)
{
return {{begin.list,value}};
}

template <typename Begin,size_t n>
LogData<Pair<Begin,const char *>>
operator<<(LogData<Begin> begin,const char (&value)[n])
{
return {{begin.list,value}};
}

inline void printList(std::ostream &os,None)
{
}template <typename Begin,typename Last>
void printList(std::ostream &os,const Pair<Begin,Last> &data)
{
printList(os,data.first);
os << data.second;
}

template <typename List>
void log(const char *file,int line,const LogData<List> &data)
{
std::cout << file << " (" << line << "): ";
printList(std::cout,data.list);
std::cout << "\n";
}

#define LOG(x) (log(__FILE__,__LINE__,LogData<None>() << x))

В G ++ 4.7.2 с оптимизацией -O2 это создает очень компактную последовательность команд, эквивалентную заполнению структуры параметрами с использованием char * для строковых литералов.

15

Другие решения

Я прошел через то же самое. И в итоге я выбрал то же решение, которое вы обрисовали в общих чертах, для которого просто требуется, чтобы клиентский API использовал запятую вместо оператора вставки. Это делает вещи довольно простыми и работает достаточно хорошо. Настоятельно рекомендуется.

0

По вопросам рекламы [email protected]