Branchless Coding in Go
mattnakama.com
mattnakama.com
Maybe it's worth doing a follow-up on this? I originally thought of doing unit tests to ensure it's actually running in constant time for different inputs, but I'm not sure they would run well on anything that wasn't bare-metal (probably too inconsistent).
Have you done this with Go or any other compiled language? I remember a colleague of mine was looking into C++ assembly generation rules a few years ago, but I haven't heard how far he got.
go tool compile -S file.go > file_amd64.s
Then you could verify it doesn't change over time, and choose to begin maintaining by hand if it makes sense.
If you do want to go the route of rolling it yourself, I'd suggest looking into something like Avo: https://github.com/mmcloughlin/avo
The process would be to break out the hand-reviewed (but not necessarily hand-written) assembler for this function into a separate source file for each supported architecture (probably x86-64 and ARMv? nowadays), compile from assembler source into an object file, then link the final Go binary together using the object file and the rest of your app's Go source.
However the Go toolchain doesn't make this particularly easy. I think it might be necessary to use the cgo package[0] and import these objects as C-style functions, even if they originally came from Go source. (If somebody has a simpler way, I'd love to know!)
I think calling conventions for pure Go functions differ somewhat from the C ABI, so you might have to tweak the assembly code to follow C calling conventions as well—with the caveat that I haven't dived deeply into this, and could be all wet.
This method would be "fragile" in the sense that toolchain changes would risk breakage with every Go revision, but at least it would be your build that breaks and not your code's timing-attack security.
I just found a bug (slow code is a bug) in the D backend where bad data layout led to 32 MILLION LLC misses (85% of the whole program) coming from one line!
Think about how much of a cacheline you are using per iteration folks.
The worst offender is slices, since you can't mark them read only or stack allocated.
It's impossible to have a generic cache-friendly datastructure because the optimal datastructure depends on access patterns. E.g. if I have a collection of struct{x:int, y:int, z:int, w:int}, and I know the two main usecases are indexing by x, and iterating over the collection performing some opp on (y,z,w), then the ideal datastructure is one where all the x are contiguous in memory ([x1, x2, x3...]) and separately the y,z,w triples are contiguous ([y1,z1,w1,y2,z2,w2,y3,z3,w3...]).
Wouldn't having ys, zs and ws occupying different cache lines be good enough? After all, the CPU only wants fast access to these data. Or maybe it's a hard thing to do for CPUs to fetch from 4 different lines at a time (+1 for the instructions)?
- Go was designed with simple compiler, because the purpose of it wasn't best performance at all costs, but simple code and fast compilation.
- Instead we watch as people try to second-guess their processor and alter their code to optimize the machine code produced. Defeating the purpose of Go?
- I've actually seen cases where removing the branches slows down code. People have learned that branching is interesting to the CPU, and have decided that no branching means "faster". Actually branching is perfectly fast, when the predictor is right (most of the time). You only want to avoid branching where the predictor does poorly.
Anyway, I think only do these tricks in very hot spots of your code, and backed by a solid benchmark to confirm improvement.
Also, shifting right by n and picking off the least significant bit (&1) may save an instruction or two, depending on the processor.
Finally, an array of bools is itself intrinsically wasteful[1], as the processor can easily test whether a certain bit is set, or set and clear bits, with a single instruction if you keep them packed them together into bytes. There's another comment here about memory layout and cache usage.
[1] It reminds me of the questions "what's the fastest way to generate/parse <bloated text format>?" in an application where you control both ends and a human almost never needs to see the data.
https://github.com/WillAbides/rjson
The generated code isn't much to look at, but it is certainly fast.
https://github.com/WillAbides/rjson/blob/main/object_handler...
Have you compared your parser to simdjson-go? I haven't looked specifically at the go rewrite, but I hear it's decent.
I was very excited to do branch free coding to parse JSON, but really only handled the 'lexing' portion of the task in branch free fashion. Certainly more of the task could be done branch free, especially if you are working on SIMD or GPGPU.
As for simdjson-go, I did benchmark it. rjson outperformed simdjson-go in most benchmarks, but simdjson-go was about 3% faster reading citm_catalog.json.
https://github.com/WillAbides/rjson#simdjson
Edit:
I see on your bio that you are one of the simdjson authors. I hope you will indulge a question about it. I am generally more interested parsing a large volume of json documents efficiently than I am in doing it quickly. Since simdjson uses parallel instructions, would that mean that the speedup comes at the expense of being able to process more documents in parallel on the same hardware?
The use of parallelism in simdjson is the use of SIMD instructions, not multiple cores. So the answer is "no" to the second question.
https://old.reddit.com/r/golang/comments/mlhvx0/i_wrote_yet_...
(Might not be worth it if your architecture has conditional move instructions and your compiler is smart enough to use them.)
I know that branch speculation can be used as an attack vector if our program is the aggressor - but does simply using branches in some way make us more likely to be the victim?
Server-side code which takes a different amount of time depending on what bits are set in the JWT (or any similar authentication token) can be probed by repeating the operation with different values. Think of lockpicking—if you can move a pin and hear a click or feel more or less resistance, you know you've poked something critical in the core.
[0] https://jwt.io/
Say that your server has an admin login that simply compares the submitted password to "thisistherootpassword", one byte at a time. The attacker starts by trying "a","b","c", etc. and measuring the time it takes for the server to respond. When they get to "t", the server takes slightly longer, so they start trying "ta","tb","tc", etc. until the server takes even longer to respond. This allows the attacker to crack the password much, much faster than naively brute-forcing the space.
Actually, I have an even better example: I performed exactly this attack in order to get 1st place in a programming competition. You can read my writeup here: https://lukechampine.com/progcomp.html
Also wouldn't other mitigations stop it, like slowing down retry after 3 attempts, blocking after 10 or so.
What if you can use the unsafe package?
In general, I try to to avoid using unsafe as much as possible. Not because I worry that it's "unsafe" or prone to errors (I'm experienced in C, C++, and assembly), but because it makes the code harder to read, and I don't think my coworkers would appreciate that. ;)
I'm unsure whether there is a perf difference, though, as register-to-register mov may get boiled away to nothing in register renaming.
Am I now wrong in expecting results of performance tests?
I optimized tight loop of network packets forwarding code with "__builtin_expect" and "inline" to get rid of at most all the branch delays and able to get > 10X increase in performance.
Wonder if same method can be used with "go"....
I'd say it probably doesn't exist. Even if it _did_ exist, the Go compiler authors would reserve it for internal use only, like they do with most other Go compiler directives.
E(X + r) = E(X) + E(r) = E(X) + r
E(X + r) is the sample mean which is easy to calculate
E(X) = 1/2 * (max(sample) - min(sample))You'll never guess what the secret number is. /s
The entire purpose of statistics is to uncover that secret number, and it works if you have enough data. Changing the random plus or minus won't save you.
TEST does a boolean AND test: https://www.aldeid.com/wiki/X86-assembly/Instructions/test
This essay just sit in good to know level.