Skip to content

Commit

Permalink
Common: Remove dependency on cpp-optparse
Browse files Browse the repository at this point in the history
Doesn't remove it from the full project since three tools still use it.

FEXLoader argument loader is fairly special since we have a generator
tied to it. This generator lets us have some niceties where everything
ends up being a string literal without any sort of dynamic requirements.

cpp-optparse has a systemic issue where it makes deep copies of
/everything/ all the time. This means it has huge runtime costs and huge
stack usage (4992 bytes). The new implementation uses 608 bytes of stack
space (plus 640 if it actually parses a strenum).

When profiling cpp-optparse with FEXLoader's argument it takes ~2,039,307 ns to parse the arguments!
As a direct comparison this new implementation takes ~56,885 ns.
Both sides not getting passed /any/ arguments, really shows how spicy
this library is.
  • Loading branch information
Sonicadvance1 committed Sep 15, 2024
1 parent c8f3fe3 commit 2558039
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 101 deletions.
104 changes: 48 additions & 56 deletions FEXCore/Scripts/config_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,82 +351,73 @@ def print_config_option(type, group_name, json_name, default_value, short, choic

output_argloader.write("\n");

def print_argloader_options(options):
output_argloader.write("#ifdef BEFORE_PARSE\n")
output_argloader.write("#undef BEFORE_PARSE\n")
def print_argloader_options_map(options):
output_argloader.write("#ifdef ARG_TO_CONFIG\n")
output_argloader.write("#undef ARG_TO_CONFIG\n")
for op_group, group_vals in options.items():
for op_key, op_vals in group_vals.items():
default = op_vals["Default"]

if (op_vals["Type"] == "str" or op_vals["Type"] == "strarray" or op_vals["Type"] == "strenum"):
# Wrap the string argument in quotes
default = "\"" + default + "\""
value_type = op_vals["Type"]

# Textual default rather than enum based
if ("TextDefault" in op_vals):
default = "\"" + op_vals["TextDefault"] + "\""
ParserType = "ParserTypes::String"

short = None
choices = None
if value_type == "bool":
ParserType = "ParserTypes::BoolTrue"

if value_type == "strenum":
ParserType = "ParserTypes::StringEnum"

if value_type == "strarray":
ParserType = "ParserTypes::StringArray"

short = None
if ("ShortArg" in op_vals):
short = op_vals["ShortArg"]
if ("Choices" in op_vals):
choices = op_vals["Choices"]

print_config_option(
op_vals["Type"],
op_group,
op_key,
default,
short,
choices,
op_vals["Desc"])
if short != None:
output_argloader.write("{{\"-{}\", FEXCore::Config::ConfigOption::CONFIG_{}, {}}},\n".format(short, op_key.upper(), ParserType))

output_argloader.write("\n")
output_argloader.write("{{\"--{}\", FEXCore::Config::ConfigOption::CONFIG_{}, {}}},\n".format(op_key.lower(), op_key.upper(), ParserType))

if value_type == "bool":
output_argloader.write("{{\"--no-{}\", FEXCore::Config::ConfigOption::CONFIG_{}, ParserTypes::BoolFalse}},\n".format(op_key.lower(), op_key.upper()))
output_argloader.write("#endif\n")

def print_parse_argloader_options(options):
output_argloader.write("#ifdef AFTER_PARSE\n")
output_argloader.write("#undef AFTER_PARSE\n")
def print_parse_argloader_strconversion(options):
output_argloader.write("#ifdef STR_CONVERT_PARSE\n")
output_argloader.write("#undef STR_CONVERT_PARSE\n")
for op_group, group_vals in options.items():
for op_key, op_vals in group_vals.items():
output_argloader.write("if (Options.is_set_by_user(\"{0}\")) {{\n".format(op_key))
value_type = op_vals["Type"]
if not "ArgumentHandler" in op_vals:
continue

output_argloader.write("case FEXCore::Config::ConfigOption::CONFIG_{}: {{\n".format(op_key.upper()))
output_argloader.write("\tauto Converted = FEXCore::Config::Handler::{0}(SecondArg);\n".format(op_vals["ArgumentHandler"]))
output_argloader.write("\tLoader->SetArg(FEXCore::Config::ConfigOption::CONFIG_{}, Converted);\n".format(op_key.upper()))
output_argloader.write("break;\n".format(op_key.upper()))
output_argloader.write("}\n")

output_argloader.write("#endif\n")

def print_parse_argloader_enumparser(options):
output_argloader.write("#ifdef STR_ENUM_PARSE\n")
output_argloader.write("#undef STR_ENUM_PARSE\n")
for op_group, group_vals in options.items():
for op_key, op_vals in group_vals.items():
value_type = op_vals["Type"]
NeedsString = False
conversion_func = "fextl::fmt::format(\"{}\", "
if ("ArgumentHandler" in op_vals):
NeedsString = True
conversion_func = "FEXCore::Config::Handler::{0}(".format(op_vals["ArgumentHandler"])
if (value_type == "str"):
NeedsString = True
conversion_func = "std::move("
if (value_type == "bool"):
# boolean values need a decimal specifier. Otherwise fmt prints strings.
conversion_func = "fextl::fmt::format(\"{:d}\", "

if (value_type == "strenum"):
output_argloader.write("\tfextl::string UserValue = Options[\"{0}\"];\n".format(op_key))
output_argloader.write("\tSet(FEXCore::Config::ConfigOption::CONFIG_{}, FEXCore::Config::EnumParser<FEXCore::Config::{}ConfigPair>(FEXCore::Config::{}_EnumPairs, UserValue));\n".format(op_key.upper(), op_key, op_key, op_key))
elif (value_type == "strarray"):
# these need a bit more help
output_argloader.write("\tauto Array = Options.all(\"{0}\");\n".format(op_key))
output_argloader.write("\tfor (auto iter = Array.begin(); iter != Array.end(); ++iter) {\n")
output_argloader.write("\t\tSet(FEXCore::Config::ConfigOption::CONFIG_{0}, *iter);\n".format(op_key.upper()))
output_argloader.write("\t}\n")
else:
if (NeedsString):
output_argloader.write("\tfextl::string UserValue = Options[\"{0}\"];\n".format(op_key))
else:
output_argloader.write("\t{0} UserValue = Options.get(\"{1}\");\n".format(value_type, op_key))
output_argloader.write("case FEXCore::Config::ConfigOption::CONFIG_{}: {{\n".format(op_key.upper()))
output_argloader.write("\tauto Converted = FEXCore::Config::EnumParser<FEXCore::Config::{}ConfigPair>(FEXCore::Config::{}_EnumPairs, SecondArg);\n".format(op_key, op_key, op_key))
output_argloader.write("\tif (Converted) { SecondArg = *Converted; }\n")
output_argloader.write("\tLoader->SetArg(FEXCore::Config::ConfigOption::CONFIG_{}, SecondArg);\n".format(op_key.upper()))
output_argloader.write("break;\n".format(op_key.upper()))
output_argloader.write("}\n")

output_argloader.write("\tSet(FEXCore::Config::ConfigOption::CONFIG_{0}, {1}UserValue));\n".format(op_key.upper(), conversion_func))
output_argloader.write("}\n")

output_argloader.write("#endif\n")


def print_parse_envloader_options(options):
output_argloader.write("#ifdef ENVLOADER\n")
output_argloader.write("#undef ENVLOADER\n")
Expand Down Expand Up @@ -574,8 +565,9 @@ def check_for_duplicate_options(options):

# Generate argument loader code
output_argloader = open(output_argumentloader_filename, "w")
print_argloader_options(options);
print_parse_argloader_options(options);
print_argloader_options_map(options);
print_parse_argloader_strconversion(options);
print_parse_argloader_enumparser(options);

# Generate environment loader code
print_parse_envloader_options(options);
Expand Down
4 changes: 2 additions & 2 deletions FEXCore/include/FEXCore/Config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

namespace FEXCore::Config {
namespace Handler {
static inline std::optional<fextl::string> SMCCheckHandler(std::string_view Value) {
static inline fextl::string SMCCheckHandler(std::string_view Value) {
if (Value == "none") {
return "0";
} else if (Value == "mtrack") {
Expand All @@ -28,7 +28,7 @@ namespace Handler {
}
return "0";
}
static inline std::optional<fextl::string> CacheObjectCodeHandler(std::string_view Value) {
static inline fextl::string CacheObjectCodeHandler(std::string_view Value) {
if (Value == "none") {
return "0";
} else if (Value == "read") {
Expand Down
Loading

0 comments on commit 2558039

Please sign in to comment.