AK: Add fast path for formatting common types

This commit changes `AK::TypeErasedParameter` to store integer,
floating-point and string types as well as characters and booleans
directly instead of through a type-erased `void const*`.

This is advantageous for binary size:
- The compiler no longer needs to push both the parameter value and a
  pointer to it to the stack (which contains the argument array);
  storing just the value is enough.
- Instead of instantiating `__format_value` for these types and taking
  its address (which generates a GOT entry and an ADRP+LDR pair in
  assembly), we just store a constant `TypeErasedParameter::Type` value.
- The biggest saving comes from the fact that we used to instantiate a
  distinct `__format_value` for each length of string literal. For
  LibJS, this meant 69 different functions! We can now just store them
  as a `char const*`.

I opted to use a manual tagged union instead of `Variant`: the code
wouldn't be much shorter if we used a `Variant` since we'd have to
handle converting the standard integer types to `u64`/`i64` and custom
types to the type erased wrapper via explicit constructors anyway. And
compile time overhead is smaller this way.

This gives us some nice binary size savings (numbers are from arm64
macOS LibJS):

     FILE SIZE        VM SIZE
  --------------  --------------
    +52% +10.3Ki   +52% +10.3Ki    [__TEXT]
   +5.2%    +768  +5.2%    +768    [__DATA_CONST]
   -0.0%      -7  -0.0%      -7    __TEXT,__cstring
   -3.0%    -144  -3.0%    -144    __TEXT,__stubs
   -1.2%    -176  -1.2%    -176    Function Start Addresses
  -11.6%    -432 -11.6%    -432    Indirect Symbol Table
   -1.0%    -448  -1.0%    -448    Code Signature
  -18.1%    -768 -18.1%    -768    __DATA_CONST,__got
   -0.8% -6.83Ki  -0.8% -6.83Ki    Symbol Table
   -1.0% -11.2Ki  -1.0% -11.2Ki    String Table
   -0.9% -26.1Ki  -0.9% -26.1Ki    __TEXT,__text
   -7.2% -20.9Ki  -9.6% -28.9Ki    [__LINKEDIT]
   -1.0% -56.0Ki  -1.1% -64.0Ki    TOTAL
This commit is contained in:
Daniel Bertalan 2025-05-24 09:06:42 +02:00 committed by Andrew Kaster
commit 04fac0031c
Notes: github-actions[bot] 2025-05-26 19:03:37 +00:00
2 changed files with 125 additions and 75 deletions

View file

@ -106,7 +106,13 @@ ErrorOr<void> vformat_impl(TypeErasedFormatParams& params, FormatBuilder& builde
auto& parameter = params.parameters().at(specifier.index); auto& parameter = params.parameters().at(specifier.index);
FormatParser argparser { specifier.flags }; FormatParser argparser { specifier.flags };
TRY(parameter.formatter(params, builder, argparser, parameter.value)); TRY(parameter.visit([&]<typename T>(T const& value) {
if constexpr (IsSame<T, TypeErasedParameter::CustomType>) {
return value.formatter(params, builder, argparser, value.value);
} else {
return __format_value<T>(params, builder, argparser, &value);
}
}));
TRY(vformat_impl(params, builder, parser)); TRY(vformat_impl(params, builder, parser));
return {}; return {};
} }

View file

@ -53,95 +53,148 @@ concept Formattable = HasFormatter<T>;
constexpr size_t max_format_arguments = 256; constexpr size_t max_format_arguments = 256;
template<typename T>
ErrorOr<void> __format_value(TypeErasedFormatParams& params, FormatBuilder& builder, FormatParser& parser, void const* value)
{
Formatter<T> formatter;
formatter.parse(params, parser);
return formatter.format(builder, *static_cast<T const*>(value));
}
struct TypeErasedParameter { struct TypeErasedParameter {
enum class Type { enum class Type {
UInt8, UnsignedInteger,
UInt16, SignedInteger,
UInt32, Boolean,
UInt64, Character,
Int8, Float,
Int16, Double,
Int32, StringView,
Int64, CString,
Custom CustomType
}; };
template<size_t size, bool is_unsigned> struct CustomType {
static consteval Type get_type_from_size() void const* value;
{ ErrorOr<void> (*formatter)(TypeErasedFormatParams&, FormatBuilder&, FormatParser&, void const* value);
if constexpr (is_unsigned) { };
if constexpr (size == 1)
return Type::UInt8;
if constexpr (size == 2)
return Type::UInt16;
if constexpr (size == 4)
return Type::UInt32;
if constexpr (size == 8)
return Type::UInt64;
} else {
if constexpr (size == 1)
return Type::Int8;
if constexpr (size == 2)
return Type::Int16;
if constexpr (size == 4)
return Type::Int32;
if constexpr (size == 8)
return Type::Int64;
}
VERIFY_NOT_REACHED(); template<typename T>
static bool const IsChar = IsOneOf<T, char, wchar_t, char8_t, char16_t, char32_t>;
template<Unsigned U>
explicit constexpr TypeErasedParameter(U const& value)
requires(!IsChar<U> && sizeof(U) <= sizeof(u64))
: value { .as_unsigned = value }
, type { Type::UnsignedInteger }
{
}
template<Signed I>
explicit constexpr TypeErasedParameter(I const& value)
requires(!IsChar<I> && sizeof(I) <= sizeof(i64))
: value { .as_signed = value }
, type { Type::SignedInteger }
{
}
explicit constexpr TypeErasedParameter(bool const& value)
: value { .as_bool = value }
, type { Type::Boolean }
{
}
explicit constexpr TypeErasedParameter(char const& value)
: value { .as_char = value }
, type { Type::Character }
{
}
explicit constexpr TypeErasedParameter(float const& value)
: value { .as_float = value }
, type { Type::Float }
{
}
explicit constexpr TypeErasedParameter(double const& value)
: value { .as_double = value }
, type { Type::Double }
{
}
explicit constexpr TypeErasedParameter(StringView const& value)
: value { .as_string_view = value }
, type { Type::StringView }
{
}
explicit constexpr TypeErasedParameter(char const* value)
: value { .as_c_string = value }
, type { Type::CString }
{
} }
template<typename T> template<typename T>
static consteval Type get_type() explicit constexpr TypeErasedParameter(T const& value)
: value { .as_custom_type = { &value, __format_value<T> } }
, type { Type::CustomType }
{ {
if constexpr (IsIntegral<T>)
return get_type_from_size<sizeof(T), IsUnsigned<T>>();
else
return Type::Custom;
} }
template<typename Visitor> template<typename Visitor>
constexpr auto visit(Visitor&& visitor) const constexpr auto visit(Visitor&& visitor) const
{ {
switch (type) { switch (type) {
case TypeErasedParameter::Type::UInt8: case Type::UnsignedInteger:
return visitor(*static_cast<u8 const*>(value)); return visitor(value.as_unsigned);
case TypeErasedParameter::Type::UInt16: case Type::SignedInteger:
return visitor(*static_cast<u16 const*>(value)); return visitor(value.as_signed);
case TypeErasedParameter::Type::UInt32: case Type::Boolean:
return visitor(*static_cast<u32 const*>(value)); return visitor(value.as_bool);
case TypeErasedParameter::Type::UInt64: case Type::Character:
return visitor(*static_cast<u64 const*>(value)); return visitor(value.as_char);
case TypeErasedParameter::Type::Int8: case Type::Float:
return visitor(*static_cast<i8 const*>(value)); return visitor(value.as_float);
case TypeErasedParameter::Type::Int16: case Type::Double:
return visitor(*static_cast<i16 const*>(value)); return visitor(value.as_double);
case TypeErasedParameter::Type::Int32: case Type::StringView:
return visitor(*static_cast<i32 const*>(value)); return visitor(value.as_string_view);
case TypeErasedParameter::Type::Int64: case Type::CString:
return visitor(*static_cast<i64 const*>(value)); return visitor(value.as_c_string);
default: case Type::CustomType:
TODO(); return visitor(value.as_custom_type);
} }
VERIFY_NOT_REACHED();
} }
constexpr size_t to_size() const constexpr size_t to_size() const
{ {
return visit([]<typename T>(T value) { return visit([]<typename T>(T value) -> size_t {
if constexpr (sizeof(T) > sizeof(size_t)) if constexpr (IsSame<T, u64>)
VERIFY(value < NumericLimits<size_t>::max()); return static_cast<size_t>(value);
if constexpr (IsSigned<T>)
if constexpr (IsSame<T, i64>) {
VERIFY(value >= 0); VERIFY(value >= 0);
return static_cast<size_t>(value); return static_cast<size_t>(value);
}
TODO();
}); });
} }
// FIXME: Getters and setters. union {
u64 as_unsigned;
void const* value; i64 as_signed;
bool as_bool;
char as_char;
float as_float;
double as_double;
StringView as_string_view;
char const* as_c_string;
CustomType as_custom_type;
} value;
Type type; Type type;
ErrorOr<void> (*formatter)(TypeErasedFormatParams&, FormatBuilder&, FormatParser&, void const* value);
}; };
class FormatBuilder { class FormatBuilder {
@ -293,15 +346,6 @@ private:
TypeErasedParameter m_parameters[0]; TypeErasedParameter m_parameters[0];
}; };
template<typename T>
ErrorOr<void> __format_value(TypeErasedFormatParams& params, FormatBuilder& builder, FormatParser& parser, void const* value)
{
Formatter<T> formatter;
formatter.parse(params, parser);
return formatter.format(builder, *static_cast<T const*>(value));
}
template<AllowDebugOnlyFormatters allow_debug_formatters, typename... Parameters> template<AllowDebugOnlyFormatters allow_debug_formatters, typename... Parameters>
class VariadicFormatParams : public TypeErasedFormatParams { class VariadicFormatParams : public TypeErasedFormatParams {
public: public:
@ -309,7 +353,7 @@ public:
explicit VariadicFormatParams(Parameters const&... parameters) explicit VariadicFormatParams(Parameters const&... parameters)
: TypeErasedFormatParams(sizeof...(Parameters)) : TypeErasedFormatParams(sizeof...(Parameters))
, m_parameter_storage { TypeErasedParameter { &parameters, TypeErasedParameter::get_type<Parameters>(), __format_value<Parameters> }... } , m_parameter_storage { TypeErasedParameter { parameters }... }
{ {
constexpr bool any_debug_formatters = (is_debug_only_formatter<Formatter<Parameters>>() || ...); constexpr bool any_debug_formatters = (is_debug_only_formatter<Formatter<Parameters>>() || ...);
static_assert(!any_debug_formatters || allow_debug_formatters == AllowDebugOnlyFormatters::Yes, static_assert(!any_debug_formatters || allow_debug_formatters == AllowDebugOnlyFormatters::Yes,