Slide 1

Slide 1 text

Static Hermes Tzvetan Mikov, Meta

Slide 2

Slide 2 text

The Fable of Sad Joe’s Slow Code Joe the RN developer

Slide 3

Slide 3 text

Joe tried everything he could think of, but was thwarted by fundamental limits on interpreter performance

Slide 4

Slide 4 text

He wondered, what if I could leverage type information to generate efficient native code ahead of time?

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

As part of RN’s cross- platform vision, we want to empower developers like Joe to not just create harmless JavaScript crashes, but REAL fast native crashes too!

Slide 7

Slide 7 text

Introducing Static Hermes ● Optional ahead of time compilation to native code ● C performance with JS usability ● Mix and match bytecode and native code based on your needs ● Requires sound types* for performance

Slide 8

Slide 8 text

Status/Disclaimer ● Static Hermes is not ready for general usage yet, but we have positive indications of performance ● This is a work in progress, and we are sharing it early to show you what we’ve been working on and gather community feedback ● We want to thank Amazon for adding TypeScript support to Static Hermes.

Slide 9

Slide 9 text

Table of Contents Challenges of AOT Native JS Compilation Sound Typing Reliable Perf via AOT Native Compilation Zero-cost FFI example: reading SQLite DB Zero-cost detailed FFI example: getenv()

Slide 10

Slide 10 text

Why Static Hermes? ● Hermes improves startup through bytecode, but there are limits on interpreter performance after startup ● Writing C++ to improve performance is difficult ● RN developers are already accustomed to writing typed JS code ● We want to take advantage of this to bridge predictable C performance with JS usability via native AOT compilation.

Slide 11

Slide 11 text

Why is it difficult to compile JS ahead-of-time? function addXY(x, y) { return x + y; } • We don’t know the type of x and y ahead of time. • All possible types (string, array, number, etc) and combinations need to be supported. • The result is either a size explosion or interpreter-like performance. • Solutions in academia, specifically HopC, by Manuel Serrano, do not meet our goal (not ruling it out in the future) And why is no one doing it?

Slide 12

Slide 12 text

Can’t type annotations help? function addXY(x: number, y: number) { return x + y; } • No! Because type annotations in TypeScript and Flow are unsound. • The type annotations do not guarantee that types are correct at runtime. • The compiler cannot rely on type annotations for better code.

Slide 13

Slide 13 text

Example of type unsoundness function addXY(x: number, y: number) { return x + y; } let a: number[] = [10, 20, 30]; // Uh-oh! Adding undefined + undefined! addXY(a[10], a[11]); Despite the type annotations, both parameters can be non- numbers at runtime.

Slide 14

Slide 14 text

Hermes + TypeScript/Flow Unsound Types function addXY(x: number, y: number) { return x + y; } let a: number[] = [10, 20, 30]; // Uh-oh! Adding undefined + undefined! addXY(a[10], a[11]); function addXY(x: number, y: number) { return x + y; } let a: number[] = [10, 20, 30]; // RangeError thrown at runtime! addXY(a[10], a[11]); Static Hermes + TypeScript/Flow Sound Types

Slide 15

Slide 15 text

Sound Types ● Static Hermes modifies some JS semantics to allow efficient sound typing ● The new semantics to improve performance are opt-in on a granular level ● New code can coexist and interop with unmodified code ● Both TypeScript and Flow are supported

Slide 16

Slide 16 text

Are we breaking JavaScript? ● We are enforcing the semantics the user wanted ● When they wrote x: number, they didn’t intend that x could also be undefined. ● Technically that was a bug which cannot be caught at compile time ● We are strengthening the existing TypeScript and Flow type systems by enforcing them at runtime. ● It should not make a difference for correct code. ● It is all opt-in. The standard behavior is available, if you want it.

Slide 17

Slide 17 text

Compiling Typed JS to Native

Slide 18

Slide 18 text

Interpreter vs typed native nbody.js Nbody is a well-known benchmark used in The Computer Language Benchmarks Game, simulating movement of celestial bodies. This is a math heavy benchmark with many property accesses. SH improves runtime from 5511 ms to 565 ms

Slide 19

Slide 19 text

A loop from nbody.js for (let i: number = 0; i < size; i++) { const body: Body = bodies[i]; body.x += dt * body.vx; body.y += dt * body.vy; body.z += dt * body.vz; }

Slide 20

Slide 20 text

A loop from nbody.js for (let i: number = 0; i < size; i++) { const body: Body = bodies[i]; body.x += dt * body.vx; body.y += dt * body.vy; body.z += dt * body.vz; } ; body.x += dt * body.vx; ; body.y += dt * body.vy; and x8, x0, #0xffffffffffff ldur q0, [x8, #40] ldr q1, [x8, #64] ldr q2, [sp, #16] fmla.2d v0, v1, v2[0] stur q0, [x8, #40]

Slide 21

Slide 21 text

A loop from nbody.js for (let i: number = 0; i < size; i++) { const body: Body = bodies[i]; body.x += dt * body.vx; body.y += dt * body.vy; body.z += dt * body.vz; } ; body.x += dt * body.vx; ; body.y += dt * body.vy; and x8, x0, #0xffffffffffff ldur q0, [x8, #40] ldr q1, [x8, #64] ldr q2, [sp, #16] fmla.2d v0, v1, v2[0] stur q0, [x8, #40] ; body.z += dt * body.vz; ldr d0, [x8, #56] ldr d1, [x8, #80] fmadd d0, d2, d1, d0 str d0, [x8, #56]

Slide 22

Slide 22 text

A loop from nbody.js for (let i: number = 0; i < size; i++) { const body: Body = bodies[i]; body.x += dt * body.vx; body.y += dt * body.vy; body.z += dt * body.vz; } ; body.x += dt * body.vx; ; body.y += dt * body.vy; and x8, x0, #0xffffffffffff ldur q0, [x8, #40] ldr q1, [x8, #64] ldr q2, [sp, #16] fmla.2d v0, v1, v2[0] stur q0, [x8, #40] ; body.z += dt * body.vz; ldr d0, [x8, #56] ldr d1, [x8, #80] fmadd d0, d2, d1, d0 str d0, [x8, #56]

Slide 23

Slide 23 text

More Benchmarks

Slide 24

Slide 24 text

Zero-cost FFI Unsafe native extensions Soundly typed JavaScript

Slide 25

Slide 25 text

A Surprising Benefit: Native Platform Integration Sound typing enables ● Zero-cost FFI ● Direct calling into native APIs ● Platform integration can be implemented entirely in JavaScript ● Safe/unsafe code distinction

Slide 26

Slide 26 text

Understanding the problem 01 Writing native extensions is complex and error prone. It requires switching between languages. 02 Calling existing native APIs requires writing JSI wrappers in C++. 03 Crossing JSI is not practical when we need high frequency interop with JS. The cost of JSI itself starts to dominate performance.

Slide 27

Slide 27 text

Zero-cost FFI ● No conversions, indirections, allocations ● Just a C function call ● Call overhead between 15x to 80x lower than JSI. Not percent, times!

Slide 28

Slide 28 text

Zero-cost FFI ● Native functions can be directly invoked from “unsafe” sections of JS code ● Explicit distinction between “safe” and “unsafe” code ● Native functions and native types are 1st class citizens of the language, enabling no-overhead compilation

Slide 29

Slide 29 text

Example 1: Reading SQLite DB Fortunately, this was generated automatically from a C header, using our tool ffigen.

Slide 30

Slide 30 text

Reading SQLite DB in a few lines of JS, no C++ required // Open the database res = _sqlite3_open( stringToAsciiz(”file.db"), dbPtrBuffer ); if (res !== 0) throw Error(res); db = _sh_ptr_read_ptr(dbPtrBuffer, 0); // Prepare the SQL statement sqlPtr = stringToAsciiz( "SELECT id, name FROM my_table” ); res = _sqlite3_prepare_v2( db, sqlPtr, -1, dbPtrBuffer, c_null ); if (res !== 0) throw Error(res); stmt = _sh_ptr_read_ptr(dbPtrBuffer, 0); // Loop through all rows in the table let row = 1; while (_sqlite3_step(stmt) === SQLITE_ROW) { const id = _sqlite3_column_int(stmt, 0); const namePtr = _sqlite3_column_text(stmt, 1); const name = asciizToString(namePtr, 1024); print(`row${row++}:`, id, name); }

Slide 31

Slide 31 text

It actually works!

Slide 32

Slide 32 text

Example 2: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH"));

Slide 33

Slide 33 text

Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Declaration of the native external function.

Slide 34

Slide 34 text

Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Convert the name from a JS string to a native C string

Slide 35

Slide 35 text

Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Call the native function.

Slide 36

Slide 36 text

Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Convert the result to a JS string.

Slide 37

Slide 37 text

Example: getenv const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr {throw 0;}); function getenv(name: string): string { "use unsafe"; let name_z = stringToAsciiz(name); try { let val_z = _getenv(name_z); return asciizToString_unsafe(val_z, 2048); } finally { free(name_z); } } print(getenv("PATH")); Free the native memory buffer.

Slide 38

Slide 38 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; });

Slide 39

Slide 39 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Options like calling convention

Slide 40

Slide 40 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Name of the imported native function.

Slide 41

Slide 41 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Type of the native parameter

Slide 42

Slide 42 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Native return type

Slide 43

Slide 43 text

Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name: c_ptr): c_ptr { throw 0; }); Throw away body to satisfy the type checker.

Slide 44

Slide 44 text

Thank you! ● For questions or suggestions, please use the Discussions tab on the Hermes GitHub repo. https://github.com/facebook/hermes/discussions/categories/static-hermes ● nbody.js: https://github.com/facebook/hermes/blob/static_h/benchmarks/nbody/fully-typed/nbody.js ● FFI: https://github.com/facebook/hermes/tree/static_h/examples/ffi