Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Dense/Packed JavaScript arrays #2167

Closed
wants to merge 1 commit into from

Conversation

HalidOdat
Copy link
Member

@HalidOdat HalidOdat commented Jul 6, 2022

This PR implements an optimization done by V8 and spidermonkey. Which stores indexed properties in two class storage methods dense and sparse. The dense method stores the property in a contiguous array ( Vec<T> ) where the index is the property key. This storage method is the default. While on the other hand we have sparse storage method which stores them in a hash map with key u32 (like we currently do). This storage method is a backup and is slower to access because we have to do a hash map lookup, instead of an array access.

In the dense array we can store only data descriptors that have a value field and are writable, configurable and enumerable (which by default array elements are). Since all the fields of the property descriptor are the same except value field, we can omit them an just store the JsValues in Vec<JsValue> this decreases the memory consumption and because it is smaller we are less likely to have cache misses.

There are cases where we have to convert from dense to sparse (the slow case):

  • If we insert index elements in a non-incremental way, like array[1000] = 1000 (inserting an element to an index that is already occupied only replaces it does not make it sparse)
  • If we delete from the middle of the array (making a hole), like delete array[10] (only converts if there is actualy an element there, so calling delete on a non-existent index property will do nothing)

Once it becomes sparse is stays sparse there is no way to convert it again. (the computation needed to check whether it can be converted outweighs the benefits of this optimization)

I did some local benchmarks and on array creation/pop and push there is ~45% speed up and the impact should be bigger the more elements we have. For example the code below on main takes ~21.5s while with this optimization is ~3.5s (both on release build).

let array = [];
for (let i = 0; i < 50000; ++i) {
   array[i] = i;
}

In addition I also made Array::create_array_from_list do a direct setting of the properties (small deviation from spec but it should have the same behaviour), with this #2058 should be fixed, conversion from Vec to JsArray, not JsTypedArray for that I will work on next :)

@HalidOdat HalidOdat added performance Performance related changes and issues execution Issues or PRs related to code execution Internal Category for changelog labels Jul 6, 2022
@HalidOdat HalidOdat added this to the v0.16.0 milestone Jul 6, 2022
@HalidOdat HalidOdat marked this pull request as draft July 6, 2022 04:26
@HalidOdat HalidOdat linked an issue Jul 6, 2022 that may be closed by this pull request
@github-actions
Copy link

github-actions bot commented Jul 6, 2022

Test262 conformance changes

VM implementation

Test result main count PR count difference
Total 90,847 90,847 0
Passed 59,090 59,090 0
Ignored 14,012 14,012 0
Failed 17,745 17,745 0
Panics 0 0 0
Conformance 65.04% 65.04% 0.00%

@codecov
Copy link

codecov bot commented Jul 6, 2022

Codecov Report

Merging #2167 (f8bd366) into main (d0d7034) will decrease coverage by 0.04%.
The diff coverage is 37.80%.

❗ Current head f8bd366 differs from pull request most recent head 1615d5d. Consider uploading reports for the commit 1615d5d to get more accurate results

@@            Coverage Diff             @@
##             main    #2167      +/-   ##
==========================================
- Coverage   42.32%   42.28%   -0.05%     
==========================================
  Files         228      228              
  Lines       21047    21176     +129     
==========================================
+ Hits         8909     8954      +45     
- Misses      12138    12222      +84     
Impacted Files Coverage Δ
boa_engine/src/object/internal_methods/string.rs 36.17% <ø> (ø)
boa_engine/src/object/property_map.rs 27.49% <34.01%> (+7.32%) ⬆️
boa_engine/src/builtins/array/mod.rs 75.71% <50.00%> (-0.37%) ⬇️
boa_engine/src/object/mod.rs 20.99% <50.00%> (+0.20%) ⬆️
boa_engine/src/value/display.rs 78.65% <50.00%> (-0.90%) ⬇️
boa_engine/src/object/internal_methods/array.rs 78.18% <100.00%> (ø)
boa_engine/src/object/internal_methods/global.rs 31.25% <100.00%> (+1.78%) ⬆️
boa_engine/src/object/internal_methods/mod.rs 68.55% <100.00%> (+0.59%) ⬆️
boa_engine/src/value/mod.rs 51.98% <100.00%> (+0.36%) ⬆️
boa_engine/src/realm.rs 53.84% <0.00%> (-7.70%) ⬇️
... and 7 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d0d7034...1615d5d. Read the comment docs.

@HalidOdat HalidOdat marked this pull request as ready for review July 6, 2022 04:41
@HalidOdat HalidOdat added the run-benchmark Label used to run banchmarks on PRs label Jul 6, 2022
Copy link
Member

@jedel1043 jedel1043 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great optimization! I just have two small comments about some parts of the code

boa_engine/src/object/property_map.rs Outdated Show resolved Hide resolved
boa_engine/src/object/property_map.rs Outdated Show resolved Hide resolved
- Optimized `Array::create_array_from_list()`
Copy link
Member

@jedel1043 jedel1043 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@github-actions
Copy link

github-actions bot commented Jul 6, 2022

Benchmark for 7e7af8e

Click to view benchmark
Test Base PR %
Arithmetic operations (Compiler) 522.5±0.86ns 516.8±0.74ns -1.09%
Arithmetic operations (Execution) 528.9±3.38ns 528.2±0.50ns -0.13%
Arithmetic operations (Parser) 5.9±0.01µs 6.2±0.01µs +5.08%
Array access (Compiler) 1506.1±3.65ns 1483.6±7.73ns -1.49%
Array access (Execution) 8.6±0.04µs 8.2±0.02µs -4.65%
Array access (Parser) 14.0±0.06µs 14.1±0.03µs +0.71%
Array creation (Compiler) 2.3±0.00µs 2.5±0.02µs +8.70%
Array creation (Execution) 2.5±0.02ms 1200.0±5.38µs -52.00%
Array creation (Parser) 16.3±0.06µs 16.3±0.03µs 0.00%
Array pop (Compiler) 3.9±0.01µs 4.1±0.02µs +5.13%
Array pop (Execution) 1141.1±4.57µs 631.9±6.48µs -44.62%
Array pop (Parser) 150.8±0.26µs 152.7±0.17µs +1.26%
Boolean Object Access (Compiler) 1158.8±5.79ns 1159.8±3.35ns +0.09%
Boolean Object Access (Execution) 4.7±0.01µs 4.4±0.01µs -6.38%
Boolean Object Access (Parser) 16.7±0.05µs 16.5±0.09µs -1.20%
Clean js (Compiler) 5.0±0.01µs 5.1±0.03µs +2.00%
Clean js (Execution) 756.6±3.66µs 628.3±2.76µs -16.96%
Clean js (Parser) 35.0±0.17µs 35.0±0.06µs 0.00%
Create Realm 230.3±0.32ns 233.7±2.21ns +1.48%
Dynamic Object Property Access (Compiler) 1823.7±6.90ns 1808.3±9.27ns -0.84%
Dynamic Object Property Access (Execution) 5.5±0.01µs 5.3±0.02µs -3.64%
Dynamic Object Property Access (Parser) 12.7±0.06µs 12.8±0.08µs +0.79%
Fibonacci (Compiler) 2.8±0.01µs 2.8±0.03µs 0.00%
Fibonacci (Execution) 1448.0±3.46µs 1387.9±2.94µs -4.15%
Fibonacci (Parser) 19.0±0.05µs 19.1±0.06µs +0.53%
For loop (Compiler) 2.7±0.01µs 2.7±0.02µs 0.00%
For loop (Execution) 17.6±0.19µs 17.0±0.05µs -3.41%
For loop (Parser) 16.6±0.09µs 16.8±0.13µs +1.20%
Mini js (Compiler) 4.4±0.01µs 4.5±0.03µs +2.27%
Mini js (Execution) 709.6±5.33µs 579.9±3.04µs -18.28%
Mini js (Parser) 30.1±0.08µs 30.9±0.48µs +2.66%
Number Object Access (Compiler) 1090.1±3.40ns 1080.0±4.79ns -0.93%
Number Object Access (Execution) 3.6±0.02µs 3.4±0.01µs -5.56%
Number Object Access (Parser) 12.9±0.09µs 12.8±0.02µs -0.78%
Object Creation (Compiler) 1619.9±15.43ns 1565.9±4.41ns -3.33%
Object Creation (Execution) 5.3±0.02µs 5.1±0.02µs -3.77%
Object Creation (Parser) 11.3±0.02µs 11.2±0.04µs -0.88%
RegExp (Compiler) 1817.9±4.69ns 1866.7±22.08ns +2.68%
RegExp (Execution) 12.3±0.03µs 12.2±0.04µs -0.81%
RegExp (Parser) 12.3±0.03µs 12.1±0.03µs -1.63%
RegExp Creation (Compiler) 1625.0±8.96ns 1690.8±25.46ns +4.05%
RegExp Creation (Execution) 9.2±0.05µs 9.1±0.04µs -1.09%
RegExp Creation (Parser) 10.3±0.04µs 10.1±0.04µs -1.94%
RegExp Literal (Compiler) 1822.1±5.07ns 1852.7±17.56ns +1.68%
RegExp Literal (Execution) 12.3±0.03µs 12.2±0.03µs -0.81%
RegExp Literal (Parser) 9.7±0.03µs 9.7±0.02µs 0.00%
RegExp Literal Creation (Compiler) 1632.4±17.46ns 1712.6±26.78ns +4.91%
RegExp Literal Creation (Execution) 9.2±0.05µs 9.2±0.03µs 0.00%
RegExp Literal Creation (Parser) 7.8±0.09µs 7.6±0.01µs -2.56%
Static Object Property Access (Compiler) 1627.3±7.07ns 1612.8±5.59ns -0.89%
Static Object Property Access (Execution) 5.5±0.02µs 5.2±0.01µs -5.45%
Static Object Property Access (Parser) 12.0±0.14µs 12.0±0.20µs 0.00%
String Object Access (Compiler) 1470.4±6.23ns 1473.1±5.57ns +0.18%
String Object Access (Execution) 6.8±0.09µs 6.1±0.01µs -10.29%
String Object Access (Parser) 16.2±0.03µs 16.5±0.05µs +1.85%
String comparison (Compiler) 2.4±0.01µs 2.3±0.01µs -4.17%
String comparison (Execution) 4.9±0.02µs 4.8±0.02µs -2.04%
String comparison (Parser) 12.8±0.04µs 13.4±0.02µs +4.69%
String concatenation (Compiler) 1824.0±3.24ns 1792.8±7.73ns -1.71%
String concatenation (Execution) 4.6±0.03µs 4.5±0.04µs -2.17%
String concatenation (Parser) 9.0±0.03µs 9.2±0.03µs +2.22%
String copy (Compiler) 1492.4±4.11ns 1459.7±8.18ns -2.19%
String copy (Execution) 4.3±0.02µs 4.2±0.03µs -2.33%
String copy (Parser) 6.9±0.03µs 7.0±0.04µs +1.45%
Symbols (Compiler) 1121.6±3.77ns 1118.9±3.54ns -0.24%
Symbols (Execution) 4.5±0.02µs 4.3±0.02µs -4.44%
Symbols (Parser) 5.4±0.02µs 5.3±0.02µs -1.85%

@github-actions
Copy link

github-actions bot commented Jul 6, 2022

Benchmark for 3f569d2

Click to view benchmark
Test Base PR %
Arithmetic operations (Compiler) 695.9±43.27ns 692.9±31.62ns -0.43%
Arithmetic operations (Execution) 753.1±29.26ns 758.8±25.01ns +0.76%
Arithmetic operations (Parser) 7.0±0.22µs 7.1±0.28µs +1.43%
Array access (Compiler) 1947.0±141.98ns 1918.0±90.17ns -1.49%
Array access (Execution) 10.4±0.30µs 10.3±0.31µs -0.96%
Array access (Parser) 16.3±1.00µs 15.5±0.62µs -4.91%
Array creation (Compiler) 2.9±0.13µs 2.9±0.12µs 0.00%
Array creation (Execution) 2.8±0.10ms 1318.8±53.54µs -52.90%
Array creation (Parser) 18.9±0.73µs 18.1±0.79µs -4.23%
Array pop (Compiler) 5.0±0.19µs 5.0±0.19µs 0.00%
Array pop (Execution) 1348.6±61.66µs 693.0±19.03µs -48.61%
Array pop (Parser) 181.5±9.64µs 179.6±11.51µs -1.05%
Boolean Object Access (Compiler) 1432.8±57.18ns 1436.8±66.04ns +0.28%
Boolean Object Access (Execution) 5.6±0.23µs 5.5±0.38µs -1.79%
Boolean Object Access (Parser) 19.0±0.55µs 18.0±0.90µs -5.26%
Clean js (Compiler) 6.1±0.30µs 6.1±0.20µs 0.00%
Clean js (Execution) 874.2±27.01µs 738.0±28.64µs -15.58%
Clean js (Parser) 41.1±1.95µs 38.9±1.49µs -5.35%
Create Realm 276.1±10.94ns 286.0±13.37ns +3.59%
Dynamic Object Property Access (Compiler) 2.3±0.08µs 2.2±0.09µs -4.35%
Dynamic Object Property Access (Execution) 7.3±0.45µs 6.8±0.29µs -6.85%
Dynamic Object Property Access (Parser) 14.4±0.52µs 14.3±0.57µs -0.69%
Fibonacci (Compiler) 3.5±0.11µs 3.5±0.15µs 0.00%
Fibonacci (Execution) 1877.3±125.42µs 1745.3±71.29µs -7.03%
Fibonacci (Parser) 22.1±1.08µs 21.7±0.88µs -1.81%
For loop (Compiler) 3.4±0.26µs 3.2±0.15µs -5.88%
For loop (Execution) 23.0±1.95µs 21.8±0.70µs -5.22%
For loop (Parser) 19.3±0.90µs 19.0±0.76µs -1.55%
Mini js (Compiler) 5.3±0.22µs 5.4±0.56µs +1.89%
Mini js (Execution) 802.3±26.45µs 668.2±26.13µs -16.71%
Mini js (Parser) 35.9±1.64µs 34.4±1.46µs -4.18%
Number Object Access (Compiler) 1362.7±48.35ns 1379.6±57.36ns +1.24%
Number Object Access (Execution) 4.3±0.14µs 4.1±0.20µs -4.65%
Number Object Access (Parser) 15.0±0.59µs 14.8±0.70µs -1.33%
Object Creation (Compiler) 2.0±0.07µs 1995.5±68.88ns -0.22%
Object Creation (Execution) 6.8±0.26µs 6.7±0.28µs -1.47%
Object Creation (Parser) 12.5±0.41µs 12.7±0.66µs +1.60%
RegExp (Compiler) 2.2±0.11µs 2.3±0.12µs +4.55%
RegExp (Execution) 16.3±0.79µs 15.8±0.79µs -3.07%
RegExp (Parser) 13.8±0.51µs 13.5±0.64µs -2.17%
RegExp Creation (Compiler) 2.1±0.10µs 2.0±0.06µs -4.76%
RegExp Creation (Execution) 11.7±0.63µs 11.4±0.51µs -2.56%
RegExp Creation (Parser) 11.5±0.51µs 11.3±0.81µs -1.74%
RegExp Literal (Compiler) 2.2±0.09µs 2.3±0.11µs +4.55%
RegExp Literal (Execution) 16.7±0.97µs 15.7±0.78µs -5.99%
RegExp Literal (Parser) 11.0±0.45µs 10.7±0.72µs -2.73%
RegExp Literal Creation (Compiler) 2.0±0.09µs 2.0±0.20µs 0.00%
RegExp Literal Creation (Execution) 11.8±0.45µs 11.7±0.41µs -0.85%
RegExp Literal Creation (Parser) 8.8±0.28µs 8.4±0.34µs -4.55%
Static Object Property Access (Compiler) 2.0±0.10µs 2.0±0.05µs 0.00%
Static Object Property Access (Execution) 7.0±0.27µs 6.7±0.25µs -4.29%
Static Object Property Access (Parser) 13.3±0.79µs 13.2±0.61µs -0.75%
String Object Access (Compiler) 1819.2±49.00ns 1758.3±77.04ns -3.35%
String Object Access (Execution) 8.4±0.35µs 7.9±0.33µs -5.95%
String Object Access (Parser) 18.7±1.23µs 18.1±1.22µs -3.21%
String comparison (Compiler) 2.9±0.11µs 2.9±0.13µs 0.00%
String comparison (Execution) 6.2±0.37µs 6.0±0.42µs -3.23%
String comparison (Parser) 14.9±0.79µs 14.5±0.43µs -2.68%
String concatenation (Compiler) 2.2±0.08µs 2.2±0.09µs 0.00%
String concatenation (Execution) 5.9±0.28µs 5.6±0.30µs -5.08%
String concatenation (Parser) 10.4±0.51µs 10.1±0.36µs -2.88%
String copy (Compiler) 1902.3±124.81ns 1892.7±92.00ns -0.50%
String copy (Execution) 5.7±0.21µs 5.4±0.24µs -5.26%
String copy (Parser) 7.8±0.42µs 7.7±0.28µs -1.28%
Symbols (Compiler) 1461.8±43.37ns 1454.5±80.74ns -0.50%
Symbols (Execution) 5.5±0.22µs 5.5±0.21µs 0.00%
Symbols (Parser) 6.0±0.26µs 5.9±0.36µs -1.67%

Copy link
Member

@raskad raskad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice change. Great work!

@raskad
Copy link
Member

raskad commented Jul 6, 2022

bors r+

bors bot pushed a commit that referenced this pull request Jul 6, 2022
This PR implements an optimization done by V8 and spidermonkey. Which stores indexed properties in two class storage methods dense and sparse. The dense method stores the property in a contiguous array ( `Vec<T>` ) where the index is the property key. This storage method is the default. While on the other hand we have sparse storage method which stores them in a hash map with key `u32` (like we currently do). This storage method is a backup and is slower to access because we have to do a hash map lookup, instead of an array access.

In the dense array we can store only data descriptors that have a value field and are `writable`, `configurable` and `enumerable` (which by default array elements are). Since all the fields of the property descriptor are the same except value field, we can omit them an just store the `JsValue`s in `Vec<JsValue>` this decreases the memory consumption and because it is smaller we are less likely to have cache misses.

There are cases where we have to convert from dense to sparse (the slow case):
- If we insert index elements in a non-incremental way, like `array[1000] = 1000` (inserting an element to an index that is already occupied only replaces it does not make it sparse)
- If we delete from the middle of the array (making a hole), like `delete array[10]`  (only converts if there is actualy an element there, so calling delete on a non-existent index property will do nothing)

Once it becomes sparse is *stays* sparse there is no way to convert it again. (the computation needed to check whether it can be converted outweighs the benefits of this optimization)

I did some local benchmarks and on array creation/pop and push there is ~45% speed up and the impact _should_ be bigger the more elements we have. For example the code below on `main` takes `~21.5s` while with this optimization is `~3.5s` (both on release build).

```js
let array = [];
for (let i = 0; i < 50000; ++i) {
   array[i] = i;
}
```

In addition I also made `Array::create_array_from_list` do a direct setting of the properties (small deviation from spec but it should have the same behaviour), with this #2058 should be fixed, conversion from `Vec` to `JsArray`, not `JsTypedArray` for that I will work on next :)
@bors
Copy link

bors bot commented Jul 6, 2022

Pull request successfully merged into main.

Build succeeded:

@bors bors bot changed the title Dense/Packed JavaScript arrays [Merged by Bors] - Dense/Packed JavaScript arrays Jul 6, 2022
@bors bors bot closed this Jul 6, 2022
@bors bors bot deleted the dense-arrays branch July 6, 2022 23:52
bors bot pushed a commit that referenced this pull request Aug 17, 2022
Currently we only spread spread-expressions if they are the last argument in the function call. With this fix all arguments are spread if needed. The downside is that an array object is allocated to store all arguments if the arguments contain a spread-expression. But with dense indexed properties inplemented in #2167 this should be reasonably fast.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
execution Issues or PRs related to code execution Internal Category for changelog performance Performance related changes and issues run-benchmark Label used to run banchmarks on PRs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

More efficient conversion of Vec to JsArray
3 participants