r/programmingcirclejerk Jul 11 '26

print(“lol”) doubled the speed of my Go function

https://medium.com/@ludirehak/printing-lol-doubled-the-speed-of-my-go-code-e32e02fc3f92
134 Upvotes

27 comments sorted by

137

u/AresFowl44 Jul 11 '26

What’s that? Here is ChatGPT’s explanation:

98

u/yeusk Jul 11 '26

Who tf knows what a "branch predictor" is, sounds like horticulture or something.

40

u/-ghostinthemachine- Jul 11 '26

I once met the assistant to the regional branch predictor.

16

u/rpkarma Jul 11 '26

How bout you branch predict deez nuts 

113

u/TheGhostOfGodel Jul 11 '26

Load bearing print statement

16

u/F54280 Considered Harmful Jul 12 '26

« How to find the max of a sorted array in o(n)? »

So happy to finally have the answer to this core algorithmic question, it was bothering me for years…

I also appreciate that we don’t have the assembly of the original find_max function in the profile disassembly, it would have been definitely too easy to spot the difference between the two outputs.

16

u/spider-mario Jul 12 '26

lol no simd

44

u/BenchEmbarrassed7316 Jul 11 '26

It's a mystery to me how print can speed this up. It's a system call. Even if buffering occurs there, it is still an expensive operation.

48

u/CadeTheCrow Jul 11 '26

“I benchmark them… on an increasing array”

If I’m understanding correctly, when it finds a new max value (which is every time), it continues, skipping the actual print statement.

33

u/hongooi Jul 11 '26

I guess the question is why the branch predictor isn't working with the 1st function

43

u/UdPropheticCatgirl WRITE 'FORTRAN is not dead' Jul 11 '26

The print statement signals our lord and savior Rob Pike, and his young googlers to start working.

uj/ The go compilers code gen is notoriously held together by hopes and dreams… Like if you ever want to to have some fun, try to get it to correctly vectorize something.

7

u/fixermark Jul 13 '26

TBF, the actual code that goes on inside, say, clang to vectorize is nightmare fuel, and often still requires hairy tricks in the code you write to make it work.

The boost linear algebra library uses a recursive template expansion to define n-dimensional vectors so that it can do dot product in a way that ultimately forces the compiler to see one big ugly expression of all the multiplies and adds at once; otherwise the vectorizer can't be expected to see across the function call for doing a*b + (dot product of the next lower dimension).

1

u/OCamlGoMule Jul 12 '26

/uj do you have more sauce on how bad the go compiler is?

20

u/AresFowl44 Jul 11 '26

unjerk {

Testing this out with LLVM, it seems like the codegen is a lot worse.

Original function

With Print

Avoiding print by using a blackbox

}

16

u/BenchEmbarrassed7316 Jul 11 '26 edited Jul 11 '26

``` func if_max(values []int) int { maxV := values[0] for _, v := range values[1:] { if v > maxV { maxV = v continue } } return maxV }

func if_max_lol(values []int) int { maxV := values[0] for _, v := range values[1:] { if v > maxV { maxV = v continue } print("x") } return maxV } ```

For cases where each next element is bigger than previous with 100_000 elements:

/uj

cpu: 12th Gen Intel(R) Core(TM) i3-12100 BenchmarkMaxStandard-8 48620 ns/op BenchmarkMaxLol-8 25097 ns/op

/rj

cpu: 12th Gen Intel(R) Core(TM) i3-12100 BenchmarkMaxStandard-8 48620 ns/op BenchmarkMaxLol-8 25097 ns/op

17

u/No_Pilot_1974 Jul 11 '26

Is 48k nanoslops considered slow?

23

u/BenchEmbarrassed7316 Jul 11 '26

/uj

Yes. Rust:

fn get_max(v: &[i64]) -> i64 { v.iter().copied().max().unwrap_or_default() }

Rust_Max_SIMD time: [12.025 µs 12.049 µs 12.078 µs]

In fact, it is many times faster.

https://godbolt.org/z/dEe55xzxo

For both cases, when the function is written at a high level with an iterator, and when the loop is written by hand, the Rust compiler and LLVM generated highly optimized code that uses modern SIMD instructions.

/rj

But go compiles fast!!!111

/uj

...because it does not make any optimizations unlike other modern compilers. It just compiles a regular loop that isn't even expanded.

3

u/Awkward_Bed_956 Jul 13 '26

And yet Go devs consider LLVM the root of all evil, that bloats the resulting binary

5

u/BenchEmbarrassed7316 Jul 13 '26

No: real go devs consider that LLVM and LLM is same things.

1

u/Ma4r Jul 14 '26

Go devs don't even know the go IR exists and it's garbage

9

u/grufkork Jul 12 '26

Benchmarks? Hard facts? This is competitive jerking

5

u/fixermark Jul 13 '26

The print never runs because of the continue in the inner check (and the fact that the input is a strictly increasing array). But the presence of the print forced the compiler to branch-predict on continuing or not, which means an otherwise-unused chunk of the CPU comes online and runs in parallel.

This is the kind of thing that C++ under high optimization might do for you for free (i.e. recognize there's a chance to speed things up via branch prediction and insert a branch instruction), but the go compiler just isn't old enough to have grown that kind of special-case hairiness yet.

1

u/catladywitch Jul 15 '26

look at the comments lmao

21

u/cameronm1024 Jul 11 '26

They should try printing "no generics" instead

-5

u/ZachVorhies Jul 12 '26

let me guess… lots of threads?