fmtlib / fmt

A modern formatting library
https://fmt.dev
Other
20.78k stars 2.5k forks source link

Difference in performance between the default and explicit presentation specifiers in format string compilation #4108

Open ZENOTME opened 3 months ago

ZENOTME commented 3 months ago

Format Specification can used to constrain the type safety but I find that when format using Format Specification, there is a huge decline in performance. Is there way to avoid this.

test code usin google bench

constexpr const char *log_fmt =
   "arg1:{} arg2:{} arg3:{} arg4:{} arg5:{} arg6:{} arg7:{} arg8:{} arg9:{} arg10:{} "
        "arg11:{}(ms) arg12:{} arg13:{} arg14:{} arg15:{} arg16:{} arg17:{} arg18:{}";
constexpr const char *log_const_fmt =
    "arg1:{:p} arg2:{:d} arg3:{:p} arg4:{:s} arg5:{:s} arg6:{:d} arg7:{:s} arg8:{:d} arg9:{:s} arg10:{:d} "
    "arg11:{:g}(ms) arg12:{:s} arg13:{:d} arg14:{:s} arg15:{:s} arg16:{:d} arg17:{:d} arg18:{:s}";
char prefix[] = "arg1:0x12345678 arg2:12345 arg3:0x87654321 arg4:example_user ";
void *arg1 = (void *)0x12345678;    
unsigned long arg2 = 12345;         
void *arg3 = (void *)0x87654321;  
const char *arg4 = "example_user"; 
int arg4_1 = 12;                 
const char *arg5 = "example_C";   
int arg5_1 = 9;                   
int arg6 = 100;                   
const char *arg7 = "example_G";   
int arg7_1 = 9;                   
int arg8 = 200;                   
const char *arg9 = "example_S";   
int arg9_1 = 9;                   
int arg10 = 300;                  
float arg11 = 123.123;          
const char *arg12 = "00000";    
int arg13 = 0;                  
const char *arg14 = "No error"; 
const char *arg15 = "query_id"; 
int arg15_1 = 8;                
int arg16 = 42;                 
long arg17 = 9876543210;        
const char *arg18 = "SELECT * FROM table WHERE id = 1"; 
int arg18_1 = 32; 

template<typename... Args>
char* compile_format(Args...args) {
    fmt::basic_memory_buffer<char, 320> buf;
    fmt::format_to(fmt::appender(buf), FMT_COMPILE(log_fmt), args...);
    return buf.data();
}

static void bench_spdlog_compile_format(benchmark::State& state)
{
    for (auto _: state) {
        benchmark::DoNotOptimize(compile_format(arg1, arg2, (int)(long)arg3, arg4, arg5, arg6,
              arg7, arg8, arg9, arg10, arg11, arg12, arg13,
              arg14, arg15, arg16, arg17, arg18));
    }
}
BENCHMARK(bench_spdlog_compile_format);

template<typename... Args>
char* compile_constrain_format(Args...args) {
    fmt::basic_memory_buffer<char, 320> buf;
    fmt::format_to(fmt::appender(buf), FMT_COMPILE(log_const_fmt), args...);
    return buf.data();
}

static void bench_spdlog_constrain_compile_format(benchmark::State& state)
{
    for (auto _: state) {
        benchmark::DoNotOptimize(compile_constrain_format(arg1, arg2, arg3, arg4, arg5, arg6,
              arg7, arg8, arg9, arg10, arg11, arg12, arg13,
              arg14, arg15, arg16, arg17, arg18));
    }
}
BENCHMARK(bench_spdlog_constrain_compile_format);

result

bench_spdlog_compile_format 349 ns 349 ns 1971834 bench_spdlog_constrain_compile_format 2586 ns 2586 ns 270360

vitaut commented 3 months ago

The default format is more optimized in the format string compilation. In general it doesn't make sense to use redundant specifiers but we could handle them in the same way (note that g is not equivalent to the default though). A PR would be welcome.