From 0e3a704d661fad6cb786d469c7332d307a14f5bd Mon Sep 17 00:00:00 2001 From: Olivier Nicole Date: Fri, 8 Mar 2024 11:06:56 -0500 Subject: [PATCH] Have physical equality inspect Javascript objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently in wasm, physical equality behaves differently from in Javascript: Javascript objects are boxed, and non-equality of the pointers causes the values to be physically not equal. This can be an issue for some folks who rely on the JS-object-inspecting semantics of physical equality to implement things such as memoizing. This restores the fact that physical equality inspects Javascript objects. Other values are unaffected. This entails to pay an additional cost of between one and two Wasm runtime type tests in physical equality. A quick benchmark performing a few million `(==)` in an array of integers show a slowdown of 20~25 %. Since typical programs should perform physical equality much less frequently, we expect the overhead to be in the noise in practice. That being said, we may want to make this behaviour opt-in using a flag. Co-authored-by: Jérôme Vouillon --- compiler/lib/wasm/wa_asm_output.ml | 7 +++ compiler/lib/wasm/wa_ast.ml | 1 + compiler/lib/wasm/wa_code_generation.ml | 6 +- compiler/lib/wasm/wa_gc_target.ml | 69 +++++++++++++++++++++-- compiler/lib/wasm/wa_initialize_locals.ml | 4 ++ compiler/lib/wasm/wa_wat_output.ml | 8 +++ 6 files changed, 87 insertions(+), 8 deletions(-) diff --git a/compiler/lib/wasm/wa_asm_output.ml b/compiler/lib/wasm/wa_asm_output.ml index 76cb73ad8..42dfc32d1 100644 --- a/compiler/lib/wasm/wa_asm_output.ml +++ b/compiler/lib/wasm/wa_asm_output.ml @@ -301,6 +301,13 @@ module Output () = struct | MemoryGrow (mem, e) -> expression e ^^ line (string "memory.grow " ^^ integer mem) | Seq (l, e') -> concat_map instruction l ^^ expression e' | Pop _ -> empty + | IfExpr (ty, e, e1, e2) -> + expression e + ^^ line (string "if" ^^ block_type { params = []; result = [ ty ] }) + ^^ indent (expression e1) + ^^ line (string "else") + ^^ indent (expression e2) + ^^ line (string "end_if") | RefFunc _ | Call_ref _ | RefI31 _ diff --git a/compiler/lib/wasm/wa_ast.ml b/compiler/lib/wasm/wa_ast.ml index 193e74482..a5641e716 100644 --- a/compiler/lib/wasm/wa_ast.ml +++ b/compiler/lib/wasm/wa_ast.ml @@ -157,6 +157,7 @@ type expression = | ExternExternalize of expression | Br_on_cast of int * ref_type * ref_type * expression | Br_on_cast_fail of int * ref_type * ref_type * expression + | IfExpr of value_type * expression * expression * expression and instruction = | Drop of expression diff --git a/compiler/lib/wasm/wa_code_generation.ml b/compiler/lib/wasm/wa_code_generation.ml index 9fcdc5ec2..ab8a10849 100644 --- a/compiler/lib/wasm/wa_code_generation.ml +++ b/compiler/lib/wasm/wa_code_generation.ml @@ -347,11 +347,13 @@ let bin_op_is_smi (op : W.int_bin_op) = false | Eq | Ne | Lt _ | Gt _ | Le _ | Ge _ -> true -let is_smi e = +let rec is_smi e = match e with | W.Const (I32 i) -> Int32.equal (Int31.wrap i) i | UnOp ((I32 op | I64 op), _) -> un_op_is_smi op | BinOp ((I32 op | I64 op), _, _) -> bin_op_is_smi op + | I31Get (S, _) -> true + | I31Get (U, _) | Const (I64 _ | F32 _ | F64 _) | ConstSym _ | UnOp ((F32 _ | F64 _), _) @@ -373,7 +375,6 @@ let is_smi e = | RefFunc _ | Call_ref _ | RefI31 _ - | I31Get _ | ArrayNew _ | ArrayNewFixed _ | ArrayNewData _ @@ -388,6 +389,7 @@ let is_smi e = | Br_on_cast _ | Br_on_cast_fail _ -> false | BinOp ((F32 _ | F64 _), _, _) | RefTest _ | RefEq _ -> true + | IfExpr (_, _, ift, iff) -> is_smi ift && is_smi iff let get_i31_value x st = match st.instrs with diff --git a/compiler/lib/wasm/wa_gc_target.ml b/compiler/lib/wasm/wa_gc_target.ml index ec98462cc..60e07927b 100644 --- a/compiler/lib/wasm/wa_gc_target.ml +++ b/compiler/lib/wasm/wa_gc_target.ml @@ -435,15 +435,72 @@ module Value = struct let le = binop Arith.( <= ) - let eq i i' = + let ref_eq i i' = let* i = i in let* i' = i' in - val_int (return (W.RefEq (i, i'))) + return (W.RefEq (i, i')) - let neq i i' = - let* i = i in - let* i' = i' in - val_int (Arith.eqz (return (W.RefEq (i, i')))) + let ref ty = + { W.nullable = false; typ = Type ty } + + let ref_test typ e = + let* e = e in + return (W.RefTest (typ, e)) + + let caml_js_strict_equals x y = + let* x = x in + let* y = y in + let* f = + register_import + ~name:"caml_js_strict_equals" + ~import_module:"env" + (Fun { params = [ Type.value; Type.value ]; result = [ Type.value ] }) + in + return (W.Call (f, [ x; y ])) + + let if_expr ty cond ift iff = + let* cond = cond in + let* ift = ift in + let* iff = iff in + return (W.IfExpr (ty, cond, ift, iff)) + + let map f x = + let* x = x in + return (f x) + + let (>>|) x f = map f x + + let eq_gen ~negate x y = + let xv = Code.Var.fresh () in + let yv = Code.Var.fresh () in + let* js = Type.js_type in + let n = + if_expr + I32 + (* We mimic an "and" on the two conditions, but in a way that is nicer to the + binaryen optimizer. *) + (if_expr + I32 + (ref_test (ref js) (load xv)) + (ref_test (ref js) (load yv)) + (Arith.const 0l)) + (caml_js_strict_equals (load xv) (load yv) + >>| (fun e -> W.RefCast ({ nullable = false; typ = I31 }, e)) + >>| (fun e -> W.I31Get (S, e))) + (ref_eq (load xv) (load yv)) + in + seq + (let* () = store xv x in + let* () = store yv y in + return ()) + (val_int (if negate then Arith.eqz n else n)) + + + let eq x y = + eq_gen ~negate:false x y + + let neq x y = + eq_gen ~negate:true x y let ult = binop Arith.(ult) diff --git a/compiler/lib/wasm/wa_initialize_locals.ml b/compiler/lib/wasm/wa_initialize_locals.ml index 0f0931135..969a6fd23 100644 --- a/compiler/lib/wasm/wa_initialize_locals.ml +++ b/compiler/lib/wasm/wa_initialize_locals.ml @@ -51,6 +51,10 @@ let rec scan_expression ctx e = | Call (_, l) | ArrayNewFixed (_, l) | StructNew (_, l) -> scan_expressions ctx l | BlockExpr (_, l) -> scan_instructions ctx l | Seq (l, e') -> scan_instructions ctx (l @ [ Push e' ]) + | IfExpr (_, cond, e1, e2) -> + scan_expression ctx cond; + scan_expression (fork_context ctx) e1; + scan_expression (fork_context ctx) e2 and scan_expressions ctx l = List.iter ~f:(fun e -> scan_expression ctx e) l diff --git a/compiler/lib/wasm/wa_wat_output.ml b/compiler/lib/wasm/wa_wat_output.ml index 9c9e07579..7253974f1 100644 --- a/compiler/lib/wasm/wa_wat_output.ml +++ b/compiler/lib/wasm/wa_wat_output.ml @@ -331,6 +331,14 @@ let expression_or_instructions ctx in_function = ] | ExternInternalize e -> [ List (Atom "extern.internalize" :: expression e) ] | ExternExternalize e -> [ List (Atom "extern.externalize" :: expression e) ] + | IfExpr (ty, cond, ift, iff) -> + [ List + (Atom "if" + :: (block_type { params = []; result = [ ty ] }) + @ expression cond + @ [ List (Atom "then" :: expression ift) ] + @ [ List (Atom "else" :: expression iff) ]) + ] and instruction i = match i with | Drop e -> [ List (Atom "drop" :: expression e) ]