From c8f610a69da407b88f49ba45ddc5e79bf4877e64 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Sat, 5 Sep 2026 09:08:56 +0200 Subject: [PATCH] Optimize format.h (#1588) Dispatch only the argument types which are actually stated. For everything else, don't generate the code anymore. Detemplatize parse_format_spec(). Collapse long double and double to double on targets where they are same size. This helps prevent duplicate code generation. --- include/etl/format.h | 264 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 246 insertions(+), 18 deletions(-) diff --git a/include/etl/format.h b/include/etl/format.h index 830e64a6..288454fd 100644 --- a/include/etl/format.h +++ b/include/etl/format.h @@ -652,6 +652,197 @@ namespace etl struct is_formattable : etl::bool_constant::value && has_formatter_format::value> { }; + + //************************************************************************* + // Compile-time argument mask. + // + // basic_format_arg stores its value in a variant with a fixed set of + // alternatives, and etl::visit dispatches over every one of them. That + // would instantiate etl::formatter for all alternatives - including the + // whole floating point formatting chain - even for a call that only ever + // passes an int. + // + // The mask records which alternatives can actually occur for a given pack + // of argument types. format_visitor turns the unreachable alternatives into + // empty functions, so their formatters are never instantiated and the + // linker never sees the code. + // + // A single table, arg_type_mask, drives both halves: it builds the mask + // from the argument pack, and it tells the visitor which bit an alternative + // corresponds to. + // + // Soundness: the mask is only ever narrowed for argument types that are + // recognised explicitly below. Anything else - including user-defined types + // and pointers other than void* - falls back to mask_all, which reproduces + // the unmasked behaviour exactly. + //************************************************************************* + typedef unsigned int arg_mask_t; + + static ETL_CONSTANT arg_mask_t mask_monostate = 1U << 0; + static ETL_CONSTANT arg_mask_t mask_bool = 1U << 1; + static ETL_CONSTANT arg_mask_t mask_char = 1U << 2; + static ETL_CONSTANT arg_mask_t mask_int = 1U << 3; + static ETL_CONSTANT arg_mask_t mask_uint = 1U << 4; + static ETL_CONSTANT arg_mask_t mask_llong = 1U << 5; + static ETL_CONSTANT arg_mask_t mask_ullong = 1U << 6; + static ETL_CONSTANT arg_mask_t mask_float = 1U << 7; + static ETL_CONSTANT arg_mask_t mask_double = 1U << 8; + static ETL_CONSTANT arg_mask_t mask_ldouble = 1U << 9; + static ETL_CONSTANT arg_mask_t mask_cstring = 1U << 10; + static ETL_CONSTANT arg_mask_t mask_string_view = 1U << 11; + static ETL_CONSTANT arg_mask_t mask_voidp = 1U << 12; + static ETL_CONSTANT arg_mask_t mask_all = ~static_cast(0); + + // Maps a type onto the set of alternatives a value of that type can be + // stored as. Mirrors the converting constructors of basic_format_arg. + // + // This serves both directions of the problem. For a user argument type it + // gives the alternatives that argument may end up in, which is how the + // per-call mask is built. For a variant alternative it degenerates to that + // alternative's own bit, which is how format_visitor tests reachability. + // + // The primary template is deliberately conservative: an unrecognised type + // yields mask_all, so it is never wrongly excluded. + template + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_all; + }; + + // Not constructible as a user argument; listed so that the table covers + // every variant alternative. Every mask contains this bit regardless, as + // the args_mask recursion bottoms out at mask_monostate. + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_monostate; + }; + + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_bool; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_char; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_int; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_uint; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_int; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_uint; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_int; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_uint; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_llong; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_ullong; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_llong; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_ullong; + }; + #if ETL_USING_FORMAT_FLOATING_POINT + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_float; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_double; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_ldouble; + }; + #endif + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_cstring; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_cstring; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_string_view; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_voidp; + }; + template <> + struct arg_type_mask + { + static ETL_CONSTANT arg_mask_t value = mask_voidp; + }; + + // etl::string, etl::istring and friends are stored as a string_view. + template + struct arg_type_mask, T>::value>> + { + static ETL_CONSTANT arg_mask_t value = mask_string_view; + }; + + // The empty alternative is always reachable, so monostate is always set. + template + struct args_mask; + + template <> + struct args_mask<> + { + static ETL_CONSTANT arg_mask_t value = mask_monostate; + }; + + template + struct args_mask + { + static ETL_CONSTANT arg_mask_t value = arg_type_mask>::value | args_mask::value; + }; } // namespace private_format template @@ -1206,11 +1397,11 @@ namespace etl return false; } - template - void parse_format_spec(format_parse_context& parse_ctx, format_context& fmt_context) + // Not templated on the output iterator: the parse phase only reads the + // parse context and writes the format spec, so a single instantiation is + // shared by every output iterator type. + inline void parse_format_spec(format_parse_context& parse_ctx, format_spec_t& format_spec) { - auto& format_spec = fmt_context.format_spec; - format_spec = format_spec_t(); // reset format_spec to defaults format_spec.index = parse_num(parse_ctx); // optional explicit index @@ -1536,6 +1727,19 @@ namespace etl } #if ETL_USING_FORMAT_FLOATING_POINT + //*********************************** + // On many targets (e.g. ARM EABI, MSVC) 'long double' has exactly the same + // representation as 'double'. There, formatting a long double through the + // 'double' instantiation is value-preserving, and avoids emitting a second, + // byte-identical copy of the whole floating point formatting chain. + // On targets where the two differ (e.g. x86 80-bit) 'long double' is kept. + typedef etl::conditional_t<(sizeof(long double) == sizeof(double)) + && (etl::numeric_limits::digits == etl::numeric_limits::digits) + && (etl::numeric_limits::max_exponent == etl::numeric_limits::max_exponent) + && (etl::numeric_limits::min_exponent == etl::numeric_limits::min_exponent), + double, long double> + long_double_format_type; + #if ETL_NOT_USING_FORMAT_LONG_DOUBLE_MATH //*********************************** // Math function wrappers to handle toolchains that don't provide @@ -2004,7 +2208,7 @@ namespace etl } #endif - template + template struct format_visitor { using output_iterator = OutputIt; @@ -2019,11 +2223,12 @@ namespace etl template void operator()(T value) { - formatter f; - format_parse_context::iterator it = f.parse(parse_ctx); - parse_ctx.advance_to(it); - OutputIt fit = f.format(value, fmt_ctx); - fmt_ctx.advance_to(fit); + // T is a variant alternative here, so arg_type_mask is that + // alternative's own bit. + // Tag dispatch rather than 'if constexpr' so that the formatter for an + // unreachable alternative is not instantiated in C++11/14 either. + typedef etl::conditional_t<(arg_type_mask::value & Mask) != 0, etl::true_type, etl::false_type> reachable; + dispatch(value, reachable()); } // for user-defined types routed through basic_format_arg::handle @@ -2034,6 +2239,25 @@ namespace etl format_parse_context& parse_ctx; format_context& fmt_ctx; + + private: + + template + void dispatch(T value, etl::true_type) + { + formatter f; + format_parse_context::iterator it = f.parse(parse_ctx); + parse_ctx.advance_to(it); + OutputIt fit = f.format(value, fmt_ctx); + fmt_ctx.advance_to(fit); + } + + // This alternative cannot be reached for the argument types of this call, + // so no formatter is instantiated for it. + template + void dispatch(T, etl::false_type) + { + } }; template @@ -2645,7 +2869,8 @@ namespace etl template typename format_context::iterator format(long double arg, format_context& fmt_ctx) { - return private_format::format_aligned_floating(arg, fmt_ctx); + typedef private_format::long_double_format_type type; + return private_format::format_aligned_floating(static_cast(arg), fmt_ctx); } }; #endif @@ -2715,12 +2940,15 @@ namespace etl } }; - template + // Mask is a compile-time set of the basic_format_arg alternatives that the + // caller can actually supply. It only removes unreachable code paths; the + // default of mask_all formats every alternative, as before. + template OutputIt vformat_to(OutputIt out, etl::string_view fmt, format_args args) { - format_parse_context parse_context(fmt, args.size()); - format_context fmt_context(out, args); - private_format::format_visitor v(parse_context, fmt_context); + format_parse_context parse_context(fmt, args.size()); + format_context fmt_context(out, args); + private_format::format_visitor v(parse_context, fmt_context); while (parse_context.begin() != parse_context.end()) { @@ -2736,7 +2964,7 @@ namespace etl } else { - private_format::parse_format_spec(parse_context, fmt_context); + private_format::parse_format_spec(parse_context, fmt_context.format_spec); // Resolve nested replacement fields for width/precision private_format::resolve_nested_replacements(fmt_context.format_spec, args); @@ -2774,14 +3002,14 @@ namespace etl OutputIt format_to(OutputIt out, format_string fmt, Args&&... args) { auto the_args{make_format_args(args...)}; - return vformat_to(etl::move(out), fmt.get(), format_args(the_args)); + return vformat_to::value>(etl::move(out), fmt.get(), format_args(the_args)); } template , class... Args> OutputIt format_to_n(OutputIt out, size_t n, format_string fmt, Args&&... args) { auto the_args{make_format_args(args...)}; - return vformat_to(WrapperIt(out, n), fmt.get(), format_args(the_args)).get(); + return vformat_to::value>(WrapperIt(out, n), fmt.get(), format_args(the_args)).get(); } // non std in the following, specific to etl