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?
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
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.
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.
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.