Bug 246009

Summary: Object.entries() is 1.5x slower in JSC compared to V8
Product: WebKit Reporter: Jarred Sumner <jarred>
Component: JavaScriptCoreAssignee: Yusuke Suzuki <ysuzuki>
Status: RESOLVED FIXED    
Severity: Normal CC: mark.lam, webkit-bug-importer, ysuzuki
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
Attachments:
Description Flags
Launch_bun_2022-10-03_23.22.34_2576143F.trace.zip
none
rollup.browser.js none

Description Jarred Sumner 2022-10-04 00:28:20 PDT
Created attachment 462781 [details]
Launch_bun_2022-10-03_23.22.34_2576143F.trace.zip

I've attached an Instruments trace and sampling profiler output of using Rollup.js to bundle the "@babel/standalone" npm package. End-to-end, it runs about 1.6x slower in JSC (bun) compared to V8 (node). Nearly all the time is spent in JSC. 

I think it's _mostly_ because of Object.entries() (which is why the issue is titled about Object.entries()). Rollup seems to use Object.entries() in their code a _lot_. In @babel/standalone's case, Object.entries() is called 550,000 times. For larger objects, Object.entries() is around 1.5x slower than in V8 (microbenchmark). It looks like ObjectKeys exists in DFGSpeculateJIT64.cpp but ObjectEntries and ObjectValues do not. Maybe adding it as a JIT operation would help? I don't know if that's the whole story though. It feels like there's a side effects tracking thing that would enable skipping some of the PropertyTable::find lookups, maybe? 

```
❯ bun /Users/jarred/Code/bun/bench/snippets/object-entries.mjs
cpu: Apple M1 Max
runtime: bun 0.1.14 (arm64-darwin)

benchmark                    time (avg)             (min … max)       p75       p99      p995
--------------------------------------------------------------- -----------------------------
Object.entries(26 keys)    1.27 µs/iter     (1.21 µs … 1.99 µs)   1.28 µs   1.99 µs   1.99 µs
Object.keys(26 keys)     515.77 ns/iter (477.57 ns … 628.68 ns) 526.63 ns 617.05 ns 628.68 ns
Object.entries(2 keys)   132.46 ns/iter (125.25 ns … 205.22 ns) 127.54 ns 194.48 ns 196.87 ns
Object.keys(2 keys)        4.68 ns/iter   (4.12 ns … 219.59 ns)   4.42 ns   7.48 ns  10.22 ns
```

```
❯ node /Users/jarred/Code/bun/bench/snippets/object-entries.mjs
cpu: Apple M1 Max
runtime: node v18.9.1 (arm64-darwin)

benchmark                    time (avg)             (min … max)       p75       p99      p995
--------------------------------------------------------------- -----------------------------
Object.entries(26 keys)  830.02 ns/iter   (803.19 ns … 1.07 µs) 821.85 ns   1.07 µs   1.07 µs
Object.keys(26 keys)     327.72 ns/iter (319.58 ns … 421.81 ns) 329.03 ns 400.85 ns 421.81 ns
Object.entries(2 keys)    98.28 ns/iter  (96.05 ns … 452.63 ns)  97.19 ns  105.5 ns 108.87 ns
Object.keys(2 keys)       13.36 ns/iter   (10.75 ns … 76.29 ns)   15.3 ns  17.92 ns  21.13 ns

```


-

Benchmark:

```
import { bench, run } from "../../node_modules/mitata/src/cli.mjs";

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
  e: 5,
  f: 6,
  g: 7,
  h: 8,
  i: 9,
  j: 10,
  k: 11,
  l: 12,
  m: 13,
  n: 14,
  o: 15,
  p: 16,
  q: 17,
  r: 18,
  s: 19,
  t: 20,
  u: 21,
  v: 22,
  w: 23,
  x: 24,
  y: 25,
  z: 26,
};

bench("Object.entries(26 keys)", () => {
  var k;
  for (let [key, value] of Object.entries(obj)) {
    value = key;
  }
  return k;
});

bench("Object.keys(26 keys)", () => {
  var k;
  for (let [key, value] of Object.keys(obj)) {
    value = key;
  }
  return k;
});

bench("Object.entries(2 keys)", () => {
  var k;
  for (let [key, value] of Object.entries({ a: 1, b: 2 })) {
    value = key;
  }
  return k;
});

bench("Object.keys(2 keys)", () => {
  var k;
  for (let item of Object.keys({ a: 1, b: 2 })) {
  }
  return k;
});

await run();
```

Sampling profiler output:

Sampling rate: 100.000000 microseconds. Total samples: 19077
Top functions as <numSamples  'functionName#hash:sourceID'>
  4028    'include#<nil>:5'
  1552    'parseNode#<nil>:5'
  1299    '#<nil>:5'
  1089    'entries#<nil>:4294967295'
   979    'includeCallArguments#<nil>:5'
   809    'create#<nil>:4294967295'
   656    'hasEffects#<nil>:5'
   606    'arrayIteratorNextHelper#<nil>:7'
   568    'NodeBase#<nil>:5'
   547    '#<nil>:4294967295'
   506    'bind#<nil>:5'
   495    'render#<nil>:5'
Sampling rate: 100.000000 microseconds. Total samples: 19077
Tier breakdown:
-----------------------------------
LLInt:                   184  (0.964512%)
Baseline:                935  (4.901190%)
DFG:                    3991  (20.920480%)
FTL:                   10469  (54.877601%)
js builtin:              862  (4.518530%)
Wasm:                      0  (0.000000%)
Host:                    559  (2.930230%)
RegExp:                  115  (0.602820%)
C/C++:                     0  (0.000000%)
Unknown Frame:             4  (0.020968%)
Unknown Executable:     2820  (14.782198%)
Hottest bytecodes as <numSamples   'functionName#hash:JITType:bytecodeIndex'>
  1089    'entries#<nil>:None:<nil>'
  1001    'include#<nil>:FTL:bc#33'
   809    'create#<nil>:None:<nil>'
   575    'parseNode#<nil>:FTL:bc#218'
   547    '#<nil>:None:<nil>'
   443    'parseNode#<nil>:FTL:bc#479'
   411    'Set#<nil>:None:<nil>'
   404    'include#<nil>:FTL:bc#107'
   363    'include#<nil>:FTL:bc#296'
   337    'include#<nil>:FTL:bc#93'
   280    'arrayIteratorNextHelper#<nil>:FTL:bc#122'
   272    'include#<nil>:FTL:bc#54'
   251    'arrayIteratorNextHelper#<nil>:FTL:bc#58'
   221    'includeCallArguments#<nil>:FTL:bc#59'
   206    'includeProperties#<nil>:FTL:bc#59'
   127    'Map#<nil>:None:<nil>'
   117    'getEntities#<nil>:FTL:bc#180'
   111    'include#<nil>:FTL:bc#0'
   107    'parseModule#<nil>:None:<nil>'
   106    'values#<nil>:None:<nil>'
   106    'parseNode#<nil>:FTL:bc#760'
    95    'includeCallArguments#<nil>:DFG:bc#201'
    90    'include#<nil>:FTL:bc#138'
    87    'getEntities#<nil>:FTL:bc#76'
    84    'includeCallArguments#<nil>:FTL:bc#0'
    81    'bind#<nil>:FTL:bc#262'
    81    'delete#<nil>:None:<nil>'
    81    'include#<nil>:FTL:bc#196'
    77    '#<nil>:FTL:bc#0'
    75    'createScope#<nil>:FTL:bc#11'
    73    'include#<nil>:FTL:bc#27'
    73    'include#<nil>:FTL:<nil>'
    72    '/^(?:break|case|catch|continue|debugger|default|do|else|finally|for|function|if|return|switch|throw|try|var|while|with|null|true|false|instanceof|typeof|void|delete|new|in|this|const|class|extends|export|import|super)$/#<nil>:RegExp:<nil>'
    72    'bind#<nil>:FTL:bc#65'
    71    'includeCallArguments#<nil>:FTL:<nil>'
    71    'includeProperties#<nil>:FTL:bc#19'
    70    'include#<nil>:DFG:bc#54'
    66    'includeProperties#<nil>:FTL:bc#70'
    65    'includeUnknownTest#<nil>:DFG:bc#22'
    65    'include#<nil>:FTL:bc#229'
Comment 1 Jarred Sumner 2022-10-04 01:23:40 PDT
Created attachment 462784 [details]
rollup.browser.js

Here is a build of Rollup.js that will run in JSC

If you run `jsc -m rollup.browser.js` with a file named "file.js" in the same cwd it should build file.js 

If you uncomment the last line it will print it to the terminal, but since we are not benchmarking printing to stdout from this function it is commented out
Comment 2 Yusuke Suzuki 2022-10-04 11:04:24 PDT
Note
1. We should apply Object.assign’s optimization
2. We should wire Object.entries with own-keys cache to reuse string cells.
Comment 3 Radar WebKit Bug Importer 2022-10-04 15:10:22 PDT
<rdar://problem/100783096>
Comment 4 Yusuke Suzuki 2022-10-11 23:58:28 PDT
Pull request: https://github.com/WebKit/WebKit/pull/5281
Comment 5 EWS 2022-10-12 21:52:07 PDT
Committed 255470@main (2b999503e689): <https://commits.webkit.org/255470@main>

Reviewed commits have been landed. Closing PR #5281 and removing active labels.