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

Static Hermes (React Native EU 2023 Announcement)

Tzvetan Mikov
September 09, 2023

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

Other Decks in Programming

Transcript

  1. Static Hermes
    Tzvetan Mikov, Meta

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. View Slide

  6. 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!

    View Slide

  7. 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

    View Slide

  8. 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.

    View Slide

  9. 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()

    View Slide

  10. 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.

    View Slide

  11. 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?

    View Slide

  12. 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.

    View Slide

  13. 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.

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

  16. 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.

    View Slide

  17. Compiling Typed JS to
    Native

    View Slide

  18. 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

    View Slide

  19. 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;
    }

    View Slide

  20. 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]

    View Slide

  21. 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]

    View Slide

  22. 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]

    View Slide

  23. More Benchmarks

    View Slide

  24. Zero-cost FFI
    Unsafe native
    extensions
    Soundly typed
    JavaScript

    View Slide

  25. 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

    View Slide

  26. 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.

    View Slide

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

    View Slide

  28. 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

    View Slide

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

    View Slide

  30. 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);
    }

    View Slide

  31. It actually works!

    View Slide

  32. 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"));

    View Slide

  33. 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.

    View Slide

  34. 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

    View Slide

  35. 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.

    View Slide

  36. 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.

    View Slide

  37. 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.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. 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.

    View Slide

  44. 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

    View Slide