Skip to content

Commit

Permalink
Optimise field access by computing the minimal positional index (#2708)
Browse files Browse the repository at this point in the history
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 `<lower_bound, hash>` 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.
  • Loading branch information
ggreif authored Aug 13, 2021
1 parent 73c34e1 commit 7087e86
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 14 deletions.
45 changes: 31 additions & 14 deletions src/codegen/compile.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 *)

Expand Down Expand Up @@ -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 ^^
Expand All @@ -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 ^^
Expand Down
31 changes: 31 additions & 0 deletions test/run/idx.mo
Original file line number Diff line number Diff line change
@@ -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();

0 comments on commit 7087e86

Please sign in to comment.