c++ - Una mejor macro LOG() usando metaprogramación de plantillas
logging c++11 (2)
Aquí hay otra plantilla de expresión que parece ser incluso más eficiente en función de algunas pruebas que he ejecutado. En particular, evita la creación de múltiples funciones para cadenas con diferentes longitudes por el operator<<
especializado operator<<
para usar un miembro char *
en la estructura resultante. También debería ser fácil agregar otras especializaciones de este formulario.
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))
Con G ++ 4.7.2, con la optimización de -O2, esto crea una secuencia de instrucciones muy compacta, equivalente a llenar una estructura con los parámetros usando un char *
para literales de cadena.
Una solución típica de registro basado en macros LOG () puede verse así:
#define LOG(msg) /
std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl
Esto permite a los programadores crear mensajes ricos en datos utilizando operadores de transmisión convenientes y seguros para el tipo:
string file = "blah.txt";
int error = 123;
...
LOG("Read failed: " << file << " (" << error << ")");
// Outputs:
// test.cpp(5): Read failed: blah.txt (123)
El problema es que esto hace que el compilador en línea múltiples llamadas ostream :: operator <<. Esto aumenta el código generado y, por lo tanto, el tamaño de la función, que sospecho que puede afectar el rendimiento de la memoria caché de instrucciones y dificultar la capacidad del compilador para optimizar el código.
Aquí hay una alternativa "simple" que reemplaza el código en línea con una llamada a una función de plantilla variable :
*********
SOLUCIÓN # 2: FUNCIÓN DE PLANTILLA VARIÁDICA *********
#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;
}
El compilador genera automáticamente nuevas instancias de la función de plantilla según sea necesario, dependiendo del número, tipo y orden de los argumentos del mensaje.
El beneficio es que hay menos instrucciones en cada sitio de llamada. El inconveniente es que el usuario debe pasar las partes del mensaje como parámetros de función en lugar de combinarlas utilizando operadores de transmisión:
LOG("Read failed: ", file, " (", error, ")");
*********
SOLUCIÓN # 3: PLANTILLAS DE EXPRESIÓN *********
Por sugerencia de @DyP, creé una solución alternativa que utiliza plantillas de expresión :
#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);
}
La solución de plantillas de expresión le permite al programador usar los operadores de transmisión de video convenientes y seguros para el tipo:
LOG("Read failed: " << file << " " << error);
Sin embargo, cuando la creación de la Part<A, B>
está en línea, no se realizan llamadas del operador <<, lo que nos da el beneficio de ambos mundos: operadores de transmisión de datos convenientes y seguros para el tipo + menos instrucciones. ICC13 con -O3 produce el siguiente código de ensamblaje para lo anterior:
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_
El total es de 19 instrucciones incluyendo una llamada de función. Parece que cada argumento adicional transmitido agrega 3 instrucciones. El compilador crea una instanciación diferente de la función Log () dependiendo del número, tipo y orden de las partes del mensaje, lo que explica el extraño nombre de la función.
*********
SOLUCIÓN # 4: PLANTILLAS DE EXPRESIÓN DE CATO *********
Aquí está la excelente solución de Cato con un ajuste para admitir manipuladores de flujo (por ejemplo, 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)
{ }
Como señala Cato, la ventaja sobre la última solución es que produce menos ejecuciones de funciones, ya que la especialización const char * maneja todos los literales de cadena. También hace que se generen menos instrucciones en el sitio de la llamada:
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&)
Déjeme saber si puede encontrar alguna manera de mejorar el rendimiento o la facilidad de uso de esta solución.
He pasado exactamente por lo mismo. Y terminé con la misma solución que describiste, que solo requiere que la API del cliente use una coma en lugar del operador de inserción. Mantiene las cosas bastante simples y funciona lo suficientemente bien. Muy recomendable.