Agreed, I'm currently taking a similar approach, but forgoing pointers as much as possible as well. This leads to structs being passed by value, and const abuse.
Ray h_cast(const Hero hero, const Hit hit, const Sheer sheer, const int yres, const int xres)
{
const Point end = p_sub(hit.where, hero.where);
const Point corrected = p_turn(end, -hero.yaw);
const Line trace = { hero.where, hit.where };
const Projection projection = p_project(yres, xres, hero.fov.a.x, hero.pitch, corrected, hero.height);
const Ray ray = { trace, corrected, p_sheer(projection, sheer), hit.surface, hit.offset, hero.torch };
return ray;
}
It leads to some nice somewhat functional styles. I estimate its 80% pure, given its C99 alone. I've adopted this style in my Age of Empires 2 engine rewrite if anyone is curious: https://github.com/glouw/openempires
As for performance I am heavily relying on LTO. I seem to get a 50% performance boost with pass by values on structs - as I've sedded openempires to replace occurrences of _Type name_ with C++'s _Type& name_ for all functions arguments - and the added pointer aliasing and de-references bogged the engine down in heavily forested areas.
Compilers love singly assigned const values, and seem to suffer under heavy pointer aliasing.
If it's a static method (or in an anonymous namespace in C++), it should get fixed at any optimisation level. However if it's a global method, you'll need to pass -flto to defer optimisations to link time to make it work.
There's a lot to be said for real single-assignment programming. It provides many of the advantages of functional programming. The code is more readable, because the intermediate variables have names. When you need to do something imperative, you don't have to jump through hoops to do it. Go and Rust both encourage that style - the default is to create a new variable, not assign to an old one. (The semantics of multiple := assignment in Go, where some variables are assigned and others are created, though...)
One of the historical headaches of C is that function parameters are non-const by-value by default. That's backwards. The default should be const ref. If you need a local mutable copy, or the parameter is an "in-out", as Ada calls it, you should have to specify that. Those are the less likely cases and the ones that cause trouble if missed. Passing small const ref values by value is a compiler optimization; the programmer shouldn't have to worry about it, especially since whether it's a win varies with the target machine.
The language in the article seems more like a variant of Matlab, not C.
i recall a micro language built around the infamous brainfuck that has a non destructive read for function arguments. seems like a bigger concept exists. let the caller determine what the called can modify.
It does have some positive consequences for optimization. For example, if you "recycle" local variables by using them again for different purposes, you might think you're saving stack space by sharing the space. You're not. Optimizers are pretty good at storing multiple variables in the same stack slot when their usages (i.e. "live ranges") are disjoint.
On the other hand, if you write:
int i = value;
... use i ...
... lots of code ...
i = another value;
... use i ...
the optimizer may not be smart enough to split this into two distinct variables, unless it does the SSA optimization. Hence, you'll lose the optimization where in [... lots of code ...] i will not be occupying a register.
But just for code comprehension, it is better to use different variables for disjoint uses. A related point is to "shrink wrap" a variable into the smallest scope you can. The less you have to look over the rest of the function for uses, the better.
And lastly,
int x = value;
if (condition) x = another;
is just better written as:
const int x = condition ? value : another;
It reads better, and it enables x to be const, which is always better.
> Optimizers are pretty good at storing multiple variables in the same stack slot when their usages (i.e. "live ranges") are disjoint.
Exactly. Also the true lifetime of the variable might be way longer than what the programmer thinks due to compiler reordering the code to help CPU with dependency chain latency. (I've also seen plenty of cases where CPU out-of-order engine apparently completely fails, but (Intel) compiler saves the day.)
> int x = value;
> if (condition) x = another;
> is just better written as:
> const int x = condition ? value : another;
> It reads better, and it enables x to be const, which is always better.
While const sure is safer, the generated code should be same in both cases.
In this case I agree with you. But not if the ternary operator has any more complexity, because it quickly becomes hard to read. In my experience complicated ternary operators are rather bug prone, especially with large teams.
That said, I guess you agree with me your benchmark being "reads better". :-)
Go apparently didn't do SSA until 1.7. Then again it's not exactly an optimising compiler.
It's also somewhat common for functional language implementations to use CPS rather than SSA e.g. ghc does CPS conversion rather than SSA, and apparently didn't even used to do that: https://stackoverflow.com/a/8031177/8182118
This is interesting; I just investigated SAC for the first time about seven hours ago (I actually posted a license-fragment from it on my social media account: https://blob.cat/notice/9qMowPTjR9F7fjid3Q), finding it technically impressive but unfortunately proprietary.
Was there news about it, or something? It came to mind because something I was wanting to try depended on it, and it doesn't get posted to HN very often, so I'm really curious as to how you found it.
How is this "single assignment"? Aren't v and p assigned values already before those lines? Or do they have some default initial value for their type that doesn't count as an assignment?
It's not C, but a new language with similar syntax. Based on reading the docs at [1], the assignment is sugar for generating a new binding for the result of that expression that shadows the previous one.
Probably that the original binding of p is immutable, as it would be in a functional language: you can create a new binding, but you can't actually change the value of the existing binding.
New bindings live at different addresses (if you pass a pointer to one binding, and then make a new binding with a new value, the existing pointer will point to the old value and not the new binding's value) and bindings made in a loop iteration won't live to the next iteration.
An SSA pass is an implementation detail that can't change language semantics, so, as I understand, it either doesn't transform or applies only to pointers when values have externally (to the block in which they are defined) visible mutation based on the language semantics; having single assignment semantics and thereby lacking mutability is a significant property for, particularly, parallel code with shared references. If no one can mutate a reference, you don't need Rust- or Pony-like tracking of who can see or mutate it, it's safe for everyone to see it.
I was interested because I've started adopting the single assignment style in my own code, and like the results (the code is easier to comprehend).