From 7087e86658469c30f1b3b3595cdeae5b7c7e4e8a Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Fri, 13 Aug 2021 14:17:56 +0200 Subject: [PATCH] Optimise field access by computing the minimal positional index (#2708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formerly we did a linear search from field № 0 until finding the right hash. But frequently the _positional index_ where we find the hash can be given a static lower bound: - when we have a supertype approximation of the actual memory object, we can sort the field hashes and anticipate (some of) the layout - then statically search for the accessed field's hash to obtain the lower bound for starting the linear search. The only drawback I see is that some (generated template) code is duplicated, by getting new functions for each lower bound. So basically this is a tradeoff. Maybe this optimisation should reside behind a performance flag. Code duplication also means that we potentially get more cache-cold code on the nodes, but I think that is not reflected in our cost model. Next move: generate stubs `` that call a common worker. -------- ## Background On the surface (Motoko) you index fields by name (which is by hash value in the machine model). Under the hood we have two _parallel arrays_: `[field_hashes_sorted]` and `[field_values]`. See the ASCII art below `module Object` (in `compile.ml`). Example: ``` | field | hash | +-------+------+ | foo | 789 | | bar | 613 | | baz | 915 | ``` So when you want to access field `(o : { foo : Int, bar : Int, baz : Int}).foo` with `o` in reality having more fields potentially than the (super)type indicates, you'll find that hash array will be `[613, .., 789, .., 915]` because of sorting. We statically know that the lowest _positional index_ for `foo` is 1, since hash `613` already occupies position 0. --- src/codegen/compile.ml | 45 +++++++++++++++++++++++++++++------------- test/run/idx.mo | 31 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 test/run/idx.mo diff --git a/src/codegen/compile.ml b/src/codegen/compile.ml index aa52de00eab..82866c4c3ae 100644 --- a/src/codegen/compile.ml +++ b/src/codegen/compile.ml @@ -604,12 +604,12 @@ let compile_while cond body = cond ^^ G.if_ [] (body ^^ G.i (Br (nr 1l))) G.nop ) -(* Expects a number on the stack. Iterates from zero to below that number. *) -let from_0_to_n env mk_body = +(* Expects a number n on the stack. Iterates from m to below that number. *) +let from_m_to_n env m mk_body = let (set_n, get_n) = new_local env "n" in let (set_i, get_i) = new_local env "i" in set_n ^^ - compile_unboxed_zero ^^ + compile_unboxed_const m ^^ set_i ^^ compile_while @@ -624,6 +624,8 @@ let from_0_to_n env mk_body = set_i ) +(* Expects a number on the stack. Iterates from zero to below that number. *) +let from_0_to_n env mk_body = from_m_to_n env 0l mk_body (* Pointer reference and dereference *) @@ -2706,17 +2708,18 @@ module Object = struct (* Returns a pointer to the object field (without following the indirection) *) - let idx_hash_raw env = - Func.share_code2 env "obj_idx" (("x", I32Type), ("hash", I32Type)) [I32Type] (fun env get_x get_hash -> + let idx_hash_raw env low_bound = + let name = Printf.sprintf "obj_idx<%d>" low_bound in + Func.share_code2 env name (("x", I32Type), ("hash", I32Type)) [I32Type] (fun env get_x get_hash -> let (set_h_ptr, get_h_ptr) = new_local env "h_ptr" in get_x ^^ Heap.load_field hash_ptr_field ^^ set_h_ptr ^^ get_x ^^ Heap.load_field size_field ^^ (* Linearly scan through the fields (binary search can come later) *) - from_0_to_n env (fun get_i -> + from_m_to_n env (Int32.of_int low_bound) (fun get_i -> get_i ^^ - compile_mul_const Heap.word_size ^^ + compile_mul_const Heap.word_size ^^ get_h_ptr ^^ G.i (Binary (Wasm.Values.I32 I32Op.Add)) ^^ Heap.load_field 0l ^^ @@ -2735,36 +2738,50 @@ module Object = struct ) (* Returns a pointer to the object field (possibly following the indirection) *) - let idx_hash env indirect = + let idx_hash env low_bound indirect = if indirect - then Func.share_code2 env "obj_idx_ind" (("x", I32Type), ("hash", I32Type)) [I32Type] (fun env get_x get_hash -> + then + let name = Printf.sprintf "obj_idx_ind<%d>" low_bound in + Func.share_code2 env name (("x", I32Type), ("hash", I32Type)) [I32Type] (fun env get_x get_hash -> get_x ^^ get_hash ^^ - idx_hash_raw env ^^ + idx_hash_raw env low_bound ^^ load_ptr ^^ compile_add_const (Int32.mul MutBox.field Heap.word_size) ) - else idx_hash_raw env + else idx_hash_raw env low_bound (* Determines whether the field is mutable (and thus needs an indirection) *) let is_mut_field env obj_type s = let _, fields = Type.as_obj_sub [s] obj_type in Type.is_mut (Type.lookup_val_field s fields) + (* Computes a lower bound for the positional index of a field in an object *) + let field_lower_bound env obj_type s = + let open Type in + let _, fields = as_obj_sub [s] obj_type in + List.iter (function {typ = Typ _; _} -> assert false | _ -> ()) fields; + let sorted_by_hash = + List.sort + (fun (h1, _) (h2, _) -> Lib.Uint32.compare h1 h2) + (List.map (fun f -> Lib.Uint32.of_int32 (E.hash env f.lab), f) fields) in + match Lib.List.index_of s (List.map (fun (_, {lab; _}) -> lab) sorted_by_hash) with + | Some i -> i + | _ -> assert false + (* Returns a pointer to the object field (without following the indirection) *) let idx_raw env f = compile_unboxed_const (E.hash env f) ^^ - idx_hash_raw env + idx_hash_raw env 0 (* Returns a pointer to the object field (possibly following the indirection) *) let idx env obj_type f = compile_unboxed_const (E.hash env f) ^^ - idx_hash env (is_mut_field env obj_type f) + idx_hash env (field_lower_bound env obj_type f) (is_mut_field env obj_type f) (* load the value (or the mutbox) *) let load_idx_raw env f = idx_raw env f ^^ load_ptr - (* load the actual value (dereferencing the mutbox) *) let load_idx env obj_type f = idx env obj_type f ^^ diff --git a/test/run/idx.mo b/test/run/idx.mo new file mode 100644 index 00000000000..0ecd4b46ed5 --- /dev/null +++ b/test/run/idx.mo @@ -0,0 +1,31 @@ +class CO4() { + public type b = Nat; + public let a : b = 25; + public let foo : b = 8; + public let field : b = 42; + public let other : b = 83 +}; + +type O1 = { field : Int }; // "\ba\94\93\00" +type O2 = { a: Int; field : Int }; // "a\00\00\00\ba\94\93\00" +type O3 = { a: Int; field : Int; other : Int }; // "a\00\00\00\ba\94\93\00\d0fv6" +type O4 = { a: Int; foo : Int; field : Int; other : Int }; // "a\00\00\00\06\c7M\00\ba\94\93\00\d0fv6" + +func go1(o : O1) : () = inner o; +func go2(o : O2) : () = inner o; +func go3(o : O3) : () = inner o; +func go4(o : O4) : () { assert o.a == 25; assert o.field == 42; assert o.other == 83; assert o.foo == 8; inner o }; + +func inner(o : O1) { assert o.field == 42 }; + +func go() { + go1({ field = 42 }); // field: 9671866 + go2({ a = 25; field = 42 }); + go3({ a = 25; field = 42; other = 83 }); + go4({ a = 25; foo = 8; field = 42; other = 83 }); + let co4 = CO4(); + let a = co4.a; + go4(co4) +}; + +go();