argparse icon indicating copy to clipboard operation
argparse copied to clipboard

`append` performance with lots of arguments

Open pauldraper opened this issue 3 months ago • 9 comments

The append action has O(n^2) performance, due to copying the list for every item.

https://github.com/nodeca/argparse/blob/a645a9a9d3d0a347f383d0b795859e67dfae6ad8/argparse.js#L1559-L1560

pauldraper avatar Sep 12 '25 09:09 pauldraper

It it a problem? What is the real use case to solve?

puzrin avatar Sep 12 '25 09:09 puzrin

A command line with several thousand arguments. (Can be really long if @flagfile is used.)

./compiler.js --src=src1.js --src=src2.js --src=src3.js ... output 

For years, I've been curious why a tool I had was unusually slow to start, now I know.

pauldraper avatar Sep 27 '25 04:09 pauldraper

I understand it's technically possible to pass multiple thousands of params. Just interesting, where is this useful in the real world?

puzrin avatar Sep 27 '25 17:09 puzrin

5000 arguments are parsed in 1 second for me... not ideal, but not horrible; python is 2x faster strangely enough, but complexity is the same.

python impl is literally the same with the same complexity: https://github.com/python/cpython/blob/195d13c85e17ab5cf6ac2a6de098bbf514cae207/Lib/argparse.py#L1099-L1100

and that code has already turned 16 years old for sure, git blame goes all the way to svn import

rlidwka avatar Sep 28 '25 22:09 rlidwka

Copying is required there because otherwise it's going to modify default args. Here's a case where it matters:

import { ArgumentParser } from 'npm:argparse';

let parser = new ArgumentParser({
  description: 'Count append-type CLI flags',
});

parser.add_argument('-a', '--append', {
  action: 'append',
  default: ["opt1", "opt2"]
});

let args = parser.parse_args();

let count = Array.isArray(args.append) ? args.append.length : 0;

console.log(`Number of flags passed: ${count}`);
console.log('Parsed values:', args);

args = parser.parse_args();

count = Array.isArray(args.append) ? args.append.length : 0;

console.log(`Number of flags passed: ${count}`);
console.log('Parsed values:', args);

rlidwka avatar Sep 28 '25 22:09 rlidwka

copying the list for every item

I've measured it, and on 10k arguments (3 second runtime) copying the list of every item takes below 100ms (3% of the execution time)

so if there's a performance issue, I believe you need to search for it elsewhere

rlidwka avatar Sep 28 '25 22:09 rlidwka

copying the list of every item takes below 100ms

The performance problem definitely appears.

const { ArgumentParser } = require("argparse");

const parser = new ArgumentParser({
  description: "Test",
  fromfile_prefix_chars: "@",
});

parser.add_argument("--file", { action: "append" });

const start = performance.now();
const args = parser.parse_args();
const end = performance.now();

console.log(`Arg parsing: ${(end - start).toFixed(1)}ms`);
seq 1 10000 | sed s/^/--file=/ > flags
node test.js @flags
Arg parsing: 2495.4ms

pauldraper avatar Oct 06 '25 02:10 pauldraper

I understand it's technically possible to pass multiple thousands of params. Just interesting, where is this useful in the real world?

For example, let's say you wanted to list source files with the most lines.

git ls-files | xargs wc -l | grep -v ' total' | sort -h | tail -n10

If you have 5k files, that is 5k arguments to wc.

(Granted, this particular case uses positional args.....but the point is thousands of args is not unusual.)

pauldraper avatar Oct 06 '25 02:10 pauldraper

Workaround, custom action.

const { Action } = require("argparse");

class AppendAction extends Action {
    constructor(options) {
        super(options);
        this.default = options.default;
    }
    call(_, namespace, values) {
        let items = namespace[this.dest];
        if (items === this.default) {
            items = this.default ? [...this.default] : [];
            namespace[this.dest] = items;
        }
        items.push(values);
    }
}

pauldraper avatar Oct 06 '25 20:10 pauldraper