Runtime that makes Dart tick

by Slava Egorov

[email protected]

I am

       a SWE

              on the Dart team

I am

       a SWE TLM

              on the Dart team

I am

       a DREAMER

              on the Dart team

Disclaimer

Nobody have seen or reviewed this talk. The content of this talk is aspirational and does not necessarily reflect plans of the Dart or Flutter teams.

LEGO® System in Play

  1. The toy has to be compact in its dimensions without limiting the free expression of imagination.
  2. It has to be reasonably priced.
  3. It has to be simple and durable and yet offer unlimited variety.
  4. It has to be suitable for children of all ages and for both boys and girls.
  5. It has to be classic in its presentation, i.e. a classic among toys, needing no renewal.
  6. It has to be easily distributed.

Dart SDK

is a construction built out of smaller pieces

Why bother?


                        // Which one is faster?

                        // Classic indexed loop.
                        for (var i = 0; i < list.length; i++) {
                            final e = list[i];
                        }

                        // or this Dart 3.0 beauty?
                        for (var (i, e) in list.indexed) {
                            // ...
                        }
                    

                        // Which one is faster? (On native)

                        // Classic indexed loop.
                        for (var i = 0; i < list.length; i++) {
                            final e = list[i];
                        }

                        // or this Dart 3.0 beauty?
                        for (var (i, e) in list.indexed) {
                            // ...
                        }
                    

Easy to measure


                        $ dart compile exe -o loops loops.dart
                        $ ./loops

                        $ flutter run --release -t lib/loops.dart
                    

but we are not here for easy

How does it work?


                        import 'dart:isolate';

                        Isolate.spawnUri('my-aot-snapshot.aot');
                    

                        #include "dart_api.h"

                        Dart_Initialize(...);  // Init runtime system
                        isolate = Dart_CreateIsolateGroup(
                            // ...
                            "main",  // isolate name
                            snapshot_data,
                            snapshot_code,
                            // ...
                        );
                    

Practice time

Dart SDK uses GN + Ninja

but today I am using Bazel


                        $ bazel build :gen_kernel    \
                                      :platform.dill \
                                      :gen_snapshot  \
                                      :run_aot
                    

                        $ bazel-bin/gen_kernel --aot           \
                            --platform bazel-bin/platform.dill \
                            -o /tmp/loops.dill                 \
                            dart/loops.dart
                        $ bazel-bin/gen_snapshot               \
                            --snapshot-kind=app-aot-assembly   \
                            --snapshot=/tmp/loops.S            \
                            /tmp/loops.dill
                        $ gcc -shared -o /tmp/loops.dylib /tmp/loops.S
                        $ bazel-bin/run_aot /tmp/loops.dylib
                    

                        $ bazel-bin/run_aot /tmp/loops.dylib
                        ClassicLoop(RunTime): 63.10348942208462 us.
                        ForInIndexedLoop(RunTime): 819.2325 us.
                    

AOT compiler can generate DWARF debug info


                        $ bazel-bin/gen_snapshot               \
                            --dwarf-stack-traces               \
                            --resolve-dwarf-paths              \
                            ...
                    

AOT compiler can print its IL


                        $ bazel-bin/gen_snapshot               \
                            --print-flow-graph-optimized       \
                            --print-flow-graph-filter=...      \
                            --disassemble-optimized            \
                            ...
                    

Problem #1

Not enough inlining ⇒ not enough specialization


                        B1:
                            v2  ← Parameter(0)
                            v5  ← LoadField(v2 . list)
                            v63 ← StaticCall( IndexedIterable. v65, v5)
                            v7  ← StaticCall( get:iterator v63)
                            v49 ← LoadField(v7 . _source)
                            v89 ← LoadField(v49 . _iterable)
                        // ...
                        B3:
                            v98 ← DispatchTableCall( Iterable.elementAt, v89, v94)
                    

Solution #1

Use @pragma('vm:prefer-inline')


                        // In sdk/lib/internal/iterable.dart
                        class IndexedIterable<T> extends Iterable<(int, T)> {
                            // ...
                            @pragma('vm:prefer-inline')
                            factory IndexedIterable(Iterable<T> source, int start) {
                                // ...
                            }
                            // ...
                            @pragma('vm:prefer-inline')
                            Iterator<(int, T)> get iterator => // ...
                            }
                    

                        $ bazel-bin/run_aot /tmp/loops.dylib
                        ClassicLoop(RunTime): 62.95430732508757 us.
                        ForInIndexedLoop(RunTime): 249.33367062990135 us
                    

ForInIndexedLoop improved by 3x

Problem #2

Compiler did not inline int.operator&


                        B32:
                            v20 ← StaticCall( & v242, v292, recognized_kind = Integer_bitAnd)
                    

Solution #2

Compiler got confused about the type of e


                        for (var (i, int e) in list.indexed) {
                            // ...
                        }
                    

Solution #2

Fix the compiler!

  • Force inline recognized arithmetic methods
  • Fix a bug in the code that was supposed to figure out that LoadIndexed(...) from a List<int> returns int

                        $ bazel-bin/run_aot /tmp/loops.dylib
                        ClassicLoop(RunTime): 62.71734375 us.
                        ForInIndexedLoop(RunTime): 109.20825648258652 us.
                    

ForInIndexedLoop improved by 2.5x

Problem #3

Unfused and redundant comparison


                            v44 ← RelationalOp(>=, v42, v263)
                            Branch if StrictCompare(===, v44, v10) goto (37, 18)

                        B37:
                            Branch if RelationalOp(>=, v256, v235) goto (19, 3)

                        B3:
                            Branch if StrictCompare(===, v44, v10) goto (32, 11)
                    

Solution #3

Fix the compiler!

  • Propagate x === true and x === false into blocks which comparison dominates

                        $ bazel-bin/run_aot /tmp/loops.dylib
                        ClassicLoop(RunTime): 62.81853125 us.
                        ForInIndexedLoop(RunTime): 93.52465990369782 us.
                    

ForInIndexedLoop improved by 15%

Problem #4

Left over iteration variable, redundant comparison and a bounds check


                        final l = list.length;
                        for (var i = 0, j = -1;
                                ++j >= 0 && i < l;
                                i++) {
                            if (i >= l) throw OutOfBoundsError();
                            final e = list[i];
                            result ^= i & e;
                        }
                    

Solution #4 (TODO)

Need to fix issues with range analysis

Much bigger project than other fixes

What else
could we do?


                        struct HelloWorldView: View {
                          @State private var name: String = ""

                          var body: some View {
                            VStack(alignment: .leading) {
                              TextField("Your name?", text: $name)
                              Text("Hello, \(name)")
                            }
                          }
                        }
                    

                        struct HelloWorldView: View {
                          @State private var name: String = ""

                          var body: some View {
                            VStack(alignment: .leading) {
                              TextField("Your name?", text: $name)
                              Text("\(askDart(name))")
                            }
                          }
                        }
                    

                        func askDart(_ input : String) -> String {
                          // ???
                        }
                    

                      func askDart(_ input : String) -> String {
                        let args : [Dart_Handle] = [
                          Dart_NewStringFromCString(input)
                        ]
                        let result =
                            args.withUnsafeBufferPointer { args in
                          return Dart_Invoke(
                            Dart_RootLibrary(),
                            Dart_NewStringFromCString("askDart"),
                            1,
                            args
                          )
                        }
                        // ...
                      }
                    

😨 way too complicated

dart:ffi?

Accessing any C ABI code from Dart is simple


                      import 'dart:ffi';

                      typedef CString = Pointer<Utf8>;

                      @Native<CString Function(CString input)>()
                      external CString askSwift(CString input);
                    

What if it was just as simple to export?


                      // hello.dart
                      @ffi.Export()
                      String askDart(String input) {
                        return 'Dart says: Hi, $input';
                      }
                    

                        // HelloWorldView.swift
                        import Hello
                        // ...
                        Text("\(Hello.askDart(input: name))")
                    

How???


                        // dart-sdk/pkg/vm/lib/transformations/ffi/export.dart
                        void transformLibraries(...) {
                            // ...
                            mainLibrary.addProcedure(Procedure(
                              Name('#ffiExports'),
                              ProcedureKind.Method,
                              FunctionNode(
                                ReturnStatement(StaticInvocation(
                                  allocateExports,
                                  Arguments([
                                    ListLiteral(elements,
                                        typeArgument: transformer.pointerVoidType),
                                    ConstantExpression(
                                      (callocField.initializer as ConstantExpression).constant),
                                    ]),
                                  )),
                                  returnType: coreTypes.intNonNullableRawType),
                              isStatic: true,
                              fileUri: mainLibrary.fileUri,
                            ));
                        }
                    

                        // Injected in Dart
                        CString _exported#askDart(CString input) =>
                          askDart(input.toDartString()).toNativeUtf8();

                        @pragma('vm:ffi:exports-list', ...)
                        @pragma('vm:entry-point')
                        int #ffiExports() => ffi._allocateExports([
                            Pointer<...>.fromFunction(_exported#askDart)
                          ]);
                    

                        // In C
                        struct hello_Exports {
                          char* (*askDart)(const char*);
                          char* (*makeView)();
                        };

                        char* hello_askDart(const char* input) {
                          return dart::embedder::
                              Exports<hello_Exports>()->askDart(input);
                        }
                    

IMAGINE


                        // In Swift
                        DynamicView(factory: Hello.makeView)
                    

                        // In Dart
                        import 'swiftui.dart' as swiftui;

                        @ffi.Export()
                        swiftui.View makeView() => swiftui.VStack([
                              swiftui.Text('Hello, World!'),
                            ]).toSwift();
                    

Not limited to Swift / iOS of course

There were some
smoke and mirrors
involved

Dart concurrency model

VS

Native concurrency model

I am

       a DREAMER

              on the Dart team

I dream about Dart

being everywhere.

I dream about Dart

being where you need it.

Dart has all the bricks,

You have the imagination!

Dream big!