-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compile-time named arguments #2243
Compile-time named arguments #2243
Conversation
54fec9a
to
f720337
Compare
There 2 more points for this PR. They are not implemented in this PR but can be added on request.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR.
Still, I can create a benchmark for this.
That would be interesting to see if you have time.
But that would add more complexity in format string compilation code, and I'm not sure if it's worth it.
I think it's not worth it.
struct is_named_arg<named_arg<Char, T>> : std::true_type {}; | ||
|
||
template <typename Char, typename T, typename... Tail, | ||
FMT_ENABLE_IF(!is_named_arg<T>::value)> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is runtime named arg handling. Why do we need to care about compile-time ones here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because first, we need to provide a new entity for named arguments storing and adapt {fmt} for it. I mean before using this new entity in compile-time API.
This function took named_arg<Char, T>
, which does not work for newly introduced statically_named_arg
.
I always have time for benchmarks 😉
DetailsI used hyperfine for proper time measurement. Source file: it's big, click to see#include "fmt/format.h"
using namespace fmt;
std::string func(int fake) {
switch (fake) {
case 0:
return format("{first}{second}{first}{third}", "first"_a = "abra",
"second"_a = "cad", "third"_a = 99);
case 1:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"first_long_argument_name"_a = 41,
"second_long_argument_name"_a = 42,
"third_long_argument_name"_a = 43);
case 2:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"first_first_first_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 41,
"second_second_second_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 42,
"third_third_third_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 43);
case 3:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"3first_long_argument_name"_a = 41,
"3second_long_argument_name"_a = 42,
"3third_long_argument_name"_a = 43);
case 4:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"4first_long_argument_name"_a = 41,
"4second_long_argument_name"_a = 42,
"4third_long_argument_name"_a = 43);
case 5:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"5first_long_argument_name"_a = 41,
"5second_long_argument_name"_a = 42,
"5third_long_argument_name"_a = 43);
case 6:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"6first_long_argument_name"_a = 41,
"6second_long_argument_name"_a = 42,
"6third_long_argument_name"_a = 43);
case 7:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"7first_long_argument_name"_a = 41,
"7second_long_argument_name"_a = 42,
"7third_long_argument_name"_a = 43);
case 8:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"8first_long_argument_name"_a = 41,
"8second_long_argument_name"_a = 42,
"8third_long_argument_name"_a = 43);
case 9:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"9first_long_argument_name"_a = 41,
"9second_long_argument_name"_a = 42,
"9third_long_argument_name"_a = 43);
case 10:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"10first_long_argument_name"_a = 41,
"10second_long_argument_name"_a = 42,
"10third_long_argument_name"_a = 43);
case 11:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"11first_long_argument_name"_a = 41,
"11second_long_argument_name"_a = 42,
"11third_long_argument_name"_a = 43);
case 12:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"12first_long_argument_name"_a = 41,
"12second_long_argument_name"_a = 42,
"12third_long_argument_name"_a = 43);
case 13:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"13first_long_argument_name"_a = 41,
"13second_long_argument_name"_a = 42,
"13third_long_argument_name"_a = 43);
case 14:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"14first_long_argument_name"_a = 41,
"14second_long_argument_name"_a = 42,
"14third_long_argument_name"_a = 43);
case 15:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"15first_long_argument_name"_a = 41,
"15second_long_argument_name"_a = 42,
"15third_long_argument_name"_a = 43);
case 16:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"16first_long_argument_name"_a = 41,
"16second_long_argument_name"_a = 42,
"16third_long_argument_name"_a = 43);
case 17:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"17first_long_argument_name"_a = 41,
"17second_long_argument_name"_a = 42,
"17third_long_argument_name"_a = 43);
case 18:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"18first_long_argument_name"_a = 41,
"18second_long_argument_name"_a = 42,
"18third_long_argument_name"_a = 43);
case 19:
return format("{first}{second}{first}{third}{first}{second}{first}{third}",
"19first_long_argument_name"_a = 41,
"19second_long_argument_name"_a = 42,
"19third_long_argument_name"_a = 43);
default:
return {};
}
}
std::wstring wfunc(int fake) {
switch (fake) {
case 0:
return format(L"{first}{second}{first}{third}", L"first"_a = L"abra",
L"second"_a = L"cad", L"third"_a = 99);
case 1:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"first_long_argument_name"_a = 41,
L"second_long_argument_name"_a = 42,
L"third_long_argument_name"_a = 43);
case 2:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"first_first_first_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 41,
L"second_second_second_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 42,
L"third_third_third_very_very_very_long_long_long_argument_argument_argument_name_name_name"_a = 43);
case 3:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"3first_long_argument_name"_a = 41,
L"3second_long_argument_name"_a = 42,
L"3third_long_argument_name"_a = 43);
case 4:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"4first_long_argument_name"_a = 41,
L"4second_long_argument_name"_a = 42,
L"4third_long_argument_name"_a = 43);
case 5:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"5first_long_argument_name"_a = 41,
L"5second_long_argument_name"_a = 42,
L"5third_long_argument_name"_a = 43);
case 6:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"6first_long_argument_name"_a = 41,
L"6second_long_argument_name"_a = 42,
L"6third_long_argument_name"_a = 43);
case 7:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"7first_long_argument_name"_a = 41,
L"7second_long_argument_name"_a = 42,
L"7third_long_argument_name"_a = 43);
case 8:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"8first_long_argument_name"_a = 41,
L"8second_long_argument_name"_a = 42,
L"8third_long_argument_name"_a = 43);
case 9:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"9first_long_argument_name"_a = 41,
L"9second_long_argument_name"_a = 42,
L"9third_long_argument_name"_a = 43);
case 10:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"10first_long_argument_name"_a = 41,
L"10second_long_argument_name"_a = 42,
L"10third_long_argument_name"_a = 43);
case 11:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"11first_long_argument_name"_a = 41,
L"11second_long_argument_name"_a = 42,
L"11third_long_argument_name"_a = 43);
case 12:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"12first_long_argument_name"_a = 41,
L"12second_long_argument_name"_a = 42,
L"12third_long_argument_name"_a = 43);
case 13:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"13first_long_argument_name"_a = 41,
L"13second_long_argument_name"_a = 42,
L"13third_long_argument_name"_a = 43);
case 14:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"14first_long_argument_name"_a = 41,
L"14second_long_argument_name"_a = 42,
L"14third_long_argument_name"_a = 43);
case 15:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"15first_long_argument_name"_a = 41,
L"15second_long_argument_name"_a = 42,
L"15third_long_argument_name"_a = 43);
case 16:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"16first_long_argument_name"_a = 41,
L"16second_long_argument_name"_a = 42,
L"16third_long_argument_name"_a = 43);
case 17:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"17first_long_argument_name"_a = 41,
L"17second_long_argument_name"_a = 42,
L"17third_long_argument_name"_a = 43);
case 18:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"18first_long_argument_name"_a = 41,
L"18second_long_argument_name"_a = 42,
L"18third_long_argument_name"_a = 43);
case 19:
return format(L"{first}{second}{first}{third}{first}{second}{first}{third}",
L"19first_long_argument_name"_a = 41,
L"19second_long_argument_name"_a = 42,
L"19third_long_argument_name"_a = 43);
default:
return {};
}
} Command:
|
b66d262
to
d4b727d
Compare
Here are more detailed benchmarks. Again, hyperfine was used for proper time measurement. There are more benchmarks this time on GCC 10.2, they demonstrate different use cases. Each benchmark name contains:
For example, source file for #include "fmt/format.h"
using namespace fmt;
void func(char* output) {
output = format_to(output, L"does not matter", L"0x"_a = 41, L"0arg"_a = 42, L"0a"_a = 43);
output = format_to(output, L"does not matter", L"1x"_a = 41, L"1arg"_a = 42, L"1a"_a = 43);
output = format_to(output, L"does not matter", L"2x"_a = 41, L"2arg"_a = 42, L"2a"_a = 43);
output = format_to(output, L"does not matter", L"3x"_a = 41, L"3arg"_a = 42, L"3a"_a = 43);
output = format_to(output, L"does not matter", L"4x"_a = 41, L"4arg"_a = 42, L"4a"_a = 43);
output = format_to(output, L"does not matter", L"5x"_a = 41, L"5arg"_a = 42, L"5a"_a = 43);
output = format_to(output, L"does not matter", L"6x"_a = 41, L"6arg"_a = 42, L"6a"_a = 43);
output = format_to(output, L"does not matter", L"7x"_a = 41, L"7arg"_a = 42, L"7a"_a = 43);
output = format_to(output, L"does not matter", L"8x"_a = 41, L"8arg"_a = 42, L"8a"_a = 43);
output = format_to(output, L"does not matter", L"9x"_a = 41, L"9arg"_a = 42, L"9a"_a = 43);
} As you can see, there are prefixes for all named arguments to eliminate potential optimizations, but I'm not sure that there would be any. And yes, these prefixes are syntactically wrong, but this function is never invoked. Also note that this is a runtime API, with static named arguments.
Raw data:
PR:
Python script for source files generation:head = '''
#include "fmt/format.h"
using namespace fmt;
void func(char* output) {
'''
short_format_call = '''
output = format_to(output,
{literal_prefix}"does not matter",
{literal_prefix}"{arg_prefix}x"_a = 41,
{literal_prefix}"{arg_prefix}arg"_a = 42,
{literal_prefix}"{arg_prefix}a"_a = 43);
'''
medium_format_call = '''
output = format_to(output,
{literal_prefix}"does not matter",
{literal_prefix}"{arg_prefix}some_medium_name"_a = 41,
{literal_prefix}"{arg_prefix}another_medium_name"_a = 42,
{literal_prefix}"{arg_prefix}last_medium_name"_a = 43);
'''
long_format_call = '''
output = format_to(output,
{literal_prefix}"does not matter",
{literal_prefix}"{arg_prefix}this_argument_name_is_quite_loooooooong"_a = 41,
{literal_prefix}"{arg_prefix}very_very_very_very_very_very_long_argument_name"_a = 42,
{literal_prefix}"{arg_prefix}argument_argument_argument_argument_argument_argument"_a = 43);
'''
tail = '''
}
'''
def compose(format_call: str, is_wide: bool, n: int):
result: str = ''
result += head
if n == 1:
result += format_call.format(literal_prefix=('L' if is_wide else ''), arg_prefix='')
else:
for x in range(0, n):
result += format_call.format(literal_prefix=('L' if is_wide else ''), arg_prefix=x)
result += tail
return result
def main():
with open('one_short.cpp', 'w') as f:
print(compose(short_format_call, False, 1), file=f)
with open('one_short_w.cpp', 'w') as f:
print(compose(short_format_call, True, 1), file=f)
with open('several_short.cpp', 'w') as f:
print(compose(short_format_call, False, 10), file=f)
with open('several_short_w.cpp', 'w') as f:
print(compose(short_format_call, True, 10), file=f)
with open('many_short.cpp', 'w') as f:
print(compose(short_format_call, False, 50), file=f)
with open('many_short_w.cpp', 'w') as f:
print(compose(short_format_call, True, 50), file=f)
with open('one_medium.cpp', 'w') as f:
print(compose(medium_format_call, False, 1), file=f)
with open('one_medium_w.cpp', 'w') as f:
print(compose(medium_format_call, True, 1), file=f)
with open('several_medium.cpp', 'w') as f:
print(compose(medium_format_call, False, 10), file=f)
with open('several_medium_w.cpp', 'w') as f:
print(compose(medium_format_call, True, 10), file=f)
with open('many_medium.cpp', 'w') as f:
print(compose(medium_format_call, False, 50), file=f)
with open('many_medium_w.cpp', 'w') as f:
print(compose(medium_format_call, True, 50), file=f)
with open('one_long.cpp', 'w') as f:
print(compose(long_format_call, False, 1), file=f)
with open('one_long_w.cpp', 'w') as f:
print(compose(long_format_call, True, 1), file=f)
with open('several_long.cpp', 'w') as f:
print(compose(long_format_call, False, 10), file=f)
with open('several_long_w.cpp', 'w') as f:
print(compose(long_format_call, True, 10), file=f)
with open('many_long.cpp', 'w') as f:
print(compose(long_format_call, False, 50), file=f)
with open('many_long_w.cpp', 'w') as f:
print(compose(long_format_call, True, 50), file=f)
with open('too_many_long.cpp', 'w') as f:
print(compose(long_format_call, False, 500), file=f)
with open('too_many_long_w.cpp', 'w') as f:
print(compose(long_format_call, True, 500), file=f)
if __name__ == '__main__':
main() |
to get arg index by name at compile-time
d4b727d
to
04603b5
Compare
Merged, thanks! |
This PR makes the name of named arguments created via
_a
literal be available at compile-time. So named arguments can be checked at compile-time now exactly like non-named ones.I haven't implemented these checks for
FMT_STRING
, but I did it for compile-time API. Thus these lines:produce the same compiled format, so it's zero-overhead for arguments naming. Of course,
fmt::arg(name, value)
-based named arguments still have this overhead because they cannot provide their names at compile-time. Here are the results:Besides that performance improvement, a fallback to runtime API is no longer needed for named arguments with specs because type information is available at compile-time. Here are the results:
Some points for this PR:
_a
literal provides statically named argument (i.e. its name available at compile-time) in form ofstatically_named_arg
typefixed_string
struct moved fromcompile.h
toformat.h
because it's used instatically_named_arg
,FMT_USE_NONTYPE_TEMPLATE_PARAMETERS
macro definition also moved thereis_named_arg
-related fixes applied for correctstatically_named_arg
treating as named argumentfield
struct creation andfield::format()
changed to have the value type of named argument stored as a templateT
parameter (notnamed_arg<T>
), same withspec_field
compile_format_string()
moved to separate functionparse_replacement_field_then_tail()
because it was duplicated 3 times, so maybe one more instantiation is not so bad 🤔runtime_named_field
for all named arguments that are not found