Bug 246009 - Object.entries() is 1.5x slower in JSC compared to V8
Summary: Object.entries() is 1.5x slower in JSC compared to V8
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Yusuke Suzuki
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2022-10-04 00:28 PDT by Jarred Sumner
Modified: 2022-10-12 21:52 PDT (History)
3 users (show)

See Also:


Attachments
Launch_bun_2022-10-03_23.22.34_2576143F.trace.zip (895.90 KB, application/zip)
2022-10-04 00:28 PDT, Jarred Sumner
no flags Details
rollup.browser.js (387.09 KB, text/javascript)
2022-10-04 01:23 PDT, Jarred Sumner
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
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.