-
Notifications
You must be signed in to change notification settings - Fork 6
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
Improve compilation time and memory #124
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## 2.x #124 +/- ##
==========================================
+ Coverage 98.42% 98.54% +0.11%
==========================================
Files 25 25
Lines 1718 1790 +72
==========================================
+ Hits 1691 1764 +73
+ Misses 27 26 -1 ☔ View full report in Codecov by Sentry. |
09c6805
to
913f6e7
Compare
Thanks for this effort. I've never had time to look at it. Please be aware that there is not yet committed change where I rewrote parser.d so it will affect this PR. I'm rebasing changes and will push them as soon as I'm done. |
Everything is fine; it’s not ready yet to look at it anyway. Rewriting |
FYI I'm planning to merge 2.x branch into master as I'm done with major changes and IMO it makes sense to have everything in default branch. |
Iʼll keep that in mind, thanks. For the record, in the last few days Iʼve been optimizing pure templates (those that do not produce any code in the binary) – rewriting Trying a different approach now. It is showing prominent results. |
913f6e7
to
d17eeaf
Compare
Thanks to your recent refactoring, even unpatched I think I’ll stop here. For the best end-user experience, I’d suggest moving tests from To sum up:
That was an amusing journey. P.S. I also thought about splitting |
argparse/source/argparse/api/cli.d Lines 155 to 160 in 498790f
By the way, now I understand that DMD was correct here. Now that |
I think the better approach would be to move those unit tests to other modules. I've been adding new unit tests there but didn't pay attention to remove them from
I looked at this some time ago and it didn't look promising at that point because I found working on parser rewriting will give greater benefit so I started working on that. May it makes sense to review that idea again since I'm done with rewriting. |
argparse/source/argparse/internal/parser.d Lines 620 to 623 in eb44f11
If I make both branches identical (passing Upd.: Measured for a |
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.
Just curious, is added argument soup (scope
nothrow
pure
@safe
etc) required?
I think we can live with that for now. I'd rather look at possibility of splitting |
A library should correctly list attributes for its public APIs, or otherwise it would be disruptive to users who wish to use those attributes. There are few things more frustrating in D programming than getting a compiler error third_party.someFunc is not safe, peeking at the implementation, and finding out there were no reasons not to declare it safe. Yet it is That being said, I think it is tolerable to leave out attributes for UDA-related API since it is supposed to be called at struct-declaration time, when it doesn’t matter. I’m also ready to drop them from private/package functions where it doesn’t affect API if you are not comfortable with them: while I think it is always good to have more semantic checks from the compiler, that is my own opinion, and I admit I’ve pushed it too far here. |
I do not propose enabling colors always/never. That was just a quick test to see how much we can gain from splitting the config. |
See andrey-zherikov#124 for more information.
No worries, I got your point :) |
I don't mind having these attributes. Could you please do the following:
|
81e987f
to
1e6c259
Compare
See andrey-zherikov#124 for more information.
No problem. I excluded that commit from this PR. |
Is it too late to ask you to split this PR into separate smaller/simpler PRs that do single thing each? It's hard to review big one since you did a lot of things. |
This helps in reducing both compilation time and (build-time) memory requirement.
2.3 GB -> 1.7 GB.
1.7 GB -> 1.6 GB.
1.6 GB -> 0.8 GB.
1e6c259
to
879a161
Compare
I hope it became more manageable now. |
I checked improvements on my WSL Ubuntu: memory usage during |
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.
I'm done with the first commit only. Will continue reviewing as time permits.
In general I prefer to leave Config
as template parameter in parser unless moving it to runtime parameters gives feasible benefits. One item in my TODO list is to try splitting Config
into smaller parts with "single responsibility", for example: there is definitely one part that affects parser that IMO should stay as template parameter and there is also a "styling" part that can absolutely be runtime parameter.
{ | ||
static assert(info.shortNames.length + info.longNames.length > 0); | ||
assert(info.shortNames.length + info.longNames.length > 0); |
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.
Note for possible future changes (not required in this PR): this should be moved to TypeTraits
|
||
private void initialize(Config config, ArgumentInfo[] infos)() | ||
private void initialize(ref const Config config, const(ArgumentInfo)[] infos) |
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.
Do we need ref
here for config
?
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.
It will work either way. I have no strong reasons to either keep ref
or drop it. Put it here just because Config
is a 168-byte struct.
|
||
static assert(name == "" || getCommandInfo.names.length > 0 && getCommandInfo.names[0].length > 0, "Command "~COMMAND.stringof~" must have name"); | ||
assert(name == "" || info.names.length > 0 && info.names[0].length > 0, "Command "~COMMAND.stringof~" must have name"); |
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.
Note for future possible changes: move this to TypeTraits
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.
Other commits were much more manageable :-D
They look good.
Hm, I see your point. To be honest, I haven’t looked at the parser’s source carefully yet; was planning to do it after we decide on #117. I’ll return Smaller parts with single responsibility sounds great. For example, |
So that the compiler can prune dead branches of code.
Renamed into `finalize` in the process. It only fills `ArgumentInfo` so it doesn't have to know about UDAs at all.
alias uda0 = defaultUDA; | ||
enum checkMinMax0 = true; // Passed `defaultUDA` always has undefined `minValuesCount`/`maxValuesCount` |
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.
defaultUDA == NamedArgument()
always so we can drop this parameter and assign uda0 = ArgumentUDA!(ValueParser!(void, void, void, void, void, void)).init
instead.
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.
defaultUDA == NamedArgument()
was done to avoid dependency cycle between .d modules. Maybe it's not needed anymore but I didn't check.
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.
Got it. I think it can be solved by splitting argumentuda.d
into two files (struct ArgumentUDA(T)
vs getMemberArgumentUDA
). Will do it after we are done with this PR.
argparse/source/argparse/internal/argumentuda.d Lines 30 to 33 in 7cff352
By the way, I wonder whether this code is correct. If |
I think |
static assert(udas.length <= 1, "Member "~TYPE.stringof~"."~symbol~" has multiple '*Argument' UDAs"); | ||
static assert(typeUDAs.length <= 1, "Type "~MemberType.stringof~" has multiple '*Argument' UDAs"); | ||
|
||
static if(udas.length > 0) |
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.
Maybe convert this if
this way since defaultUDA
always has undefined minValuesCount
/maxValuesCount
?
static if(udas.length > 0)
enum uda0 = udas[0];
else
alias uda0 = defaultUDA;
enum checkMinMax0 = uda0.info.minValuesCount.isNull || uda0.info.maxValuesCount.isNull;
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.
It’s on my checklist but can’t be done right now: defaultUDA
is not compile-time-known, we cannot assign a value derived from it to an enum
.
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.
np, we can do this later
uda.info.placeholder = symbol.toUpper; | ||
} | ||
} | ||
static if(udas.length > 0) |
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.
As an idea:
static if(udas.length > 0) | |
static if(udas.length > 0) | |
alias memberUDA = () => udas[0]; | |
else | |
alias memberUDA = () => defaultUDA; | |
static if(typeUDAs.length > 0) | |
alias addTypeUDA = _ => _.addDefaults(typeUDAs[0]); | |
else | |
alias addTypeUDA = _ => _; | |
alias finalize = _ { _.info = _.info.finalize!MemberType(config, symbol); return _; }; | |
enum uda = memberUDA.addTypeUDA.finalize; | |
static if(uda.info.minValuesCount.isNull || uda.info.maxValuesCount.isNull) | |
{ | |
alias addMinMax = _ { | |
if(_.info.minValuesCount.isNull) _.info.minValuesCount = defaultValuesCount!MemberType.min; | |
if(_.info.maxValuesCount.isNull) _.info.maxValuesCount = defaultValuesCount!MemberType.max; | |
return _; | |
}; | |
return uda.addMinMax; | |
} | |
else | |
return uda; |
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 looks nice, but unfortunately, you’ll get an error on line enum uda = memberUDA.addTypeUDA.finalize
since defaultUDA
is not available at compile time.
N.B. UFCS does not work for local symbols; should be finalize(addTypeUDA(memberUDA))
instead.
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.
Yeah, it's a drawback. I think it would be better to go that way although I don't mind it to be improved in follow up PR.
|
||
uda.info.displayNames = chain(uda.info.shortNames, uda.info.longNames).map!toDisplayName.array; | ||
} | ||
static if(checkMinMax0 && checkMinMax1) |
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.
The whole point of the code under this if
is to set min/max
if they are not set. Please see my comment above - I hope it's clearer and avoids using checkMinMax*
by just having simple static if (min/max is not set)
.
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.
I’d suggest to leave it as is for now. When I get rid of defaultUDA
, this code will become prettier as well.
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.
No objections
alias toDisplayName = _ => ( _.length == 1 ? config.namedArgPrefix ~ _ : text(config.namedArgPrefix, config.namedArgPrefix, _)); | ||
|
||
info.displayNames = chain(info.shortNames, info.longNames).map!toDisplayName.array; |
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.
Found bug in code unrelated to your changes:
alias toDisplayName = _ => ( _.length == 1 ? config.namedArgPrefix ~ _ : text(config.namedArgPrefix, config.namedArgPrefix, _)); | |
info.displayNames = chain(info.shortNames, info.longNames).map!toDisplayName.array; | |
info.displayNames = info.shortNames.map!(_ => config.namedArgPrefix ~ _) ~ | |
info.longNames.map!(_ => text(config.namedArgPrefix, config.namedArgPrefix, _)); |
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.
I agree it is better now, but could you explain why it is a bug please? Just curious.
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.
I split names
into shortNames
and longNames
in order to implement supporting of multi-character short names. This feature is not implemented yet so this bug was to be discovered. I'll take care of it later - my comment was mostly for me.
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.
OK. Apparently, ArgumentInfo
will need a constructor taking string[] names
.
Also, in finalize
, chain(info.shortNames.map!(...), info.longNames.map!(...)).array
will result in a single allocation while ...array ~ ...array
will need three.
Just a FYI: I plan to merge |
* Refactor (#96) * Move onError tp api/cli.d * Improve ANSI styling handling and making AnsiStylingArgument boolean-like * Make Parser private * Remove hooks * Remove subcommands * Remove argumentparser * Update readme * Add unit tests * Rename Config.helpStyle to Config.styling (#97) * Add unit test (#98) * Refactor (#99) * Small cleanup * Make Config a template parameter * Add unit test * Styling in error messages (#100) * Add errorMessagePrefix to Style * Rename Style.namedArgumentName => Style.argumentName * Styling in error messages * Add check for argument name (#101) * Rename Config.namedArgChar to namedArgPrefix (#102) * Add checks for positional arguments (#103) * Refactor (#105) * Add ArgumentInfo.memberSymbol * Small refactoring * Move Restriction and RestrictionGroup to internal.restriction * Remove symbol parameter * remove partial apply * Small refactoring * Small refactoring * Add unit test * Pin LDC to 1.34.0 (#108) * Add '--verbose' to builds * Refactoring (#127) * Required positional arguments must come before optional ones * Optional positional arguments are not allowed with default subcommand * Rewrite parser (#128) * Refactor * Split ArgumentInfo.names to shortNames and longNames * Add namedArguments and positionalArguments to Arguments * Rename Arguments.arguments to info * Refactor Arguments API * Remove ArgumentInfo.ignoreInDefaultCommand * Rewrite parser * unit tests * Update readme (#121) * Update readme * Update the examples as well * Apply suggested changes to readme * Declare `Style.Default` with an alternative syntax (#130) * Turn `main` and `mainComplete` into regular templates (#132) They don't need advanced features that template mixins provide. (Regular templates are mixable, too.) https://dlang.org/spec/template-mixin.html * Make Styling API `nothrow pure @safe` (#133) * Reduce allocations in Styling API (#134) * Reduce allocations in Styling API * Remove the overload of `TextStyle.opCall` that takes a sink We should make `StyledText` a range instead. * Do not depend on `std.regex` (#131) * Do not depend on `std.regex` This saves 1.5 MB in the binary, which is desirable since not every program that uses `argparse` may want to use regexes - or that particular implementation of regexes. `argparse.ansi.getUnstyledText` became eager, but the library wasn't exploiting its laziness anyway. * Make `getUnstyledText` lazy and `@nogc` * Use constants instead of hardcoded characters * Rework the auxiliary function Co-Authored-By: Andrey Zherikov <andrey-zherikov@users.noreply.github.com> * Remove one mutable variable * Add a small comment --------- Co-authored-by: Andrey Zherikov <andrey-zherikov@users.noreply.github.com> Co-authored-by: Andrey Zherikov <andrey.zherikov@gmail.com> * Improve compilation time and memory (#124) * Use regular parameters instead of template parameters where possible This helps in reducing both compilation time and (build-time) memory requirement. * Deduplicate `HelpArgumentUDA.parse` 2.3 GB -> 1.7 GB. * Deduplicate `Complete.CompleteCmd` 1.7 GB -> 1.6 GB. * Compile the completer on demand 1.6 GB -> 0.8 GB. * Simplify `CLI!(...).complete` * Deduplicate `CounterParsingFunction` * Make some of the config's fields statically known by the parser So that the compiler can prune dead branches of code. * Remove `assignChar` from parser's template parameters * Try to simplify min-max-handling logic in `getMemberArgumentUDA` * Add a unit test for `getMemberArgumentUDA` * Move `getArgumentUDA` to `argparse.internal.arguments` Renamed into `finalize` in the process. It only fills `ArgumentInfo` so it doesn't have to know about UDAs at all. * Import non-std modules once per file --------- Co-authored-by: Nickolay Bukreyev <SirNickolas@users.noreply.github.com> Co-authored-by: Andrey Zherikov <andrey-zherikov@users.noreply.github.com>
* Use regular parameters instead of template parameters where possible This helps in reducing both compilation time and (build-time) memory requirement. * Deduplicate `HelpArgumentUDA.parse` 2.3 GB -> 1.7 GB. * Deduplicate `Complete.CompleteCmd` 1.7 GB -> 1.6 GB. * Compile the completer on demand 1.6 GB -> 0.8 GB. * Simplify `CLI!(...).complete` * Deduplicate `CounterParsingFunction` * Make some of the config's fields statically known by the parser So that the compiler can prune dead branches of code. * Remove `assignChar` from parser's template parameters * Try to simplify min-max-handling logic in `getMemberArgumentUDA` * Add a unit test for `getMemberArgumentUDA` * Move `getArgumentUDA` to `argparse.internal.arguments` Renamed into `finalize` in the process. It only fills `ArgumentInfo` so it doesn't have to know about UDAs at all. * Import non-std modules once per file
Memory usage dropped from ∞ to 2.9 GB DMD + 0.25 GB linker, but I want to reduce it to a sane limit. Stay tuned.
Closes #122 when it is complete.