Live data from Hacker News

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

medium.com

121–130 of 130 posts

Re: Print(“lol”) doubled the speed of my Go function

#121

Most languages have a `max` function, so the core of the loop could be written with just something like: `maxV = max(maxV, v)` That could be entirely branchless, right?

A max function still compiles down to some kind of branch

I thought there were specific assembly instructions for this kind of thing, such as MAXSS in x86 [1], plus vector variants like SSE4 PMAXSD. Presumably it's possible the CPU can handle those with special branchless logic, depending on the compiler and CPU implementation. I guess you'd have to know about the CPU internals to know if the instruction is truly branchless, but it is branchless in the sense there is no conditional jump made in the assembly instructions.

[1] https://stackoverflow.com/questions/40196817/what-is-the-ins...

Re: Print(“lol”) doubled the speed of my Go function

#122
post #6

I read it and I still don't get it, can someone (re-)explain what the presence of the print() is doing that is helpful for branch prediction (or any other aspect of the CPU)? Update: It seems to be the conditional move, see https://news.ycombinator.com/item?id=37245325

The high-level view is that adding code that never gets executed causes the compiler to emit code that the CPU predicts better. IDK if this is the compiler assuming that the `print()` call is cold or the branch predictor getting luckier by chance but basically this tickles the CPU in the right way to get better performance.

It seems that this is mostly luck in a strange situation. And of course if you ever hit the `print()` it will be way slower than not. You can probably do better by adding something like a `__builtin_expect(...)` intrinsic in the right place to be more explicit about what the goal is here.

Re: Print(“lol”) doubled the speed of my Go function

#123
post #35

Go can have unexpected performance differences way higher up in the stack. Ask me about that one time I optimized code that was deadlocking because the Go compiler managed to not insert a Gosched[1] call into a loop transforming data that took ~30 minutes or so. The solution could've been to call Gosched, but optimizing the loop to a few seconds turned out to be easier. I assume the inverse - the go compiler adding t…

The Go scheduler is now (well, for years) preemptive.

Re: Print(“lol”) doubled the speed of my Go function

#124

Earlier quoted context omitted.

For branch instructions but not for conditional move instructions, the processor will happily execute the next instruction before the values are actually ready, then invalidate the work it has done later if it turns out retrospectively to have been wrong. This is called branch prediction.

Technically my understanding of terminology is that the branch predictor is responsible for determining which branch to take when speculative execution encounters a branch. Speculative execution is the thing that will invalidate work if the branch predictor was wrong. Also my understanding based on reading this thread is that a conditional move is harder to speculate across because of the data dependency carried acro…

I wouldn't say it's harder to speculate the result of a conditional move, more that processors don't do it. Or even stronger, that a conditional move instruction is intentionally opting out of prediction for one reason or another.

Re: Print(“lol”) doubled the speed of my Go function

#125

Earlier quoted context omitted.

A max function still compiles down to some kind of branch

I thought there were specific assembly instructions for this kind of thing, such as MAXSS in x86 [1], plus vector variants like SSE4 PMAXSD. Presumably it's possible the CPU can handle those with special branchless logic, depending on the compiler and CPU implementation. I guess you'd have to know about the CPU internals to know if the instruction is truly branchless, but it is branchless in the sense there is no con…

clang compiles all three of these functions to use max instructions (https://godbolt.org/z/9z7hGfdhq).

    #include 
    
    using std::max;
    
    int max_array_func(int values[], size_t values_count)
    {
        int max_value = values[0];
        for (size_t j = 0; j  values[j])
            {
                max_value = values[j];
            }
        }
        return max_value;
    }

Re: Print(“lol”) doubled the speed of my Go function

#126
post #79

Earlier quoted context omitted.

Ah thanks! That's interesting but a bit weird to me. That response sounds a little bit like someone who feels like they shouldn't do something and is thinking on-the-fly for reasons they can use to justify that feeling. > We don't want to complicate the language So I can understand if this complicates the implementation but I don't know if totally optional pragmas or annotations complicates the language itself. Like…

Not giving you nice things because "Your average programmer may mess that up" is the whole philosophy of Go though

Yeah I know they don't tend to want to just hand you over low-level access, like an `asm(...)` statement in C (or maybe like instrinsics). But pragmas tend to be a bit less explicitly "mess it up". Like in the case we're talking about, a loop performs slightly worse by default and it might be nice to nudge the compiler into the right direction. Just as go isn't "messing [it] up" by selecting a slightly suboptimal instruction here, you're not "messing [it] up" if you annotate a loop slightly incorrectly.

Re: Print(“lol”) doubled the speed of my Go function

#128

I was curious what this strange assembly language was, as it looked like neither Arm nor x86. Apparently the Go toolchain has its own assembly language which partially abstracts away some architectural differences: https://go.dev/doc/asm I wonder what the advantages are? It feels like as soon as you move away from the basics, the architecture-specific differences will negate most usefulness of the abstraction.

Rob Pike's talk The Design of the Go Assembler from GopherCon 2016: https://www.youtube.com/watch?v=KINIAgRpkDA

Re: Print(“lol”) doubled the speed of my Go function

#129
post #112

I was curious what this strange assembly language was, as it looked like neither Arm nor x86. Apparently the Go toolchain has its own assembly language which partially abstracts away some architectural differences: https://go.dev/doc/asm I wonder what the advantages are? It feels like as soon as you move away from the basics, the architecture-specific differences will negate most usefulness of the abstraction.

I guess it's for historical reasons. As the document you linked states, "The assembler is based on the input style of the Plan 9 assemblers". It's important to know that at least two of the "founding fathers" of Go (Rob Pike and Ken Thompson) are ex-Bell Labs guys and were involved with Plan 9. The Plan 9 compiler toolchain was available, they were familiar with it, so that's what they used for Go. Some parts of the…

It's not just historical, it's more "the same justification as back then".

Re: Print(“lol”) doubled the speed of my Go function

#130
I tried to come up with the most efficient implementation of this rather simple function that I could think of with pure Go without going down to SIMD Assembly: https://go.dev/play/p/zHFxwvWOoeT

-32.31% geomean across the different tests looks rather great. Any ideas how to make it even faster?

Post reply on HN