c-blake / cligen

Nim library to infer/generate command-line-interfaces / option / argument parsing; Docs at
https://c-blake.github.io/cligen/
ISC License
496 stars 23 forks source link

Setting `clCfg.helpSyntax = ""` doesn't prevent inclusion of the syntax into the binary #207

Closed ZoomRmc closed 2 years ago

ZoomRmc commented 2 years ago

Setting clCfg.helpSyntax = "" doesn't prevent inclusion of the full original syntax string into the binary. Same goes (less surprisingly) for suppressing the help field with "CLIGEN-NOHELP".

Tested on 1.5.23

This poses a bigger question, what's the status/plans on supporting modularity of available features? See how this is presented for Rust's clap: https://docs.rs/clap/latest/clap/#feature-flags

If cligen had a bunch of options which would prevent the corresponding parts of the code to be compiled at all, this would definitely help with the image of a somewhat bloated library.

c-blake commented 2 years ago

Sorry for the delay - I was not notified of this issue creation by the system.

I only check helpSyntax.len (since Nim ditched nil as a valid state for string). So, you can probably set it to a single space to get what you want.

I am not sure I understand the NOHELP complaint. Do you want "CLIGEN-NOHELP" to not appear? or the option key to not appear? If you are willing to set hTabSuppress like test/OmitInHelpTable.nim to, say, a single space (and be sure to use it for that semantically..) you can remove the bytes.

As to the bigger question, I am very aware of the object code explosion and I am certainly open to adding define guards like -d:noSuggest to let space-sensitive users "opt out". It's a somewhat involved project, though, and I am not sure where the "lowest hanging fruit" is. Anyone know a binary space profiler tool for Nim?

ZoomRmc commented 2 years ago

Disregard "CLIGEN-NOHELP" related remark, it's nonsense.

I only check helpSyntax.len (since Nim ditched nil as a valid state for string). So, you can probably set it to a single space to get what you want.

Not sure what you mean. Setting clCfg.helpSyntax to an empty string or a single space makes no difference (except the former being 18 bytes smaller than the latter), the whole "BASIC CHEAT SHEET" gets compiled in.

Just curious, does anyone really use overriding syntax help text? How valuable is it, considering you can't really change the syntax very much (only separator characters, if I'm not mistaken).

A static (and strdefined) switch for clCfg which can when-bypass the whole "--help-syntax" path would be nice.

c-blake commented 2 years ago

Oh...I see. That comes from cligen/syntaxHelp.nim. You can replace that whole file with your own whole copy which sets it to "" or maybe " ". You just want that file to be "ahead of cligen actual" in your nim module search path. Slightly more work than a -d:, but should already work.

Much of the advanced syntax described in the message is amendable/augmentable through your own custom argument convertors (overriding or augmenting cligen/argcvt.nim). See, e.g., the # seqs section of argcvt.nim.

ZoomRmc commented 2 years ago

Much of the advanced syntax described in the message is amendable/augmentable through your own custom argument convertors (overriding or augmenting cligen/argcvt.nim). See, e.g., the # seqs section of argcvt.nim.

Where's the line between a library and a library-building framework? :D

Thanks for the workaround tip.

c-blake commented 2 years ago

It's just a library. Like most Nim libraries (and even C libraries if you count .o's in .a's), it can have replaceable components at the "whole file" level. There is some art to deciding what file boundaries should be, but I tried to do a granularity that might be useful to advanced use cases. Some other "file hooks", if you will, are cligen/helpTmpl.nim, and cligen/clCfgInit.nim. There are other "pluggable" things like mergeParams, too. I try to have it be an "open architecture" as much as is easy.

ZoomRmc commented 2 years ago

Don't take my comments so seriously, I'm being too frivolous lately. Sorry for that.

c-blake commented 2 years ago

Ok. Mostly elaborating.

For what it's worth, I don't do this myself, but it is also possible to override the contents of helpSyntax at the user-config level. I realize this is a bit incoherent. On the one hand, advanced their-own-argcvt-writers using the rest of the framework might want to change the include file. OTOH, some end-CLusers might want the advanced dump to be a briefer refresher or more expansive or bold italic/highlight/UPCASE something they always forget about the standard set of argcvt's. So, this allows for CLusers to kind of outsmart themselves IF CLauthors change things but just keep the default config parsing. It's all a many layered beast that probably deserves about 10x the documentation I've given it.

I think we can close this issue and defer the "diet mode" for cligen to such a time as when we actually know which features cause what amount of bloat. Until then, it's actually hard to answer those questions. The rSTdown parsing/color highlights/etc might also be a cause..or the config parsing that brings in that part of the stdlib or ...? (Yes, yes...I realize without an object file space profiler that investigating is almost as much work as implementing off switches...PRs along these off switch lines are welcome.)

c-blake commented 2 years ago

See https://github.com/c-blake/cligen/commit/e3ddf4631064f0b3db4d978b8ae7325bce4f0029 for one move along the direction desired here.

c-blake commented 2 years ago

I compiled 4 programs with -d:danger --cc:gcc --opt:size (gcc-12) on Linux (the danger is quite important for object file size, but obviously has safety implications):

import os, strutils; echo parseInt(paramStr(1))
import cligen/parseopt3
for kind, key, val in getopt(): echo kind, key, val

and:

proc foo() = discard
import cligen; dispatch foo

and stripped with -d:cgCfgNone on one of the full.nim compiles (and empty.nim is just a zero byte file) I got:

  Bytes Program
  30992 empty
  39184 ostrut
  47480 popt3
 113088 full
 158240 full-cfg

The leap from ostrut to popt3 only adds about 8 kB. So, CritBitTree is not looking very expensive here. Looking at the code, it would be pretty involved to compile-time conditionalize that since CBT turns up in types (to compute just once vs. every time nullifying its speed-up creating a once/many/object-space trade-off space). I'm not too interested in the work of trying to make CritBitsTree dependence fully compile-time disable-able myself, but I am open to PRs. It's arguably not worth it, but it seems you were interested in as little as 3K of syntaxHelp...

The user-config parsing leap is about 45 kB (probably more for TOML), but now there is a switch for that which took but a 1-line patch. :) A minimal API still generates about 2.4x (64 kB) more than just parseopt3. So, there is probably other lower hanging fruit anyway, but it is unlikely to be as big as the 45 kB savings of -d:cgCfgNone. So, that's probably the single biggest improvement we could have made, at least for the empty API and cligen/argcvt is user overridable/extendible.

Anyway, 113088-30992 = 82096 as overhead for a trimmed cligen build does not seem so terrible to me even if you don't want the various functionalities. I mean, it's only about 2.6x the base-case Nim overhead of 30992.

c-blake commented 2 years ago

NOTE: Updated the above quite a bit.

c-blake commented 2 years ago

A little more color on https://github.com/c-blake/cligen/commit/595e8599f51c2c68220d2512cb49adec5624d785. The following was my test set up (Linux, gcc-12.1.1_p20220625) (from head *nim*):

  ==> f.nim <==
  proc foo = discard
  import cligen; dispatch foo
  ==> f.nims <==
  patchFile("cligen", "syntaxHelp", "shelp")
  ==> shelp.nim <==
  const syntaxHelp = " "

and commands like (& substrings):

nim c --threads:off -d:danger -d:cgCfgNone -d:cgNoColor --cc:gcc --opt:size f

I get sizes like:

158240 f-base
154144 f-noHelps
108992 f-noCf
 96704 f-noCfCo

which is a pretty good reduction for not much effort. To me this seems largely a marketing gimmick anyway and remaining overhead is only 2X an empty Nim file (which is 30992 bytes) and even trivial programs will be 50-75k. Programs that do anything useful will be more like 150k and the extra 60 of cligen seems not onerous. Rust programs on my system are all multi-megabyte anyway (with stripped fd over 2.5 MB, rg over 4.5, and pijul over 13). So, you know - costs in context & all that.

size *.c.o in nim cache with follow-up nm -S --size-sort -td xyz.c.o suggests as follows.

The next biggest space taker is the somewhat important wrap and at 7158 and alignTable at 2827 bytes & friends totalling about ~12 kB. Having a program using a tool to auto-generate/format help but properly format it seems lame. So, while we could probably --define:crappyHelpFmt, I'm not sure I'd call that good advertising either.

Our misspelling/suggestion system is a more factorizable feature, but it is tiny - like 1337 bytes..just kind of noise on the big --help-syntax message. I think people like the feature enough to pay so few bytes and the run-time cost is only incurred upon failure/program about to exit. So it seems "worth it" all around for the better error message (it's really fast anyway unless you have inhuman option sets).

Remaining space-takers are dispatcher/parser overhead for the simplest case; Hint - it's not empty: help, help-syntax are there as well as various behavioral things keyed off of clCfg. That could probably be trimmed - I would guess by not much more than ~2X down from 7532 bytes. It'd take lifting a bunch of run-time stuff into AST building and much careful work & tests to save that <4 kB.

Everything else would be about how space scales up with the complexity of the parameter list of the wrapped function which someone can study if they want.

So, anyway - yeah, cligen has many features, but 96 kB total overhead including 31 for Nim itself seems not so bloated in practical space terms. (I'm aware you can go nuts and get down to single 4k page binaries..but that seems more like climbing a mountain because it is there work.) I'd estimate only 4..16 kB more can be saved, and that with a lot of work. And just these two defines & --helps hack take cligen-specific overhead down from 158240-30992=127248 down to 96704-30992=65712 which is basically half the overhead.

"In real life", people probably don't do --opt:size, but --opt:speed. With that, space numbers (both subdivided and total) roughly double (e.g. 179 kB binary). But then with -flto total size comes back down to 137k, but the same easy size-profiling trick seems to fail with -flto. It probably requires more sophisticated tooling to do "attribution" in that mode.

In short, I think this covers the really easy big space reductions, but if someone else wants to try some things, be my guest.

ZoomRmc commented 2 years ago

Thanks for the detailed analysis.

c-blake commented 2 years ago

Just one other data point, but if I use this clapper package that has no deps outside Go's stdlib to build this Go program:

package main
import ("fmt"; "os"; "github.com/thatisuday/clapper")
func main() {
  reg := clapper.NewRegistry() // create a new registry
  if _, ok := os.LookupEnv("NO_ROOT"); !ok {
    rootCommand, _ := reg.Register("")       // root command
    rootCommand.AddFlag("verb", "v", true, "") // --verb,-v;dflt: true
  }
  command, err := reg.Parse(os.Args[1:])
  if err != nil {
    fmt.Printf("error => %#v\n", err); return
  }
  fmt.Printf("sub-command => %#v\n", command.Name)
  for argName, argValue := range command.Args {
    fmt.Printf("argument(%s) => %#v\n", argName, argValue)
  }
  for flagName, flagValue := range command.Flags {
    fmt.Printf("flag(%s) => %#v\n", flagName, flagValue)
  }
}

with the same version of gcc and go build -compiler gccgo -gccgoflags='-Os -flto' foo.go then I get a binary executable that is 90360 bytes. Meanwhile, if I do an mm:arc/danger/opt:size/useMalloc/-d:lto/no-config-no-color build of that f.nim in utils/size-prof, I get only 80200 bytes. Go at least has a reputation/image for these lean-mean dependency free binaries and we already do a little better than that with cligen-HEAD just with the right compile-flags.

I haven't tried the same experiment with the Rust package you mentioned, but I would not have a very optimistic Bayesian prior on it beating the cligen numbers.

Now, of course, anyone looking through the above or the new util/size-prof would mostly see evidence that Nim is probably better than Go at getting small binaries, and Nim's system module is already considered kind of bloated. So, it's not like ultra-conclusive or anything. I'm not sure cligen binaries with the new trim-defines deserve much scorn, either though.

c-blake commented 2 years ago

Just as a follow-up to that last comment, according to your (EDIT: issue opening clap) link, only color and suggestions are activated by default and if I clone that repo and do cargo build --release --features="derive" --example demo then I get target/release/examples/demo clocking in at 887072 bytes (after stripping).

I did not learn enough to disable color & suggestions, but I'd bet you don't go below 744 kB which is how big my procs is - the largest most tricked out PGO-compiled cligen multi-program I know. I don't know enough about driving Rust builds to do the -Os -flto equivalent. If the 2X reduction for gcc going from non-space to space optimized roughly applies, I would remain...not optimistic (EDIT: about Rust sizes being very good).

Don't get me wrong - I do get the "pay for what you use" mentality, and support it generally - if not hard to provide - but most benchmarking needs to concern itself with "compared to what?". Compared to bare Nim, sure we could do a bit better - maybe 25..30 kB more of the 50..60 KB excess in the smallest space-taking mode, but we are already doing better than Go and much better than Rust.

c-blake commented 1 year ago

Sure enough - after following the instructions here - the same minimal features clap demo clocked in at 510232 bytes, over 5X bigger than the 96704 cligen got down to disabling 2 spacey features. (Although compared to itself internally, enabling all the features might make clap balloon even more dramatically - measuring all that is out of scope for me.)