Is it possible to help the optimizer?

I was playing a bit with code generation and I noticed an interesting behavior. Take the following simple code (partially stolen from this thread):

module Option = {
  let flatMapU = (x, f) => {
    switch x {
    | Some(x) => f(. x)
    | None => None
    }
  }
}

let reciprocal = (. x) => x == 0. ? None : Some(1. /. x)

let indirect = Option.flatMapU(Some(2.), reciprocal)
switch indirect {
| Some(x) => Js.Console.log(x)
| None => ()
}

let direct = switch Some(2.) {
| Some(x) => Some(reciprocal(. x))
| None => None
}
switch direct {
| Some(x) => Js.Console.log(x)
| None => ()
}

let immediate = Some(2.)
switch immediate {
| Some(x) => Js.Console.log(reciprocal(. x))
| None => ()
}

Nothing particularly exiting, just applying reciprocal to a value if it’s Some. FYI: using the re-implementation of Option.flatMapU produces the same result as using Belt. Here the esbuild bundle:

(() => {
  // node_modules/@rescript/std/lib/es6/caml_option.js
  function some(x2) {
    if (x2 === void 0) {
      return {
        BS_PRIVATE_NESTED_SOME_NONE: 0
      };
    } else if (x2 !== null && x2.BS_PRIVATE_NESTED_SOME_NONE !== void 0) {
      return {
        BS_PRIVATE_NESTED_SOME_NONE: x2.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0
      };
    } else {
      return x2;
    }
  }
  function valFromOption(x2) {
    if (!(x2 !== null && x2.BS_PRIVATE_NESTED_SOME_NONE !== void 0)) {
      return x2;
    }
    var depth = x2.BS_PRIVATE_NESTED_SOME_NONE;
    if (depth === 0) {
      return;
    } else {
      return {
        BS_PRIVATE_NESTED_SOME_NONE: depth - 1 | 0
      };
    }
  }

  // lib/es6/src/demo.js
  function flatMapU(x2, f) {
    if (x2 !== void 0) {
      return f(valFromOption(x2));
    }
  }
  var $$Option = {
    flatMapU
  };
  function reciprocal(x2) {
    if (x2 === 0) {
      return;
    } else {
      return 1 / x2;
    }
  }
  var indirect = flatMapU(2, reciprocal);
  if (indirect !== void 0) {
    console.log(indirect);
  }
  var x = 2;
  var direct = x !== void 0 ? some(reciprocal(x)) : void 0;
  if (direct !== void 0) {
    console.log(valFromOption(direct));
  }
  var immediate = 2;
  if (immediate !== void 0) {
    console.log(reciprocal(immediate));
  }
})();

As you can see, the bundler is able to see through valFromOption in case of immediate. However, in the other two cases it’s unable to optimize anything. The direct case is pretty bad, because instead of removing some, the branch and valFromOption, we have all the overhead.

Said that, this is probably more a topic for the optimizer than the rescript compiler, but I am wondering: is there something that could be done to help the optimizer? I am asking because a nice rescript codebase would use option (and result) all over the place, and even if the major performance issue are related to everything else (i.e. DOM manipulation), it would be nice if some low-hanging fruits could be available to have better codegen.

hi, I would suggest you stop worrying about such things, this is just a small function shared by all.

With regard to optimizations, if the compiler know the option type as a concrete type, it will be specialized, so better code generated. take direction ,
immediate for example, its type is known when type checking (option). You can annotate the option type to a concrete type for micro-optimizations

bundler does not do such optimizations to my best knowledge.

Note it is weird that $$Option is not removed, maybe you can file an issue to esbuild repo

Thanks for the reply!

I am not “worried”: I strongly believe that the gain of a strong type system is really greater than the small overhead introduced in the code (which is probably optimized away from the JIT).
In any case, as human beings, we can reason (no pun intended :blush:) about simple cases: if I see that for a micro-test that there are a set of operations that could be avoided, then it is a plausible opportunity for optimizations. If, for cases like this, nothing can be really done to improve, it is fine. But if the opportunity can be taken, then it’s right to do so, because at the very end very small performance gains sum up, and the resulting code could be significantly faster, even if by 1%.

I just realized that I messed up a thing without noticing (because Console.log takes virtually everything) – the right code for direct should be

let direct = switch Some(2.) {
| Some(x) => reciprocal(. x)
| None => None
}
switch direct {
| Some(x) => Js.Console.log(x)
| None => ()
}

without the inner Some. In this way the optimizer is able to do the right thing, which is very nice!

At this point the only case that involves overhead is when helper functions are used (flatMapU in this case). From what you are saying (and what I found around), it looks like there isn’t a common way to tell bundlers “hey, this function is pure and very small, try to inline it”, right?

Good catch! However this is my fault: I did not create an empty .resi file to make Option private. Doing that makes $$Option disappear. :blush: