Question about tuples and memory

In the below code there are two functions that are doing the same thing. With a single argument “match function” there is some syntactic sugar with = function. You can use this with a tuple to shorten a function and it made me wonder if there were any performance penalties with this route.

I skimmed the article in Real World OCaml and it looks like tuples are compiled down into C arrays which would mean that they are heap allocated right? Unless you already have a tuple, wouldn’t you want to always opt for something like type_effectiveness over type_effectiveness_tuples?

type ptype = TNormal | TFire | TWater

type effectiveness  = ENormal | ENotVery | ESuper 

let effectiveness_multiplier : effectiveness  -> float = function
  | ENormal -> 1.0
  | ENotVery -> 0.5
  | ESuper -> 2.0

let type_effectiveness (user_type : ptype) (target_type : ptype) =
  match user_type, target_type with
    | (TFire, TFire) | (TWater, TWater) | (TFire, TWater) -> ENotVery
    | (TWater, TFire) -> ESuper
    | _ -> ENormal

let type_effectiveness_tuples : ptype * ptype -> effectiveness  = function 
  | (TFire, TFire) | (TWater, TWater) | (TFire, TWater) -> ENotVery
  | (TWater, TFire) -> ESuper
  | _ -> ENormal

Also in type_effectiveness is there a difference between:

  match user_type, target_type with
    | (TFire, TFire) | (TWater, TWater) | (TFire, TWater) -> ENotVery
    | (TWater, TFire) -> ESuper
    | _ -> ENormal
  match user_type, target_type with
    | TFire, TFire | TWater, TWater | TFire, TWater -> ENotVery
    | TWater, TFire -> ESuper
    | _ -> ENormal

I find 1 much easier to read.

My intuition is yes, there is slight performance penalty for making a copy into a tuple.
The compiler explorer seems to confirm my suspicion.

There is no difference between the two.

1 Like

I’m not sure exactly which question you are asking, and which question @crackcomm is answering with his code.

  1. match foo, bar with p1, p2 -> ... is syntactically equivalent to match (foo, bar) with (p1, p2) -> .... The code suggests that a tuple is constructed and then deconstructed, but the match compiler is in fact careful to compile this into a parallel match on the inputs, so there is no actual tuple construction there.

  2. let f1 x y = ... and let f2 (x, y) = ... have different semantics, but the compiler tries to hide any performance gap between the two. The second is a “tupled” function (all arguments are in a big tuple) that is a specifically recognized form, and calls with a direct tuple f2 (foo, bar) will in fact be compiled without a tuple creation again.

  3. Some forms of tuple arguments will still allocate a tuple on the heap. But note that due to the generational GC, short-lived tuples allocated on the heap are very cheap, almost as cheap as allocating on the stack. (In particular if the function is defined as let f (x, y) ... =, then the tuple is deconstructed on entry and becomes dead on arrival, so it is short-lived.) In some performance- or latency-critical scenarios people start worrying about not running the minor GC too often, and then they avoid short-lived allocations, but this is certainly not the typical OCaml programming style and not necessary to get good performance for OCaml programs.

3 Likes