176 points | 26h ago | Discuss on Hacker News | Back to Radar
Of note, as of C#9 (and maybe prior), the dotnet runtime does this automatically whenever it is deemed safe. https://devblogs.microsoft.com/dotnet/performance-improvemen...
The same technique is applied as an optimization, when deemed safe, in all current gen c compilers (gcc, llvm, etc).
I'm very confused why neither measurements nor references to when this is done automatically in most modern languages is included in the article.
For example:
var map = new HashMap<String, String>();
map.put("foo", "bar");
for (var i : items) {
if ("bar".equals(map.get("foo")) {
doStuff(i);
}
}
Even though `map` isn't mutated, it's hard enough for the JVM to detect and the underlying `get` functions are complex enough that it'll run the `get("foo")` every time, which can be quite expensive.Speed was almost never the reason.
"the loop runs without a branch, and is a candidate for vectorization".
That's it, that's the article. This matters a lot in huge-scale / scientific computing / HPF, where if you can express something as an operation on vectors on matrices, you win big (those ops parallelize well, can be run on GPUs, clusters, what have you).
https://github.com/taolson/Admiran/blob/main/examples/fizzBu...
/s
https://docs.swift.org/latest/documentation/the-swift-progra...
They're one of those good practices that look like bad practice to everyone who just got a CS degree. Seems ex-students are unsettled by asymmetry or want to minimize the number of return statements.
And if someone decides the codebase needs an abstraction over (and therefore specific functions to handle) Iterable[Walrus|None] then you check the weather and suggest they take a break and go for a stroll. (You check the weather to see if you should lend them your brolly.)
What am I missing?
From an SE perspective, make a flatmap function that explicitly handles Collection<Optional<Walrus>>. The implementation doesn't matter. If your language/framework already has a compatible flatmap function, make a single frobnicate(Optional<Walrus>) function that returns whatever value is necessary for flatmap(frobnicate) to discard them.
From a CS perspective, doing a filter from Collection<Optional<Walrus>> to Collection<Walrus> is probably a bad idea. If your collection is small, nothing matters. If your collection is large, you probably don't want to spend time making a new copy of it. If your filter just returns a view rather than a hard copy, then there is no optimization benefit and you should just do whatever makes the most sense from an SE perspective. If frobnicate is cheap then you're paying the branch prediction failure tax anyway regardless of when you frobnicate, and if frobnicate is more expensive then your should probably parallelize and have each thread handle unpacking the Optional. Either way, you probably don't want to spend time making a copy.
These are all generalizations based on hypotheticals and there are certainly a lot of exceptions, but broadly speaking I don't see a strong argument here. If optimization matters then optimize based on your own profiling of your situation, and if optimization doesn't matter then design your functions based on what features and paradigms are available/common in your area.
Second: From an SE perspective it has some nice properties too. Obviously every "if" can't be moved up - even the for's that are being pushed down have an implicit "if". But when you try to follow the advice without making the code too crazy, you end up with business logic clustered in a much smaller number of places, and don't have to dig down into a leaf function in an unrelated module to find out why the some transaction was being rejected (aka the accountants said that qty > 100 was not allowed or whatever).
But I suppose my greater philosophy for making code that avoids bugs is that you have a couple things that are done when dealing with data:
- distribution
- deciding
And you want to avoid distribution and deciding being mixed together in the same spot.
"Distribution" can be for loops but also breaking up some data based on some key into N bistinct buckets
"Deciding" is where you're looking at the data more closely to make some decision (like "is this a big customer or a small customer")
Distribution often involves decision making, but if you mix them all in one spot you can obfuscate your decision points. Splitting it up just makes things "obviously" right or "obviously" wrong. Perf stuff is another discussion of course, but in practice most things are not at a scale where it matters.
by_category = defaultdict(list)
for d in data:
by_category[category(d)].append(d)
for category, per_category_data in by_category.items():
do_thing(category, per_category_data)
I really value code patterns that make mistakes obvious, or at least makes it harder to stuff a mistake in somewhere. Some patterns are harder to describe in this model though.(I do like the advice of having a consistent vocabulary for working on collections as a principle though, I just find that top-level conditional use tends to quickly get you into "... why is this method not called" territory, which is a more annoying problem than "why is this slow")
I guess it's a case of perspective
Even worse, by splitting one state (and one derivable category from that state) into two separate arguments for do_thing, something can be off rather badly. I'd then feel the need to design an assertion of the relation of the arguments in order to make things bearable again:
def do_thing(category, data):
# to avoid shadowing lets rename your function `category` to `category_from_item`
assert all(category == category_from_item(item) for item in data)
...
But that would add a third loop to your two loops, and would duplicate the computation of a category.Instead, if category would be a property of data item:
class DataItem:
@property
def category(self):
...
...and the do_thing function would work on a single data item, then it'd just become a simple matter of one for loop and one match/case: def do_thing(item):
match item.category:
case ...:
...There's very high-leverage abstractions that completely constrain a space. An example is a good definition - you can't think of something outside to compare it to, it just is. These things survive for a long time since they define it.
But if you're trying to do that philosophy super deep into a program, you're probably violating a bunch of invariants subtly.
Of course, there is no good separation at the end of the day as we all know from spaghetti codebases :)
def classify(:ok)
def classify({:error, reason})
def classify([first | rest])
def classify(%{name: name, age: age}) when age >= 18
def classify(%{name: name, age: age}) when age < 18You don't want to go "woof if you're a dog, meow if you're a cat", you want to go "make a sound, whoever you are". If you're a fish, you can no-op or not "accept" the call, depending on the language and convention. Obviously there is a lot of nuance, but the general idea is to push responsibilities and logic (which includes branching) down, to the level/entity/object that's best equipped to handle it.
If you apply this to the "a bunch of functions" example, you get low-level functions with branching, but you also get clean callers.
I feel like this isn't such a huge problem IRL, it's usually quite apparent where a decision should be made, but both of these "schools of thought" seem equally valid to me.
Comments are loaded live from Hacker News and are not stored by Mid or Real.
socializer 25h ago on HN
bioneuralnet 25h ago on HN
moritzwarhier 25h ago on HN
msdz 23h ago on HN
cowlevel 21h ago on HN
dmoy 20h ago on HN
msdz 4h ago on HN
moritzwarhier 3h ago on HN
rezonant 21h ago on HN
jibalt 23h ago on HN
swiftcoder 25h ago on HN
mahboi 24h ago on HN
ahartmetz 22h ago on HN
gpderetta 22h ago on HN
econ 22h ago on HN
bzbz 4h ago on HN
Easier for humans to reason about the branch, when they know that it happens without input from i or y[i].
This is helpful when the `if` statement is complex, or there are multiple if statements, some of which do take y[i] as input.
theteapot 20h ago on HN
bathtub365 20h ago on HN
ngaiorn 19h ago on HN
The machine?
theteapot 19h ago on HN
A loop contains a branch - keep looping or break. At least according to structured programming -- https://en.wikipedia.org/wiki/Structured_program_theorem.
pasquinelli 18h ago on HN
interesting question...
> A loop is a chunk of code that will be run N times depending
oh! turns out you do!
ismailmaj 10h ago on HN
Jtsummers 17h ago on HN
Just a simple example, but the conditional branch occurs on line 14 of the generated assembly. It does a comparison (line 13) and then a conditional jump (jl, line 14).
swiftcoder 14h ago on HN
Anyone who started out programming in assembly languages
folago 13h ago on HN
The CPU.
mahboi 2h ago on HN
ahartmetz 19h ago on HN
tremon 8h ago on HN
mkesper 6h ago on HN
mahboi 3h ago on HN
mahboi 3h ago on HN
robofanatic 22h ago on HN
mathisfun123 22h ago on HN
adamgordonbell 21h ago on HN
cryptonector 17h ago on HN
globular-toast 15h ago on HN
I think the main reason I don't write more is I think once I've thought through something it's too obvious to write down. I consider it a defect of mine and sometimes have to force myself to write.
Gravityloss 11h ago on HN
tomsmeding 11h ago on HN
eyelidlessness 6h ago on HN
mcswell 3h ago on HN
Here are the particular comments having to do with this (I don't know how to link to an individual post in the discussion...maybe somebody can write that down for me :):
A: There is a vast amount of mathematical knowledge that hasn’t even been written down, much less formalized.
B: There is no such thing as knowledge that has never even been written down a single time by anyone or anything. Those are simply called ideas...