How to understand negative arity?

Inspecting the runtime representation (compiled with ocamlopt) of

let triple_sum (x,y,z) = x + y + z 

I got the data below.

OCaml object: 0X0000563E4A1190E8
     is a block with header: 0X0000000000000FF7
     with number of fields : 3
                     color : 3
                       tag : 247
     (field 0) code pointer: 0X0000563E4A0D5190
       (field 1) code info : 0XFD00000000000007
                     arity : -3
                env offset : 3
(field 2) opt code pointer : 0X0000563E4A0D52D0

There are two things that puzzle me:

  1. The arity is minus three. The function takes a triple, why the arity is not one?
  2. Field 2 is code for total application. Again, the only argument is a triple and there is no chance of partially applying this function. Why this field is needed?
1 Like

OCaml does not distinguish between

let triple_sum (x,y,z) = x + y + z 

and

let triple_sum x y z = x + y + z 

It always uses the latter, more optimized version.

But when considered as a closure, the runtime has to do some extra work to reconcile both versions, since the argument will then be a triple. So, the generated code approximately looks as follows:

let triple_sum x y z = x + y + z 
let caml_tuplify3 f (x, y, z) = f x y z
let triple_sum_closure = (caml_tuplify3, (-3 | 3), triple_sum)

So, the first code pointer is in fact caml_tuplify3, while the second one is the actual function.

As to why the arity is a negative number, I am not quite sure. Perhaps it is just to be on the safe side, in case some code would be confused by this peculiar closure otherwise.

3 Likes

I think the negative arity is actually necessary. Take the following example:

let tuple_sum_with_more (x, y, z) =
  let sum = x + y + z in
  fun a b -> sum + a + b

(* Prevent inlining/simplification *)
let f = Sys.opaque_identity tuple_sum_with_more
let s = f (1, 2, 3) 4 5

If we had used 3 as the arity of the tupled function, then the last application of f would see a 3-argument application of a function of arity 3, and call the underlying function directly, with some unfortunate results.

On the other hand, if we used the real arity of 1, then the runtime wouldn’t know how many code pointer fields there are in the closure (it’s one code pointer for functions of arity 1 (or 0), and two otherwise, but the tupled functions need two code pointers). It’s less important since 4.12, as the arity field now also encodes information about the first environment slot so scanning the closure for GC doesn’t need to look at the actual arity, but I think there is still a bit of code in the compactor which relies on this property.

I believe any negative arity would work (it could be -1 instead of -3 in this example), but I guess there’s no harm in storing the additional information.

As a side note, Flambda doesn’t use the same compilation scheme and will never generate closures with negative arity. Instead, it generates a specialised version of the caml_tuplify function for each tupled function, which is then allocated in a regular closure of arity 1. Going back to your example, let triple_sum (x, y, z) = x + y + z is compiled to:

let triple_sum_direct x y z = x + y + z
let triple_sum (x, y, z) = triple_sum_direct x y z [@@inline always]

The same scheme could be used with Closure, but with a performance cost (the tuple allocation wouldn’t be removed anymore, and there are a few corner cases where the function would be inlined currently but fall outside the threshold with this version).

1 Like