1B nested loop iterations
benjdd.com
benjdd.com
Some points to consider:
- `for` is not how you'd write an optimised loop in all languages.
- Be VERY suspicious of microbenchmarks on C, C++, and similar - the compilers are smarter than you think and you won't get the code you wrote.
- Languages do arrays differently - added memory effects on performance.
- Languages do numbers differently.
- Summing and modulo is somewhat contrived example. Modulo is one of the slower instructions out there.
I think curves make it tricky to maintain objective perception, but the layout contributes significantly to the idea that internal latency is a different order from external.
I was hoping this would test how long it takes to compile 1 billion nested loops. As in, a 2-billion-line source file where the first billion lines each open a loop and the next billion lines close them all. That would be… interesting. Pointless, but interesting.
I've been thinking of getting it into WASM somehow - it's really just one function that needs to be replaced by a WASM module - but one of the things which has given me pause are benchmarks like this, as well as other anecdotes, that demonstrate I'd only get a minor speedup from WASM. I guess part of this is due to modern JS engines being so optimized.
Just about all of the relevant code is adding and subtracting integers, but theres's some Boolean logic. There's also some multiplication in there that I could eliminate if I really had to, although at this point I'm not certain it would make a significant difference. Most of the data that contributes to the value of each pixel is spread across a tree of objects, and there's some polymorphism involved too. The point is it's not like I'm just dealing with a bunch of contiguous data in memory that is easily cached by the CPU, which might speed things up considerably. I guess I'm just venting now, lamenting a nightmare of my own creation...
Of course, this setup leaves a very thin JS client - it's essentially just rendering the same image over and over. So if you've already got everything done in JS, it would mean porting all of your functional code over to something that can compile into WASM. Not a huge lift, but not fun either.
And then, on the other hand, if it's just rendering you're seeing as a bottleneck, JS works pretty well with webGL and webGPU for simple form rendering. A little loop unrolling might be a lot easier than porting the rest of the app.
Anyway, best of luck on it!
I was absolutely stunned to find that on web WASM ~= JS which altogether weren't far off from a naive C++ stdlib port. I had to make custom data structures instead of using stdlib ones to really get performance significantly different . IIRC my most optimized C++ converted to WASM was about 70% the speed of JS.
Since I've mostly focused on rendering, there's just not much that really should be done on the CPU side. But I do have plans to extend my renderer to have a CPU pipeline which doesn't rely on WebGPU or WebGL, and I imagine I'll be doing quite a bit of benchmarking there. Forcing the CPU to render is going to make js choke - it's just a matter of how much it takes. I imagine any forward rendering will be fine, but there's no chance it does path tracing worth a damn. So somewhere between "textures" and "realtime global illumination" I expect I might have a lot of data about what WASM can do to help in that arena (if anything; though I am expecting it to help quite a bit, especially with stuff like SIMD).
On the other hand, entirely, I have been using the Rapier physics engine since it has a javascript version and as far as I know it heavily utilizes WASM for its physics calculations, so there might be some good benchmarking for WASM benefits surrounding that library.
Is that something I could port to WebGL? Can objects contain or be parents of other objects in that way, and mask thier children to only appear directly in front of them? I have done a tiny bit of WebGL for a CS assignment but it was ages ago. Hopefully once I make this public I'll be able to get some feedback on all this. I don't want to say too much more as it would begin to spoil things!
When going to a dedicated graphics renderer, the biggest "change" from standard dev is "unrolling" your loops. So, right now, you have a bunch of parents and children and parents inform children. This is not, at all, different from a game engine which allows nesting of render objects (very common). Usually, the "transform" of a parent mesh is added to the transform of its children, for a very basic example.
If you're working in 3D, there's a bit of a complication with alpha channels because not only do you have to worry about blending, but you also have to deal with the intersection of alpha objects - simple front-to-back-then-back-to-front rendering won't cover that. But since you're just doing forms, you can be sure that all of your alpha content will be rendered after all of your opaque content, and blended according to its mode at that time, so it's a fairly straightforward version of "unrolling" that you would need.
But all of that is to say: no, WebGL (or any graphics API) won't have any kind of built in way to manage nesting objects. Those relations would have to be defined by you and uploaded to the GPU and then referenced separately, rather than looping through each parent and drilling down into the objects. It's just a different type of problem-solving. Rather than dealing with things per-object, you're dealing with them per-pixel (or pixel group). So you tell each pixel how it should render, based on what objects it is expected to describe at that pixel, and then let them all do their work at the exact same time. It's less "layer painting" and more "arrange and stamp".
Just caching blobs of things that don't change frame to frame and got me a 5x-8x speedup, even if where I render those things at changes frame to frame.
See here for why the Go version performs worse https://news.ycombinator.com/item?id=42251571
GOGC=-1
EDIT: Trying it out quickly shows a small improvement actually, but so small as to likely be noise as I was doing other things on the machine. Summary
./mainnogc 1000 ran
1.01 ± 0.06 times faster than ./maingc 1000Editing the code and running local to get a difference factor, it looks like the same Go code with int takes 1.5x the amount of time as the int32 code on my machine. That puts it ~right next to Java (assuming the perf change maps 1:1). Remove the GC and it comes in ~right next to C++.
Looking at the compiler assembly output on Godbolt it's fun to see all the extra panic info and checks Go adds before going to the loop whereas the C just segfaults from blasting forward anyways, sometimes with no detail.
Anyways, that's why I don't like these types of "benchmarks" (micro tests made into a media post). By the time someone actually looks into them to see what's up everyone has already seen the results and moved on.
Yep.
(display (foldl + 0 (map (lambda (x) (/ x (+ x 1))) (range 0 1000))))
The solution matched and I was happy. Then I thought wouldn't it be neat to do this in a dozen languages and benchmark...but got nowhere with that idea. None of the languages supported fraction addition without jumping through whole bunch of hoops. I wonder if its still the same situation. If someone wants to give this a shot, the answer if you do this for the first ten is 17819/2520.
ghci> sum [i % (i+1) | i <- [0..9]]
17819 % 2520
ghci>
% is the constructor for Rational numbers. I.e. one half is written (and shown as) 1 % 2.I'll spare people the full result, but:
ghci> sum [i % (i+1) | i <- [0..999]]
70755...77483 % 71288...20000
ghci>Real-world code has closures, allocations, and pointer-dererencing. This code is just iterations, additions and modulus.
Which is what the average programmer out there writes. Therefore, I think this is actually a good realistic benchmark.
Folks are always surprised to see just how fast Java is able to perform on benchmarks - it has a reputation as such a slow language then how come it's able to execute almost as fast as C++?
But Java's problem isn't execution performance. It's:
* Startup performance. It takes time to instantiate the JVM. That matters for some, not for others.
* Consistent performance. This is the nature of Garbage Collection. It can surprise you when it executes you and drop a performance blip when you least expect it.
Most developers who think they need the fastest performance actually need CONSISTENT performance more than the fastest.
VR games are now the frontier of gaming (in terms of the industry outdoing itself) and there you're approaching hard real time requirements because it is so awful to get seasick. I appreciate it.
Most incarnations of C# offer a GC.Collect method and an ability to configure the threading model around collections. Used properly, you can keep maximum delays well-bounded. You still have to work your ass off to minimize allocations, but when some do inevitably occur due to framework internals, etc., you don't want to have to clean up 3 hours worth of it at once. Do it every frame or scene change.
Dome Keeper's dev Bippinbits is a Godot studio. Some comments on their opinions that I can't read until I dig out my twitter login lol. https://x.com/Bippinbits/status/1702351798302851398
Godot has some perf warts still being churned out, so I don't know what all they've got to do to finesse around them. Even if GDScript was greasy gcc lightning, it's really easy to crunch yourself into some compromises when you're trying to eat your way out of the bottom of ticket mountain before Nextfest or you starve or whatever. Small team gamedev is wild.
This has been getting much better in Java in recent years. Shenandoah has been the default since Java 15, and we've been seeing further improvements since then.
> Processing thread stacks concurrently gives us reliable sub-millisecond pauses in JDK 17.
https://developers.redhat.com/articles/2021/09/16/shenandoah...
It's literally just "1 Billion nested loop iterations", methods and results.
You make of that whatever you want.
Interesting nonetheless.
Yes and no.
It emphasizes that there are some scenarios where language choice can have an outsized effect on performance when running on modern architectures, which is a reminder some people could use or have never really witnessed.
But it's a very particular scenario that relatively few projects will encounter at a scale that meaningfully impacts them.
In particular, C and Rust are much better positioned to instruction pipelining and lack of cache busting because there are no conditionals and little-to-no out-of-loop code to execute. Even with a massive loop like this, that can be a real world scenario in things like data/signal processing and numerical computation. When you're doing those things, language choice can make a huge difference that may not be apparent without understanding how they work or at least seeing visualizations like this.
But even in C and Rust, most applications don't have to do this kind of thing. Most are dominated by smaller loops, with branching that breaks pipelining, complexity that busts cpu cache, calls and jumps that might interfere with both etc. In those much more common cases, the practical performance difference is going to be quite a lot less significant, and the fluency/ergonomics/safety/convenience of working in some other language can easily matter more.
My take though is that branchy, old-AI, and heavy code (say keep every fact in a triple store and not as a Python object at the benefit of having an index for every permutation of ?s-?p-?o) that cannot be handled with numpy or similar tools benefits greatly from PyPy, but not 100x.
Totally couldn't show that effectively with a static bar chart ;>
Effective as-in that one's 4x faster than that one? Really?
#!/usr/bin/env elixir
u =
case System.argv() do
[arg] -> String.to_integer(arg)
_ -> raise "Please provide a valid integer as the first argument."
end
r = :rand.uniform(10_000) - 1
a = :array.new(10_000, default: 0)
a =
Enum.reduce(0..9_999, a, fn i, acc ->
inner_sum =
Enum.reduce(0..99_999, 0, fn j, sum ->
sum + rem(j, u)
end)
updated_value = :array.get(i, acc) + inner_sum + r
:array.set(i, updated_value, acc)
end)
IO.puts(:array.get(r, a))The benchmark code linked in the submission unfortunately suffers from the mistakes that happen whenever you write a benchmark for the first time. The visuals are pretty but might as well have been based on something more comprehensive.
(also, please stop measuring performance with loops that perform no work and fibonacci sequences, this was bad 30 years ago, this is bad today too)
How deep people have actually written useful nested loops?
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Just think twice before using a for loop. Most of the time it is not needed.
The nested loop becomes: j <- sum((seq_len(100000) - 1) %% u) a <- a + j + r
But seriously, other comments have this nailed. This 'benchmark' isn't comparing apples with apples. There are too many quirks, optimisations and design decisions (of a compiler) that have not being taken into account.
Also, as others have repeated, this isn't indicative of actual usage - real world usage. In that regard, this type of 'benchmark' isn't scientific at all, and worse, is misleading a naive reader.
These are very simple programs with no fancy tricks going on.
If not then it's a pretty good proxy to one or two dimensions of performance, and a mix of similar benchmarks could paint a good fraction of a reasonable picture.
Personally, I'm going to bite the apple.
This program is a perfectly reasonable "bite". It's not doing weird stuff.
Presumably the C compiler has a relative advantage at optimizing the looped algorithm vs the arithmetic algorithm. How much? Is C 100x faster than python, or was the compiler able to get closer to the non-looping algorithm than python? How close? Is it something about the arithmetic or the looping that's natively faster in C? What part of it was optimized by the compiler? Since nobody does large loops in python, would python be more comparable if we introduced a C-backed library like numpy?
What other objects do you think we should bash this apple with?
I don't. So the simple math just makes things easier, it doesn't harm results.
Your suggestion to skip the math entirely is the apple-smash.
> Since nobody does large loops in python
Oh I doubt that.
Numpy would be fine as a different entry.
Nobody who cares about performance enough to look at a benchmark like this would do large loops in pure python.
Because they already know it would be very slow, and putting specific numbers on that is helpful for them and for non-veterans too. Even with some wiggle room for "microbenchmarks are weird".
Personally, I find it misleading for the results of a benchmark to be posted when it's not made clear up front what the benchmark is measuring. At minimum I'd want a disclaimer that mentions at least one of the first two points mentioned in the top comment https://news.ycombinator.com/item?id=42250205
The one serious caveat is that someone that already knows CPython is godawful at this will switch to numpy, but that reinforces what the benchmark says.
None of the things in the post are going to change the order of magnitude here.
https://web.archive.org/web/20010424150558/http://www.bagley...
https://web.archive.org/web/20010124092800/http://www.bagley...
It's been a long time since the benchmarks game showed pico-benchmarks
https://benchmarksgame-team.pages.debian.net/benchmarksgame/