mapAttrs
does not do a lookup immediately, but if f
uses the value:
argument, it has to be looked up eventually. My point was that the thunk attrs.${name}
, if unused in f
, is likely not forced, and therefore incurs very minimal overhead. No idea if that’s actually true though, it’s just a guess.
In my benchmark above, I did not use the value in f
. Let’s try and see what happens if we do.
Huge attrset
Nixpkgs Version
let
lib = (import <nixpkgs> {}).lib;
data = lib.genAttrs (lib.genList toString 1000000) (i: {a = i; b = i;});
in
lib.mapAttrsToList (k: v: {inherit k v;}) data
❯ hyperfine --warmup 2 'nix eval --json -f foo.nix > /dev/null'
Benchmark 1: nix eval --json -f foo.nix > /dev/null
Time (mean ± σ): 10.231 s ± 0.059 s [User: 4.931 s, System: 0.593 s]
Range (min … max): 10.160 s … 10.335 s 10 runs
Your Version
let
lib = (import <nixpkgs> {}).lib;
data = lib.genAttrs (lib.genList toString 1000000) (i: {a = i; b = i;});
mapAttrsToList = f: attrs: builtins.attrValues (builtins.mapAttrs f attrs);
in
mapAttrsToList (k: v: {inherit k v;}) data
❯ hyperfine --warmup 2 'nix eval --json -f foo.nix > /dev/null'
Benchmark 1: nix eval --json -f foo.nix > /dev/null
Time (mean ± σ): 8.106 s ± 2.543 s [User: 4.557 s, System: 0.573 s]
Range (min … max): 5.147 s … 10.259 s 10 runs
Many small attrsets
Nixpkgs Version
let
lib = (import <nixpkgs> {}).lib;
data = map (i: {a = toString i; b = toString i;}) (lib.genList toString 1000000);
in
map (lib.mapAttrsToList (k: v: {inherit k v;})) data
❯ hyperfine --warmup 2 'nix eval --json -f foo.nix > /dev/null'
Benchmark 1: nix eval --json -f foo.nix > /dev/null
Time (mean ± σ): 10.229 s ± 0.100 s [User: 4.961 s, System: 0.715 s]
Range (min … max): 10.078 s … 10.459 s 10 runs
Your Version
let
lib = (import <nixpkgs> {}).lib;
data = map (i: {a = toString i; b = toString i;}) (lib.genList toString 1000000);
mapAttrsToList = f: attrs: builtins.attrValues (builtins.mapAttrs f attrs);
in
map (mapAttrsToList (k: v: {inherit k v;})) data
❯ hyperfine --warmup 2 'nix eval --json -f foo.nix > /dev/null'
Benchmark 1: nix eval --json -f foo.nix > /dev/null
Time (mean ± σ): 10.050 s ± 0.639 s [User: 4.657 s, System: 0.704 s]
Range (min … max): 8.245 s … 10.388 s 10 runs
Hm, this shows higher variance but I’m not sure this is a conclusive benchmark. Maybe it really does one less lookup and the benchmark is just too small. To really be able to tell what is going on, someone who understands the Nix code base better than me should probably look into the implementation of mapAttrs
, attrValues
, and .${name}
.