How to write a variadic function like printf

More concretely: I want to implement a dprintf function that works like this

let dprintf fmt = 
  let flag = true in
  if flag then
    Printf.eprintf fmt
  else
    ???

What do I write instead of ??? to make dprintf a no-op if flag = false? I tried Printf.ifprintf stderr fmt which sort of works? However godbolt shows that if ifprintf is far from being zero-cost, which is unacceptable in a high-performance program I’m writing. What I want is that if flag = false then dprintf <whatever> is (). I guess this would require a way to write variadic functions (much like *printf). Is there any way to do this?

If you really want absolute zero-cost I think the only way is to lift the if flag then .. test outside of the function at each calling site:

if flag then dprintf fmt ...;

You could write a tiny ppx to make this nicer (and probably this already exists) so that you can write

[%dprintf fmt ...]

Best wishes,
Nicolás

We are using the following tiny plugin to optimize debug logging in Why3:

I think this discussion could interest you: Format.kprintf usage (incl. the following link https://github.com/mirage/ocaml-git/pull/130#issuecomment-149326614 ).

Isn’t Printf.{ifprintf|ikfprintf} what you are really looking for?

Cf. https://caml.inria.fr/pub/docs/manual-ocaml/libref/Printf.html

To be clear, we do use Printf.ifprintf in Why3. But it is still way too slow, because the logging code is called in the critical loop of a bytecode interpreter. Moreover, logging is not decided at compile time; it can be enabled/disabled at runtime. That is why we are also using a compiler plugin, so that Printf.ifprintf is never called in the critical parts of the code, whether logging is enabled or not. This makes logging theoretically a bit slower, but branch prediction in the processor cancels this overhead.

I think it might be interesting to go in the opposite direction: making a non-variadic version of fprintf makes it easier to skip all the interpretation of the format string in the debug mode. A simple version of non-variadic fprintf can be made externally by grouping all arguments in a heterogeneous list:

type ('l, 'final) hlist =
  | []: ('a,'a) hlist
  | (::): 'x * ('l,'f) hlist -> ('x -> 'l, 'f) hlist

let rec apply: type f r. f -> (f,r) hlist -> r = fun f l ->
  match l with
  | [] -> f
  | a :: q -> apply (f a) q

let lprintf ppf fmt l = apply (Format.fprintf ppf fmt) l

With this encoding, we need to write

lprintf Format.std_formatter "%s=%d+%a" ["two"; 1; Format.pp_print_int;1]

rather than

Format.fprintf Format.std_formatter "%s=%d+%a"
  "two" 1 Format.pp_print_int 1

but the syntactic cost seems bearable. And it enable to write a debug function

let debug = ref false
let debugf ppf fmt l =
  if !debug then lprintf ppf fmt l else ()

where we are guarantee to not look at all at the format string when not in the debugging mode.

1 Like

If you want a slightly less mystical take on the issue, I suggest the approach used by @dbuenzli’s logs library (docs). It wraps the printing in a function which is only called when necessary:

Logs.err (fun m -> m "invalid kv (%a,%a)" pp_key k pp_val v);

That approach has been benchmarked very thoroughly to ensure it has minimal overhead when logging is disabled, which should suit your needs.

5 Likes

But syntactically speaking, this solution is not very cute/terse.

Logging is almost the archetypic motivating example for macros. B/c it gets a good bit more complicated than just “check Debug.debug and if it’s true, then log”. What you really want is some sort of set of bits, and each log-line (or at least, different sets of log-lines) are enabled by different bits. And you want the code to check that, to involve no function-calls, no jumps, just loads, arithmetic, as few indirect loads as possible, and finally a branch.

You want such log-lines, when disabled, to be as low-cost as possible. This necessarily means they’re going to have some ugly code, and that’s what the macro will hide from programmer view.

Yeah, I should just use cppo maybe.

Well, I wouldn’t go that far. Writing a high-quality upgrade to bolt (logging) that is driven by PPX extensions, is on my list of stuff to do. It would need to do all the stuff that glog does, of course. That’s a “meets min” qualification.

1 Like

Dear Chet, you can also consider contributing to my 190 lines of OCaml code for logging:


By the way, I forgot to mention that you were acknowledged in this paper, along with a certain Oleg:
https://www.researchgate.net/publication/340611168_Ranking_Molecules_with_Vanishing_Kernels_and_a_Single_Parameter_Active_Applicability_Domain_Included