views:

540

answers:

6

As a follow up to this question What are the advantages of built-in immutability of F# over C#?--am I correct in assuming that the F# compiler can make certain optimizations knowing that it's dealing with largely immutable code? I mean even if a developer writes "Functional C#" the compiler wouldn't know all of the immutability that the developer had tried to code in so that it couldn't make the same optimizations, right?

In general would the compiler of a functional language be able to make optimizations that would not be possible with an imperative language--even one written with as much immutability as possible?

+4  A: 

Yes, if you don't consider F#, but consider Haskell for instance. The fact that there are no side effects really opens up a lot of possibilities for optimization.

For instance consider in a C like language:

int factorial(int n) {
    if (n <= 0) return 1;
    return n* factorial(n-1);
}

int factorialuser(int m) {
    return factorial(m) * factorial(m);
}

If a corresponding method was written in Haskell, there would be no second call to factorial when you call factorialuser. It might be possible to do this in C#, but I doubt the current compilers do it, even for a simple example as this. As things get more complicated, it would be hard for C# compilers to optimize to the level Haskell can do.

Note, F# is not really a "pure" functional language, currently. So, I brought in Haskell (which is great!).

Moron
Tail recursion optimization is trivial, it is handled by the JIT compiler. Which couldn't care less about Haskell or functional languages. Can you come up with a better example?
Hans Passant
I am not talking about the optimization of factorial. I am talking about the optimization of factorialuser to make just one call to factorial. If tail recursion was my point, what is the use of writing out factorialuser and talking about it?
Moron
Trevor Tippins
@Trevor: Correct. This was just an example, though. There are other possibilities like reordering etc. There might even be opportunities to do runtime optmization (like memoization).
Moron
@nobugz: there is no tail recursion here (as Moron said).
leppie
+6  A: 

No.

The F# compiler makes no attempt to analyze the referential transparency of a method or lambda. The .NET BCL is simply not designed for this.

The F# language specification does reserve the keyword 'pure', so manually marking a method as pure may be possible in vNext, allowing more aggressive graph reduction of lambda-expressions.

However, if you use the either record or algebraic types, F# will create default comparison and equality operators, and provide copy semantics. Amongst many other benefits (pattern-matching, closed-world assumption) this reduces a significant burden!

DannyAsher
+3  A: 

Unfortunately, because F# is only mostly pure there aren't really that many opportunities for aggressive optimization. In fact, there are some places where F# "pessimizes" code compared to C# (e.g. making defensive copies of structs to prevent observable mutation). On the bright side, the compiler does a good job overall despite this, providing comparable performace to C# in most places nonetheless while simultaneously making programs easier to reason about.

kvb
That's cool. Didn't know F# worked that way.
Jared Updike
+2  A: 

Additional optimizations for functional languages are sometimes possible, but not necessarily because of immutability. Internally, many compilers will convert code into an SSA (single static assignment) form, where each local variable inside a function can only be assigned once. This can be done for both imperative and functional languages. For instance:

x := x + 1
y := x + 4

can become

x_1 := x_0 + 1
y := x_1 + 4

where x_0 and x_1 are different variable names. This vastly simplifies many transformations, since you can move bits of code around without worrying about what value they have at specific points in the program. This doesn't work for values stored in memory though (i.e., globals, heap values, arrays, etc). Again, this is done for both functional and imperative languages.

One benefit most functional languages provide is a strong type system. This allows the compiler to make assumptions that it wouldn't be able to otherwise. For instance, if you have two references of different types, the compiler knows that they cannot alias (point to the same thing). This is not an assumption a C compiler could ever make.

Jay Conrod
+1  A: 

I would say largely 'no'.

The main 'optimization' advantages you get from immutability or referential transparency are things like the ability to do 'common subexpression elimination' when you see code like ...f(x)...f(x).... But such analysis is hard to do without very precise information, and since F# runs on the .Net runtime and .Net has no way to mark methods as pure (effect-free), it requires a ton of built-in information and analysis to even try to do any of this.

On the other hand, in a language like Haskell (which mostly means 'Haskell', as there are few languages 'like Haskell' that anyone has heard of or uses :)) that is lazy and pure, the analysis is simpler (everything is pure, go nuts).

That said, such 'optimizations' can often interact badly with other useful aspects of the system (performance predictability, debugging, ...).

There are often stories of "a sufficiently smart compiler could do X", but my opinion is that the "sufficiently smart compiler" is, and always will be, a myth. If you want fast code, then write fast code; the compiler is not going to save you. If you want common subexpression elimination, then create a local variable (do it yourself).

This is mostly my opinion, and you're welcome to downvote or disagree (indeed I've heard 'multicore' suggested as a rising reason that potentially 'optimization may get sexy again', which sounds plausible on the face of it). But if you're ever hopeful about any compiler doing any non-trivial optimization (that is not supported by annotations in the source code), then be prepared to wait a long, long time for your hopes to be fulfilled.

Don't get me wrong - immutability is good, and is likely to help you write 'fast' code in many situations. But not because the compiler optimizes it - rather, because the code is easy to write, debug, get correct, parallelize, profile, and decide which are the most important bottlenecks to spend time on (possibly rewriting them mutably). If you want efficient code, use a development process that let you develop, test, and profile quickly.

Brian
Thanks Brian; that's exactly the sort of explanation I was hoping for. I've been assuming that because of more immutability the compiler would be able to make some optimizations that would not be possible in compiling imperative code. Obviously I was making a bad assumption.
Onorio Catenacci
But it IS an advantage. You can be lazy and write code as easily as it can be done with the knowledge that the compiler can do such optimizations trivially. I'd rather not write common subexpression myself in most cases.
trinithis
I think there's a gap as wide as an ocean between 'optimizations a compiler can do trivially' and 'optimizations your compiler does do'.
Brian
It should be noted that common subexpression elimination is *harder* in Haskell, not easier, because just eliminating common subexpressions can lead to space leaks. Detecting when CSE leads to space leaks is hard, whereas determining whether a function in an impure language is pure is simple (computing a good approximation to pure/impure is simple, that is).
Jules
+18  A: 

Am I correct in assuming that the F# compiler can make certain optimizations knowing that it's dealing with largely immutable code?

Unfortunately not. To a compiler writer, there's a huge difference between "largely immutable" and "immutable". Even guaranteed immutability is not that important to the optimizer; the main thing that it buys you is you can write a very aggressive inliner.

In general would the compiler of a functional language be able to make optimizations that would not be possible with an imperative language--even one written with as much immutability as possible?

Yes, but it's mostly a question of being able to apply the classic optimizations more easily, in more places. For example, immutability makes it much easier to apply common-subexpression elimination because immutability can guarantee you that contents of certain memory cells are not changed.

On the other hand, if your functional language is not just immutable but pure (no side effects like I/O), then you enable a new class of optimizations that involve rewriting source-level expressions to more efficient expressions. One of the most important and more interesting to read about is short-cut deforestation, which is a way to avoid allocating memory space for intermediate results. A good example to read about is stream fusion.

If you are compiling a statically typed, functional language for high performance, here are some of the main points of emphasis:

  • Use memory effectively. When you can, work with "unboxed" values, avoiding allocation and an extra level of indirection to the heap. Stream fusion in particular and other deforestation techniques are all very effective because they eliminate allocations.

  • Have a super-fast allocator, and amortize heap-exhaustion checks over multiple allocations.

  • Inline functions effectively. Especially, inline small functions across module boundaries.

  • Represent first-class functions efficiently, usually through closure conversion. Handle partially applied functions efficiently.

  • Don't overlook the classic scalar and loop optimizations. They made a huge difference to compilers like TIL and Objective Caml.

If you have a lazy functional language like Haskell or Clean, there are also a lot of specialized things to do with thunks.


Footnotes:

  • One interesting option you get with total immutability is more ability to execute very fine-grained parallelism. The end of this story has yet to be told.

  • Writing a good compiler for F# is harder than writing a typical compiler (if there is such a thing) because F# is so heavily constrained: it must do the functional things well, but it must also work effectively within the .NET framework, which was not designed with functional languages in mind. We owe a tip of the hat to Don Syme and his team for doing such a great job on a heavily constrained problem.

Norman Ramsey