Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Static Hermes (React Native EU 2023 Announcement)

Static Hermes (React Native EU 2023 Announcement)

Static Hermes is the the next major version of Hermes, still under active development. It enables optional ahead-of-time native compilation of soundly typed JavaScript, using TypeScript or Flow type annotations.

Tzvetan Mikov

September 09, 2023
Tweet

Transcript

  1. Joe tried everything he could think of, but was thwarted

    by fundamental limits on interpreter performance
  2. He wondered, what if I could leverage type information to

    generate efficient native code ahead of time?
  3. 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!
  4. 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
  5. 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.
  6. 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()
  7. 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.
  8. 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?
  9. 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.
  10. 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.
  11. 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
  12. 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
  13. 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.
  14. 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
  15. 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; }
  16. 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]
  17. 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]
  18. 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]
  19. 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
  20. 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.
  21. Zero-cost FFI • No conversions, indirections, allocations • Just a

    C function call • Call overhead between 15x to 80x lower than JSI. Not percent, times!
  22. 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
  23. 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); }
  24. 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"));
  25. 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.
  26. 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
  27. 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.
  28. 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.
  29. 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.
  30. Anatomy of an Extern const _getenv = $SHBuiltin.extern_c({}, function getenv(name:

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

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

    c_ptr): c_ptr { throw 0; }); Type of the native parameter
  33. 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.
  34. 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