8
$\begingroup$

I would like to compare performance of function encoded (Church's / Scott's) vs classically encoded (assembler / C) data structures.

But before I do that I need to know how efficient is / can be function representation in memory. The function can be of course partially applied (aka closure).

I am interested in both the current encoding algorithm popular functional languages (Haskell, ML) use and also in the most efficient one that can be achieved.


Bonus point : Is there such encoding that maps function encoded integers to native integers (short, int etc in C). Is it even possible?


I value efficiency based on performance. In other words the more efficient the encoding is the less it influences performance of computation with functional data structures.

$\endgroup$
  • $\begingroup$ All my Google attempts failed, maybe I don't know the right keywords. $\endgroup$ – Ford O. May 11 '17 at 19:00
  • $\begingroup$ Can you edit the question to clarify what you mean by "efficient"? Efficient for what? When you are asking for an efficient data structure, you need to specify what operations you want to be able to perform on the data structure, as that affects the choice of data structure. Or do you mean that the encoding is as space-efficient as possible? $\endgroup$ – D.W. May 12 '17 at 13:32
  • 1
    $\begingroup$ This is quite broad. There's plenty of abstract machines for lambda calculus which aim to execute it efficiently (see e.g. SECD, CAM, Krivine's, STG). On top of that, you need to consider Church/Scott encoded data, which poses more problems. E.g. in Church encoded lists, the tail operation has to be O(n) instead of O(1). I think I read somewhere that the existence of an encoding for lists in System F with O(1) head and tail operations was still an open issue. $\endgroup$ – chi May 12 '17 at 14:57
  • $\begingroup$ @D.W. I am talking about performance / overhead. For example with efficient encoding mapping over church's list and Haskell's list should take the same time. $\endgroup$ – Ford O. May 12 '17 at 19:44
  • $\begingroup$ Performance for what operation(s)? What do you want to do with the functions? Do you want to evaluate these functions on some value? Once, or evaluate the same function on many values? Do something else with them? Are you just asking how to compile a function (written in a functional language) so it can be executed as efficient as possible? $\endgroup$ – D.W. May 12 '17 at 20:26
10
$\begingroup$

The thing is, there's really not much leeway in terms of function encoding. Here are the main options:

  • Term rewriting: you store functions as their abstract syntax trees (or some encoding thereof. When you call a function, you manually traverse the syntax tree to replace its parameters with the argument. This is easy, but terribly inefficient in terms of time and space.

  • Closures: you have some way of representing a function, maybe a syntax tree, more likely machine code. And in these functions, you refer to your arguments by reference in some way. It could be a pointer-offset, it could be an integer or De Bruijn index, it could be a name. Then you represent a function as a closure: the function "instructions" (tree, code, etc.) paired with a data structure containing all free variables of the function. When a function is actually applied, it somehow knows how to look up the free variables in its data structure, using environments, pointer arithmetic, etc.

I'm sure there are other options, but these are the basic ones, and I suspect almost every other option will be a variant of or optimization of the basic closure structure.

So, in terms of performance, closures almost universally perform better than term rewriting. Of the variations, which is better? That depends heavily on your language and architecture, but I suspect the "machine-code with a struct containing free vars" is the most efficient. It has everything the function needs (instructions and values) and nothing more, and calling doesn't end up doing large term traversals.

I am interested in both the current encoding algorithm popular functional languages (Haskell, ML) use

I'm not an expert, but I'm 99% most ML flavours use some variation of the closures I describe, albeit with some optimizations likely. See this for a (possibly out of date) perspective.

Haskell does something a bit more complicated because of lazy evaluation: it uses Spineless Tagless Graph Rewriting.

and also in the most efficient one that can be achieved.

What is most efficient? There is no implementation that will be most efficient across all inputs, so you get implementations that are efficient on average, but each will excel in different scenarios. So there's no definite ranking of most or least efficient.

There's no magic here. To store a function, you need to store its free values somehow, otherwise you're encoding less information than the function itself has. Maybe you can optimize away some of the free values with partial evaluation but that's risky for performance, and you have to be careful to ensure that this always halts.

And, maybe you can use some sort of compression, or clever algorithm to gain space efficiency. But then you're either trading time for space, or you're in the situation where you've optimized for some cases and slowed down for others.

You can optimize for the common case, but what the common case is can change on the language, area of application, etc. The type of code that's fast for a video game (number crunching, tight loops with large input) is probably different than what's fast for a compiler (tree traversals, worklists, etc.).

Bonus point : Is there such encoding that maps function encoded integers to native integers (short, int etc in C). Is it even possible?

No, this is not possible. The problem is that the lambda calculus does not let you introspect terms. When a function takes an argument with the same type as a Church-numeral, it needs to be able to call it, without examining the exact definition of that numeral. That's the thing with Church encodings: the only thing you can do with them is call them, and you can simulate everything useful with this, but not without a cost.

More importantly, the integers occupy every possible binary encoding. So if lambdas were represented as their integers, you'd have no way of representing non-church-numberal lambdas! Or, you'd introduce a flag to denote whether a lambda is a numeral or not, but then any efficiency you want is probably gone out the window.

EDIT: Since writing this, I've become aware of a third option for implementing higher-order functions: defunctionalization. Here, every function call turns into a big switch statement, depending on which lambda abstraction was given as the function. The tradeoff here is that it's a whole program transformation: you can't compile parts separately and then link together this way, since you need to have the complete set of lambda abstractions ahead of time.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.