Fast Algorithms for Dyck-CFL-Reachability with Applications to Alias

advertisement
Uncovering JavaScript Performance Code
Smells Relevant to Type Mutations
Xiao Xiao1, Shi Han2, Charles Zhang1, and Dongmei Zhang2


JS is a pivotal language for future
2

Web development:


Complex games or apps are implemented by JavaScript
Desktop/plugin development:
Win 8/10 widgets support JS
 Chrome and Firefox plugins are JS
 In fact, large portion of Firefox itself is written in JS


Limit use for developing mobile apps:

HTML5/JS based solution has much lower cross platform
development and maintenance cost
JavaScript is not fast enough
3

Variables are dynamically typed!


Cannot compile to fully native code ahead-of-time.
Modern JavaScript VMs use type-feedback based JIT:
Types in Type-feedback JIT
4

Type-feedback JIT works very well if the JavaScript
code is written like C code:
 Variable

types rarely change after warm up
But JavaScript programmers love the highly flexible
JavaScript features:
 Add/delete
fields to objects at any time in any order
 Create a new closure instance when use it
 Access arrays out of bound
Types in Type-feedback JIT
5

Many operations mutate types:
 Fields
addition/deletion, prototype change, assigning
closures to fields, and etc.


Programmers can unconsciously generate many
redundant types.
Redundant types can incur obscured performance
bugs
 Very
hard to debug
Motivating Example
6
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.
A question asked in V8 (the
JavaScript VM of Chrome)
forum:
};
5.
6.
}
7.
Foobar.prototype.runTest = function (N) {
for (var i = 0; i < N; ++i) {
8.
Why Line 13 is much slower
than Line 14 although they look
the same?
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
27ms
109ms
Motivating Example
7
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.

Method binding:


0x12345 is the address of the closure
instance assigned to “test”
Store in the type descriptor rather
than in the object instance
};
5.
6.
}
7.
Foobar.prototype.runTest = function (N) {
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
Type descriptor1:
Fields:
abc : type = integer
test : type = closure, value = 0x12345
Foobar
instance 1
Motivating Example
8
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.


};
5.
6.
}
7.
Foobar.prototype.runTest = function (N) {
“test” is bound to a different closure
instance, method binding is cancelled
A new type where “field” is not bound to
a particular closure instance is created
Type descriptor 2:
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
Fields:
abc : type = integer
test : type = closure, value = 0x67890
Foobar
instance 2
Motivating Example
9
Foobar
instance 1
@ Line 13

Line 13 and Line 14
actually create two types

This is of course not the
author’s intention
Foobar
instance 2
@ Line 14
Type descriptor1:
Type descriptor 2:
Fields:
abc : type = integer
test : type = closure, value = 0x12345
Fields:
abc : type = integer
test : type = closure, value = 0x67890
Motivating Example
10
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.
};
5.
6.
}
7.
Foobar.prototype.runTest = function (N) {
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
// 27ms
14.
(new Foobar()).runTest(N);
// 109ms
Function inlining is
disabled because it
becomes a polymorphic
callsite.
(this.test is resolved to
different function
instances).
Motivating Example
11
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {

“runTest” is deoptimized when Type2 kicks in.
};
5.
7.
“runTest” is optimized against Type1;
this.abc = n;
4.
6.

}
Foobar.prototype.runTest =
function (N) {
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
There is a not-so-short “execute & collect
types” loop between the first and second
optimization of “runTest”.
Redundant types and performance bugs
12

The performance murder is the unwanted Type2
 Type2
is called redundant type

But how possibly a regular programmer understand
what happened inside a VM?

We need a tool to help programmers diagnose the
performance bugs introduced by redundant types
Issues caused by redundant types
13

Trigger Deoptimization


Trigger Inline Cache Fallback


e.g. function inline is precluded
Enter Dictionary Mode


Runtime is always called when an IC slots are saturated
Reduce Optimization Strength


A disaster if hot functions cannot run with optimized code
Object or array degrade to hash table
Increase GC pressure

Frequently create and drop small objects or closures
How to trigger type mutations
14
6 buggy code patterns to generate
redundant types.
We extract solutions to fix relevant
bugs from the forum discussion.
Utilize the buggy code patterns
15

Detect performance bugs by pattern recognition
 Find

code places that exhibit buggy patterns
Question:
 What
object should we run the pattern recognition
against? Source code? AST tree? Machine code?
Type Evolution Graph
16
A type evolution graph captures the type transitions for all the object instances
created by the same constructor.
Add field:
test : 0x12345
function Foobar() {
this.abc = 1;
Type1
this.test = function (n) {
Add field:
abc : integer
this.abc = n;
};
}
var N = 10000000;
(new Foobar()).runTest(N);
(new Foobar()).runTest(N);
new FooBar
Type0
Type2
Type Evolution Graph
17
A type evolution graph captures the type transitions for all the object instances
with the same constructor.
Add field:
test : 0x12345
function Foobar() {
this.abc = 1;
Type1
this.test = function (n) {
Add field:
abc : integer
this.abc = n;
};
}
Type2
new FooBar
Type0
var N = 10000000;
(new Foobar()).runTest(N);
(new Foobar()).runTest(N);
new FooBar
Add field:
test : closure
Type3
JSweeter Overview
18
JavaScript
Code
Run with an
instrumented VM
Log File
Action 1
Action 2
Action 3
………
Refactoring Suggestions
addF f1
fPromote f2
fOrder f3, f4
ckIntOF f5
ckStore f6
Reconstruct
Type Evolution
Graph
Recognize patterns
6 Bug Patterns
Generate
suggestions
Always Use New Closure
Inconsistent Field Ordering
Partially Initialized objects
……………
Pattern recognition and refactoring
generation
19

Three steps:
 Select
redundant type candidates
 Map the type evolution history of the candidate to one
of the six buggy patterns
 Suggest
the prescribed refactoring solutions to
developers
Identify redundant types
20

Redundant type candidates:
 The
types that cause function deoptimizations are
valuable to look at
JSweeter collects three pieces of information for each
deoptimization
1. ic: The IC site that triggers deoptimization
2. t: The type that causes type miss at the IC site
3. T1, . . . , TK: The types collected at the IC
Pattern Recognition
21
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.
};
5.
6.
7.
FooBar
Type Evolution Graph
Add field:
abc : integer
}
Foobar.prototype.runTest =
function (N) {
Type1
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
Add field:
test : 0x12345
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
Type2
Has type
Pattern Recognition
22
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.
7.
Add field:
abc : integer
};
5.
6.
FooBar
Type Evolution Graph
}
Foobar.prototype.runTest =
function (N) {
Type1
Add field:
test : closure
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
Add field:
test : 0x12345
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
14.
(new Foobar()).runTest(N);
Type3
Type2
Has type
Type3 is a redundant
type candidate!
Pattern Recognition
23
FooBar
Type Evolution Graph
Add field:
abc : integer
Type3 deoptimized
function “runTest”, which
anticipates Type2.
Type1
Add field:
test : closure
Type3
Add field:
test : 0x12345
Type2
We extract and study the
subgraph that rooted at
the common ancestor
(Type1) of Type2 and
Type3.
Pattern Recognition
24
FooBar
Type Evolution Graph
Add field:
abc : integer
Type1
Add field:
test : closure
Type3
Add field:
test : 0x12345
Type2
Path Type1  Typ2:
test : 0x12345
Path Type1  Typ3:
test : closure (not bound to
specific closure instance anymore)
Same field assigned to different
instances of the same closure.
Report to user:
A potential “Always Use New
Closure” problem.
Suggest refactoring
25
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
this.abc = n;
4.
};
5.
6.
Refactoring suggestion generated:
Promote field “test” to the
prototype of Foobar.
}
1.
function Foobar() {
2.
this.abc = 1;
3.
}
4.
Foobar.prototype.test = function (n) {
this.abc = n;
5.
6.
};
Complete JSweeter solution
26

In our paper:
 Empirical
study discussion
 Refactoring approaches for all bug patterns
 Details of the logged events
 Details of pattern recognition algorithm and
refactoring suggestions generation
Evaluation
27
Apply JSweeter to Octane benchmark suite:
crypto
splay
box2d
gbemu
typescript
pdfjs
Total/Avg
# Total Issues
4
1
8
12
18
3
46
# Fixed Issues
3
1
3
5
5
2
19
Speedup
3.5%
23.0%
3.8%
3.8%
4.1%
3.4%
5.3%

27 unfixed bug reports:
 Unable
 need
to understand 21 bug reports:
calling contexts and call graph information
 Unable
to fix 6 bug reports:
 violate
the semantics with only simple code edits
Case Study: splay.js
28
Splay.js is a splay tree data structure implementation. The aim of Splay.js is to test the
GC performance of JavaScript VM.
1.
SplayTree.prototype.insert = Function (key, value)
{
2.
var node = new SplayTree.Node(key, value);
3.
if (key > this.root_.key) {
4.
node.left = this.root_;
Fields left and right added in
5.
node.right = this.root_.right;
different order create two types for
6.
} else {
node, which introduce many
7.
node.right = this.root_;
polymorphic inline cache sites and
8.
node.left = this.root_.left;
cause deoptimizations!
9.
}
10. };
1. SplayTree.prototype.remove = function(key) {
2.
if (!this.root_.left)
3.
this.root_ = this.root_.right;
4. }
Case Study: gbemu.js
29
gbemu.js is a Gameboy emulator, which tests the computational and memory efficiency
of JavaScript Virtual Machine.
Refactoring suggestion:
1. this.LINECONTROL[line] =
2. function (parentObj) {
3. if (parentObj.LCDTicks<80) {
4.
// … some code
5. }
6. }
Contributes 90.4% (163 in total )
deoptimizations for enclosing
closure, which is a large and hot
function responsible for
rendering screen.
Isolate “parentObj.LCDTicks“ to other code.
1.
this.LINECONTROL[line] = entry0;
2.
3.
4.
5.
6.
function entry0(parentObj) {
var ticks = parentObj.LCDTicks;
processLT(ticks, parentObj);
}
7. function processLT(ticks, parentObj) {
8.
if (ticks < 80)
9.
// … some code
10. }
Conclusions
30

The performance of type-feedback VM is sensitive
to type mutations
 It’s


unknown the sensitivity can be eliminated
It’s useful for programmers to write code that cater
to the VM design sweet spot
We can create some tools to aid developers finding
buggy code places and guide the refactoring
Thank you
31
Q&A
32
Backup Slides
Compare to related works
33
JITPerf @ FSE 2015
Optimization Coaching @
ECOOP 2015
Type collection by inline cache
34

Inline cache (IC):

1.
2.
3.
4.
5.
6.
7.
A type guard weaved into code as a fast path for
processing particular type of input
function test(a, b) {
c = a + b;
return c;
}
test("foo", "bar");
test(1, 2);
1. function test(a, b) {
2.
c = runtime_plus(a, b);
3.
return c;
4. }
1. function test(a, b) {
2.
if (is_str(a) && is_str(b))
3.
c = strcat(a, b);
4.
else
5.
c = runtime_plus(a, b);
6.
return c;
7. }
Code optimization
35

Optimized code is compiled against collected types
by inline cache:
 Deoptimize
if new types are encountered
1. function test(a, b) {
2. c = a + b;
3. return c;
4. }
5. test("foo", "bar");
6. test(1, 2);
assert(is_str(a) && is_str(b))
mov eax, a
mov ebx, b
call strcat
Integer type triggers assertion failure
and exits the optimized code
Code deoptimization
36
assert(is_str(a) && is_str(b))
mov eax, a
mov ebx, b
call strcat


Deoptimize
mov eax, a
mov ebx, b
call runtime_plus

Generic code is much slower
than optimized code usually
Deoptimization is inevitable, but
unnecessary deoptimizations
should be avoided
In reality, programmers often
unintentionally create redundant
types and trigger unnecessary
deoptimization
Motivating Example
37
1.
function Foobar() {
2.
this.abc = 1;
3.
this.test = function (n) {
Polymorphic inline cache:
encodes two type guards for
Type1 and Type2
this.abc = n;
4.
};
5.

6.
}
7.
Foobar.prototype.runTest = function (N) {
for (var i = 0; i < N; ++i) {
8.
this.test(i);
9.
}
10.
11.
};
12.
var N = 10000000;
13.
(new Foobar()).runTest(N);
// 27ms
14.
(new Foobar()).runTest(N);
// 109ms
Inline cache (IC):

A type guard weaved into code as
a fast path for processing particular
type of input
1. function test(n) {
2.
if (this points to Type1)
3.
this.abc = n
4.
else if (this points to Type2)
5.
this.abc = n
6.
else
7.
runtime_update(this, abc, n);
8. }
Case study: V8 report 2673
38
Although length() is called in a loop for many times, v8
never optimize it, because length instances are only
used once and quickly dropped.
Case study: FF report 813425
39
This report says:
A function is deoptimized 114 times due to a loop that adds fields
to objects created at the same allocation site in random order.
Download