Live data from Hacker News

Why SSA?

mcyoung.xyz

61–70 of 106 posts

Re: Why SSA?

#61
post #10

Forget compilers, SSA is an immensely valuable readability improvement for humans, too.

Why have while (c when you could have %2 = alloca i32, align 4 %3 = alloca i32, align 4 store i32 %0, ptr %3, align 4 br label %4, !dbg !18 4: %5 = load i32, ptr %3, align 4, !dbg !19 %6 = icmp slt i32 %5, 10, !dbg !20 br i1 %6, label %7, label %10, !dbg !18 7: %8 = load i32, ptr %3, align 4, !dbg !21 %9 = mul nsw i32 %8, 3, !dbg !21 store i32 %9, ptr %3, align 4, !dbg !21 br label %4, !dbg !18

Try mem2reg on that to get rid of the loads and stores.

Re: Why SSA?

#62

SSA makes me think of a few interesting points: Considering it's a functional language (bar memory access bits), and most procedural languages can target this, we can say that a lot of procedural code can be compiled down to functional code - so procedural programming is syntactic sugar on top of a functional framework Also functional programmers have a couple of pet peeves - tail recursion to implement infinite recu…

The transition from one basic block to another is to copy the live values to some common location, jump the instruction counter, then copy the values out of that location to give the results of the phi nodes. The transition from one function to another is to copy the arguments to some common location, jump the instruction counter, then copy the values our of that location to give the initial values of the parameters.…

I see your point - tail calls are identical to MLIR-style phi nodes, where not the branch source determines the value, but it's passed explicitly as a function argument.

I still think that tail recursion is too low level a construct for functional programmers to interact with, but it's wrapped into something like a reduce operation, which allows to write an identical fibonacci impl, as you would in a procedural language.

Re: Why SSA?

#63

Every time I see a clean SSA explainer like this, I’m reminded that the “simplicity” of SSA only exists because we’ve decided mutation is evil. It’s not that SSA is simpler — it’s that we’ve engineered our entire optimization pipeline around pretending state doesn’t exist. It’s a brilliant illusion that works… until you hit aliasing, memory models, or concurrency, and suddenly the beautiful DAG collapses into a pile…

What a ridiculous comment. No one says mutation is “evil.” It's just harder to optimise. Then all this talk of pretending and illusions, as if the compiler doesn't really work and is producing fake outputs. I assure you that other people do not take your strangely moralising tone to compiler optimisations.

Re: Why SSA?

#64
post #10

Forget compilers, SSA is an immensely valuable readability improvement for humans, too.

Why have while (c when you could have %2 = alloca i32, align 4 %3 = alloca i32, align 4 store i32 %0, ptr %3, align 4 br label %4, !dbg !18 4: %5 = load i32, ptr %3, align 4, !dbg !19 %6 = icmp slt i32 %5, 10, !dbg !20 br i1 %6, label %7, label %10, !dbg !18 7: %8 = load i32, ptr %3, align 4, !dbg !21 %9 = mul nsw i32 %8, 3, !dbg !21 store i32 %9, ptr %3, align 4, !dbg !21 br label %4, !dbg !18

The second code snippet doesn't use SSA. It just translates the first loop into IR and mangles the variable names. Here is an SSA version of that in the Scheme language.

  (let loop ((c c)) (if (
Notice that this is stateless and also returns the final value of “c” from the loop. People who use the below style have tended to find that it is much easier to reason about for more complicated looping structures.

Re: Why SSA?

#65

Earlier quoted context omitted.

The transition from one basic block to another is to copy the live values to some common location, jump the instruction counter, then copy the values out of that location to give the results of the phi nodes. The transition from one function to another is to copy the arguments to some common location, jump the instruction counter, then copy the values our of that location to give the initial values of the parameters.…

I see your point - tail calls are identical to MLIR-style phi nodes, where not the branch source determines the value, but it's passed explicitly as a function argument. I still think that tail recursion is too low level a construct for functional programmers to interact with, but it's wrapped into something like a reduce operation, which allows to write an identical fibonacci impl, as you would in a procedural langu…

That "tail recursion" is a thing in it's own right is more a sign of people getting their implementation wrong than anything else.

Specifically, if calling a function at the end of your current function _doesn't_ clean up the call stack first, what you've got is a space leak. That C's ABI on various architectures gives you this behaviour is a bad thing. The space leak sucks there too. You get functions with a tailcall: label at the top and `goto tailcall;` written in the body as a workaround when functional programmers collide with this.

Related is RAII in C++ - implicitly calling things on leaving scope mostly means the function call in the "tail" position has to do more stuff after it returns, so it isn't in the tail position, and you burn memory on keeping track of the work still to do. This takes the initial mistake from C and really leans into it.

If your language doesn't make that implementation mistake, you just have function calls that work fine. Tree style recursion is still a problem, but it's an out of memory one and at least only one side of the tree is eating memory, and in proportion to the book keeping needed to find the other side of the tree.

In the olden days, _function calls_ in Fortran didn't work, because they put their local state in global memory, not on a stack. So if foo called bar called foo, broken. We don't tolerate that any more, everyone has a call stack. But we still tolerate "ah, let's just leak this stack frame for a while, we've always done it like that" at present.

Re: Why SSA?

#66

Earlier quoted context omitted.

I see your point - tail calls are identical to MLIR-style phi nodes, where not the branch source determines the value, but it's passed explicitly as a function argument. I still think that tail recursion is too low level a construct for functional programmers to interact with, but it's wrapped into something like a reduce operation, which allows to write an identical fibonacci impl, as you would in a procedural langu…

That "tail recursion" is a thing in it's own right is more a sign of people getting their implementation wrong than anything else. Specifically, if calling a function at the end of your current function _doesn't_ clean up the call stack first, what you've got is a space leak. That C's ABI on various architectures gives you this behaviour is a bad thing. The space leak sucks there too. You get functions with a tailcal…

If you want automatic implicit tail calls for literally everything, then you need a solution for

  {
    FooObject foo = FooObject(123);
    return foo.bar();
  }
ending up in a UAF when FooObject::bar() tries accessing the receiver "this". Or any other case of the tail function accessing a pointer to something the caller has put on the stack. Short of some kind of crazy dependency tracking (or shoving stuff onto the heap and using a GC), at the end of the day the programmer will have to explicitly work around the stack frame no longer existing in most return statements. To which the only workaround would be doing some circumlocutions specifically to avoid the tail-call recognition.

Re: Why SSA?

#67

Earlier quoted context omitted.

That "tail recursion" is a thing in it's own right is more a sign of people getting their implementation wrong than anything else. Specifically, if calling a function at the end of your current function _doesn't_ clean up the call stack first, what you've got is a space leak. That C's ABI on various architectures gives you this behaviour is a bad thing. The space leak sucks there too. You get functions with a tailcal…

If you want automatic implicit tail calls for literally everything, then you need a solution for { FooObject foo = FooObject(123); return foo.bar(); } ending up in a UAF when FooObject::bar() tries accessing the receiver "this". Or any other case of the tail function accessing a pointer to something the caller has put on the stack. Short of some kind of crazy dependency tracking (or shoving stuff onto the heap and us…

Good example. It's a little clearer if desugared:

    {
       FooObject foo = FooObject(123);
       return FooObject::bar(&foo);
    }
The foo object needs to be somewhere that the address of it means something, because C++ passes 'this' by pointer.

The answer to this is to look at the current stack frame, shuffle everything that needs to stay alive to one end of it, move the stack pointer to deallocate the rest and then jump to bar, where bar is now responsible for deallocating N bytes of stack before it continues onwards.

It's a pain, sure, but in the scheme of compiler backends it's fine. They do very similar things on optimisation grounds, e.g. "shrink wrapping" means holding off on allocating stack in case an early return fires and you don't need it after all.

Though, when memory use is a function of escape analysis, which is a function of how much time you gave the compiler to work with, I do start to empathise with the make-the-programmer-do-it as the solution.

Re: Why SSA?

#68
post #17

Earlier quoted context omitted.

Indeed a great book; I even have a paper copy. The SSA book is also pretty good: https://web.archive.org/web/20201111210448/https://ssabook.g...

I’ve found the SSA book to be... unforgiving in its difficulty. Not in the sense that I thought it to be a bad book but rather in that I was getting the feeling that a dilettante in compilers like me wasn’t the target audience.

I was involved in making the book. It is very much a book for academics, and came out of an academic conference bringing together people working at the forefront of SSA-based research.

Re: Why SSA?

#69
post #2

I like the style of the blog but a minor nit I'd change is have a definition what SSA is right at the top. It discusses SSA for quite a while "SSA is a property of intermediate representations (IRs)", "it's frequently used" and only 10 paragraphs down actually defines what SSA is > SSA stands for “static single assignment”, and was developed in the 80s as a way to enhance the existing three-argument code (where every…

I don't think it really explains what it's for though. Here's my explanation (from my PhD thesis on compilers):

Static single assignment (SSA) form provides an efficient intermediate representation for program analysis [Cytron et al., 1989, 1991]. It was introduced as a way of efficiently representing dataflow analyses.

SSA uses a single key idea: that all variables in the program are renamed so that each variable is assigned a value at a single unique statement in the program. From this key idea, SSA is able to provide a number of advantages over other techniques of dataflow analyses:

Factored use-def chain: With a def-use chain, dataflow results may be propagated directly from the assignment of a variable to all of its uses. However, a def-use chain requires an edge from each definition to each use, which may be expensive when a program has many definitions and uses of the same variable. In practice, this occurs in the presence of switch-statements. SSA factors the def-use chain over a φ-node, avoiding this pathological case.

Flow-sensitivity: A flow-insensitive algorithm performed on an SSA form is much more precise than if SSA form were not used. The flow-insensitive problem of multiple definitions to the same variable is solved by the single assignment property. The allows a flow-insensitive algorithm to approach the precision of a flow-sensitive algorithm.

Memory usage: Without SSA form, an analysis must store information for every variable at every program point. SSA form allows a sparse analysis, where an analysis must store information only for every assignment in the program. With a unique version per assignment, the memory usage of storing the results of an analysis can be considerably lower than using bit-vector or set-based approaches.

Re: Why SSA?

#70

Earlier quoted context omitted.

Local blocks with parameters is the gross way to do it. The right way to do it is Phi/Upsilon form. https://gist.github.com/pizlonator/cf1e72b8600b1437dda8153ea... But even if you used block arguments, it's so very different from a lambda. Lambdas allow dynamic creation of variables, while SSA doesn't. Therefore, in SSA, variables must-alias themselves, while in the lambda calculus they don't. If you think that a blo…

Phi/Upsilon is even more obviously equivalent to blocks with parameters than phi nodes were. It's storing to a "shadow variable" that you "load from" in the phi, i.e. it's exactly the same "store to ABI specified location in caller, load from it in callee" as a function call.

> Phi/Upsilon is even more obviously equivalent to blocks with parameters than phi nodes were

Then you don't understand Phi/Upsilon

Post reply on HN