A while back, for my own reasons , I wrote an '
eatcpu
' program to simply eat some number of CPUs worth of CPU time (by default, all of the CPUs on the machine). I wrote it in Go, because I wanted a straightforward program and life is too short to deal with threading in C. The code that uses CPU is simply:
func spinner() {
var i int
for {
i++
}
}
At the time I described this as a simple integer CPU soaker, since the code endlessly increments an int. Recently, to make my life more convenient, I decided to put it up on Github , and as part of that I decided that I wanted to actually know what it was doing; specifically I wanted to know if it actually was all running in CPU registers or if Go was actually loading and storing from memory all of the time. I did this in the straightforward way of running '
go tool compile -S
' (after some research) and then reading the assembly. It took me some time to understand what I was reading and believe in it, because here is the entire assembly that
spinner()
compiles down to:
0x0000 00000 (eatcpu.go:27) JMP 0 0x0000 eb fe
(The second line is the actual bytes of object code.)
Go 1.12.5 had recognized that I had an infinite loop with no outside effects and had compiled it down to nothing more than that. Instead of endless integer addition, I had an endless
JMP
, which was probably using almost none of the CPU's circuitry (certainly it doesn't need to use the integer ALU ).
The Go compiler is clever enough to recognize that a variation of this is still an infinite loop:
func spinner2() int {
var i int
for {
i++
}
return i
}
This too compiles down to '
JMP 0
', since it can never exit the
for
loop to return anything.
However, the Go compiler does not recognize impossible situations as being infinite loops. For example, we can write the following:
func spinner3() uint {
var i uint
for ; i >= 0 ; {
i++
}
return i
}
Since
i
is an unsigned integer, the
for
condition is always true and the loop will never exit. However, Go 1.12.5 compiles it to actual arithmetic and looping code, instead of just a '
JMP 0
'. The core of the assembly code is:
0x0000 XORL AX, AX 0x0002 JMP 7 0x0004 INCQ AX 0x0007 TESTQ AX, AX 0x000a JCC 4 0x000c MOVQ AX, "".~r0+8(SP) 0x0011 RET
(The odd structure is because of how plain
for
loops are compiled. The exit check is relocated to the bottom of the loop, and then on initial loop entry, at 0x0002, we skip over the loop body to start by evaluating the exit check.)
If I'm understanding the likely generated x86 assembly correctly, this will trivially never exit;
TESTQ
likely compiles to some version of
TEST
, which unconditionally clears
CF
(the carry flag), and
JCC
jumps if the carry flag is clear.
(The Go assembler's JCC is apparently x86's
JAE
, per here , and per this x86 JUMP quick reference ,
JAE
jumps if
CF
is clear. Since I had to find all of that and follow things through, I'm writing it down.)
On the whole, I think both situations are reasonable. Compiling infinite
for
loops to straight
JMP
s is perfectly reasonable, since they do get used in real Go code, and so is eliminating operations that have no side effects; put them together and
spinner()
turns into '
JMP 0
'. On the other hand, the unsigned int comparison in
spinner3()
should never happen in real, non-buggy code, so it's probably fine for the optimizer to not recognize that it's always true and thus that this creates an infinite loop with no outside effects.
(There is little point to spending effort on optimizing buggy code.)
PS: I don't know if there's already a Go code checker that looks for unsigned-related errors like the comparison in
spinner3()
, but if there isn't there is probably room for one.