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.