x86 performance quirks

342 views
Skip to first unread message

Russ Cox

unread,
Oct 3, 2011, 11:37:25 PM10/3/11
to golang-nuts
I have two copies of a loop compiled with 6g,
running on a Core 2 T5600 cpu in 64-bit mode.
The loop scans a 1 MB []byte it finds the end of a slice
or a byte >= 0x80.

One copy runs at 260 MB/s; the other runs at 600 MB/s.
The difference between the two is that the slow one
does this to test each element (rearranging code here
to show executed instructions):

01cf51 80fb80 | (9) CMPB BL,$128
01cf54 7205 | (9) JCS ,1cf5b
01cf5b ffc0 | (8) INCL ,AX
01cf5d ebdd | (8) JMP ,1cf3c
01cf3c 89cb | (8) MOVL CX,BX
01cf3e 39c1 | (8) CMPL CX,AX
01cf40 7e14 | (8) JLE ,1cf56
01cf42 4863e8 | (9) MOVLQSX AX,BP

and the fast one does:
01cf51 80fb80 | (8) CMPB BL,$128
01cf54 7308 | (8) JCC ,1cf5e
01cf56 ffc0 | (8) INCL ,AX
01cf58 89cb | (8) MOVL CX,BX
01cf5a 39c1 | (8) CMPL CX,AX
01cf5c 7fe4 | (8) JGT ,1cf42
01cf42 4863e8 | (9) MOVLQSX AX,BP

The only differences I see are the inverted tests
and the extra direct JMP executed in the first trace.
A priori I would not expect either to cause such a
huge difference in performance in something that
should be memory bound anyway.

The full disassembly of each version is below.

Can anyone explain why the fast version is so much
faster than the slow version?

Thanks.
Russ


slow (260 MB/s)
for i = 0; i < len(b); i++ {
if b[i] >= 0x80 {
break
}
}
01cf1e NonASCII | (7) TEXT NonASCII+0(SB),$137438953472
01cf1e 65488b0c25a0080000 | (7) MOVQ 2208(GS),CX
01cf27 483b21 | (7) CMPQ SP,(CX)
01cf2a 7705 | (7) JHI ,1cf31
01cf2c e85364feff | (7) CALL ,3384+runtime.morestack32
01cf31 488b542408 | (7) MOVQ b+8(SP),DX
01cf36 8b4c2410 | (7) MOVL b+16(SP),CX
01cf3a 31c0 | (8) MOVL $0,AX
01cf3c 89cb | (8) MOVL CX,BX
01cf3e 39c1 | (8) CMPL CX,AX
01cf40 7e14 | (8) JLE ,1cf56
01cf42 4863e8 | (9) MOVLQSX AX,BP
01cf45 39cd | (9) CMPL BP,CX
01cf47 7205 | (9) JCS ,1cf4e
01cf49 e8833fffff | (9) CALL ,10ed1+runtime.panicindex
01cf4e 8a1c2a | (9) MOVB (DX)(BP*1),BL
01cf51 80fb80 | (9) CMPB BL,$128
01cf54 7205 | (9) JCS ,1cf5b
01cf56 89442420 | (15) MOVL AX,.noname+32(SP)
01cf5a c3 | (15) RET ,
01cf5b ffc0 | (8) INCL ,AX
01cf5d ebdd | (8) JMP ,1cf3c

fast (600 MB/s)
for i = 0; i < len(b) && b[i] < 0x80; i++ {
}
01cf1e NonASCII | (7) TEXT NonASCII+0(SB),$137438953472
01cf1e 65488b0c25a0080000 | (7) MOVQ 2208(GS),CX
01cf27 483b21 | (7) CMPQ SP,(CX)
01cf2a 7705 | (7) JHI ,1cf31
01cf2c e85364feff | (7) CALL ,3384+runtime.morestack32
01cf31 488b542408 | (7) MOVQ b+8(SP),DX
01cf36 8b4c2410 | (7) MOVL b+16(SP),CX
01cf3a 31c0 | (8) MOVL $0,AX
01cf3c 89cb | (8) MOVL CX,BX
01cf3e 39c1 | (8) CMPL CX,AX
01cf40 7e1c | (8) JLE ,1cf5e
01cf42 4863e8 | (8) MOVLQSX AX,BP
01cf45 39cd | (8) CMPL BP,CX
01cf47 7205 | (8) JCS ,1cf4e
01cf49 e8833fffff | (8) CALL ,10ed1+runtime.panicindex
01cf4e 8a1c2a | (8) MOVB (DX)(BP*1),BL
01cf51 80fb80 | (8) CMPB BL,$128
01cf54 7308 | (8) JCC ,1cf5e
01cf56 ffc0 | (8) INCL ,AX
01cf58 89cb | (8) MOVL CX,BX
01cf5a 39c1 | (8) CMPL CX,AX
01cf5c 7fe4 | (8) JGT ,1cf42
01cf5e 89442420 | (12) MOVL AX,.noname+32(SP)
01cf62 c3 | (12) RET ,

Paul Borman

unread,
Oct 4, 2011, 12:01:44 AM10/4/11
to r...@golang.org, golang-nuts
Well, the slow one has:

01cf5d  JMP     ,1cf3c

while the fast one has

01cf5c  JGT     ,1cf42

So the slow one is potentially hitting more cache lines for instructions in the loop.  I don't know what the cache lines are these days and if jumping across a 64 byte boundary would make much a difference (I assume the machine was lightly loaded so using an extra cache line shouldn't make a difference.

    -Paul

ron minnich

unread,
Oct 4, 2011, 12:10:47 AM10/4/11
to Paul Borman, r...@golang.org, golang-nuts
I thought about the size of the code, the slow one is > 32 bytes and
the fast one < 32 bytes in the inner loop. But cache lines on this
machine are 64 bytes (I just checked).

But were you worried about the size of the loop there's an easy way to
check it; shrink the inner loop:


01cf51 80fb80 | (9) CMPB BL,$128
01cf54 7205 | (9) JCS ,1cf5b
01cf56 89442420 | (15) MOVL AX,.noname+32(SP)
01cf5a c3 | (15) RET ,
01cf5b ffc0 | (8) INCL ,AX
01cf5d ebdd | (8) JMP ,1cf3c

An assembly guy might write this differently:
INCL AX


01cf51 80fb80 | (9) CMPB BL,$128

01cf54 7205 | (9) JCS ,1cf3c


01cf56 89442420 | (15) MOVL AX,.noname+32(SP)
01cf5a c3 | (15) RET ,

yes, an unnecessary INCL on the last time in the loop but it shrinks
the inner loop a bit -- and removes a JMP which may be more costly
than that last unnecessary INCL-- only assuming the size matters (and
assuming I didn't screw this up ...). I doubt that's it however.

If you really want to crack this one you might try AMD's excellent
SIMNow tool, which I've used in the past to try to figure out this
sort of thing. Or ask those excellent CLANG experts at Google :-)

ron

Aaron Durbin

unread,
Oct 4, 2011, 12:13:56 AM10/4/11
to Paul Borman, r...@golang.org, golang-nuts
On Mon, Oct 3, 2011 at 21:01, Paul Borman <bor...@google.com> wrote:
> Well, the slow one has:
> 01cf5d  JMP     ,1cf3c
> while the fast one has
> 01cf5c  JGT     ,1cf42
> So the slow one is potentially hitting more cache lines for instructions in
> the loop.  I don't know what the cache lines are these days and if jumping
> across a 64 byte boundary would make much a difference (I assume the machine
> was lightly loaded so using an extra cache line shouldn't make a difference.

I agree w/ Paul on that cacheline analysis. However, the first loop
has 4 branches through each loop that doesn't pass the condition
checks while the 2nd loop only has only 3. I think the number of
branches are what is ailing the first implementation.

ron minnich

unread,
Oct 4, 2011, 12:33:57 AM10/4/11
to Aaron Durbin, Paul Borman, r...@golang.org, golang-nuts
On Mon, Oct 3, 2011 at 9:13 PM, Aaron Durbin <adu...@gmail.com> wrote:

> I agree w/ Paul on that cacheline analysis.

On a machine like this with 64 byte cache lines, the loop sizes are:
5e - 3c = 34 bytes
5d - 42 = 25 bytes.

But, good point, the "slow" one crosses (I assume) a cache line
boundary, since the 64-byte cache lines ought to be 0x40 aligned. The
second does not.

> However, the first loop
> has 4 branches through each loop that doesn't pass the condition
> checks while the 2nd loop only has only 3. I think the number of
> branches are what is ailing the first implementation.

But harder to imagine that that's worth a factor of 3x in memory bandwidth.

The fast one has only relative jumps, the slow one has an absolute
jmp. In the old days we used certain types of abs. jumps to flush the
pipeline but that's a pretty ancient trick and I can't find references
to such things in newer CPU docs, except a few special cases when you
change CPU modes.

Would be interesting to change the "slow" one as I suggested earlier
and see if shrinking it and aligning it to fit one cache line helped.
I'm skeptical. But the one big difference, still, is that
unconditional backward jmp. Would also be interesting to see if that's
it.

Gotta love modern CPUs -- well, really you do, they're so fast --
until you hit this stuff ...

But it's extremely helpful to pass stuff like this through a good
simulator (I'm sure intel can point you at one) and see what it thinks
is going on.

ron

Paul Borman

unread,
Oct 4, 2011, 12:34:22 AM10/4/11
to Aaron Durbin, r...@golang.org, golang-nuts
Cache lines play part of it.

package s

var extra int
            
func slow(b []byte) (i int) {
    // extra = i
    for i = 0; i < len(b); i++ {
        if b[i] >= 0x80 {
            break
        } 
    }
    return
}

func fast(b []byte) (i int) {
    for i = 0; i < len(b) && b[i] < 0x80; i++ {
    }
    return
}

When I bench mark this I get:

s.BenchmarkSlow     500   4634360 ns/op
s.BenchmarkFast    1000   1992247 ns/op

But when I uncomment the "extra" line I get:

s.BenchmarkSlow     500   3974158 ns/op
s.BenchmarkFast    1000   1996931 ns/op

If I make a slow1 and slow2 in the same file (one with the extra line and one without) it lines things up different and both slow loops are nearly identical.  

So it is probably a combination of the extra check as well as the cache.

I guess I should learn how to dump a 6.out

    -Paul

On Mon, Oct 3, 2011 at 9:13 PM, Aaron Durbin <adu...@gmail.com> wrote:

Paul Borman

unread,
Oct 4, 2011, 12:38:03 AM10/4/11
to ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
Crays were much easier to understand.  The only cache was the instruction cache.  Cycle instruction accuracy was not terribly hard and you could do it by hand.  But as you say Gotta love modern CPUs

    -Paul

ron minnich

unread,
Oct 4, 2011, 12:38:28 AM10/4/11
to Paul Borman, Aaron Durbin, r...@golang.org, golang-nuts
On Mon, Oct 3, 2011 at 9:34 PM, Paul Borman <bor...@google.com> wrote:
> Cache lines play part of it.
>
> package s
> var extra int
>
> func slow(b []byte) (i int) {
>     // extra = i
>     for i = 0; i < len(b); i++ {
>         if b[i] >= 0x80 {
>             break
>         }
>     }
>     return
> }
> func fast(b []byte) (i int) {
>     for i = 0; i < len(b) && b[i] < 0x80; i++ {
>     }
>     return
> }
>

I'd be pretty surprised if these turned into very different object
code. But I don't always have a good sense for what 6g will produce,
as opposed to the optimizing compilers I'm used to. OK, now I get to
be surprised :-) But note that you added an extra line of code (which
I doubt matters) but you also changed the sense of the test (which I
think matters more). But let's see the code.

Would be nice to see a 6.out dump.

ron

Paul Borman

unread,
Oct 4, 2011, 12:43:29 AM10/4/11
to ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
No, all I should have done was put an extra load before the loop in the faster one.  This should have moved the loop down and I guess it is now at or just beyond a 64 byte boundary rather than before.

How do I dump a 6.out on a Mac?  gdb isn't my friend.  It is built with:

I see that 6l takes a -e option, but I really am not a plan 9 tool chain expert.  I just let make do its thing.

    -Paul

Devon H. O'Dell

unread,
Oct 4, 2011, 12:50:21 AM10/4/11
to Paul Borman, ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
You should have either nm or objdump (or both -- I don't have a mac
handy right now). Both should be able to give you a full disassembly.

--dho

2011/10/4 Paul Borman <bor...@google.com>:

Paul Borman

unread,
Oct 4, 2011, 1:14:11 AM10/4/11
to Devon H. O'Dell, ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
No, no objdump that I can find.  (These aren't ELF files)  I did get gdb to help in combination with 6nm if I ^C the run.

So this is very odd now.  The JMP of the slower of the two slow ones is

0x15bcf: jmp    0x15ba8

while the faster the slow ones is

0x15bd6: jmp    0x15baf

But both are jumping into the same cache line.  When I add 2 assignments (14 bytes) the speed is the same as with no assignments, with 3 assignments it slows down to 5377906 ns/op.  4 is about the same as 2 or none, But adding *5* extra assignment speeds it up to 2814054 ns/s!?!

Here is the assembly for the fastest of the slow ones:

0x15b8f: mov    %gs:0x8a0,%rcx
0x15b98: cmp    (%rcx),%rsp
0x15b9b: ja     0x15ba2
0x15b9d: callq  0x31cf
0x15ba2: mov    0x10(%rsp),%ecx
0x15ba6: xor    %eax,%eax
0x15ba8: mov    %eax,0xf459c
0x15baf: mov    %eax,0xf45a0
0x15bb6: mov    %eax,0xf45a4
0x15bbd: mov    %eax,0xf45a8
0x15bc4: mov    %eax,0xf45ac
0x15bcb: mov    %ecx,%ebx
0x15bcd: mov    %eax,0x18(%rsp)
0x15bd1: cmp    %eax,%ecx
0x15bd3: jle    0x15bef
0x15bd5: movslq %eax,%rbp
0x15bd8: cmp    %ecx,%ebp
0x15bda: jb     0x15be1
0x15bdc: callq  0x10d6d
0x15be1: mov    0x8(%rsp),%r8
0x15be6: mov    (%r8,%rbp,1),%bl
0x15bea: cmp    $0x80,%bl
0x15bed: jb     0x15bf0
0x15bef: retq   
0x15bf0: inc    %eax
0x15bf2: jmp    0x15bcb

The 4 assignment case (which is much slower than the 5) has the final jump to 0x15bc4 so it is jumping into the same cache line as the 5 case.  I think that some instructions probably load faster at different offsets.  Intel really can byte you with non-aligned memory loads (where other CPUs simply say "no can do").

    -Paul

Paul Borman

unread,
Oct 4, 2011, 1:24:12 AM10/4/11
to Devon H. O'Dell, ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
Okay, this all only gets worse.

I wrote a version of this loop using range, just to see what woud happen.

s.BenchmarkSlow      500       4652004 ns/op
s.BenchmarkFast     1000       1991039 ns/op
s.BenchmarkRange     500       3961546 ns/op

My source had

func Slow
func Range
func Fast

When I move Slow to be *after* Range rather than before it I get:

s.BenchmarkSlow      500       4659046 ns/op
s.BenchmarkFast     1000       1998310 ns/op
s.BenchmarkRange    1000       2285858 ns/op

So the difference is all where the instructions are aligning.  I think have 6g align function calls and then some carefully placed no-ops could really speed some loops up.

    -Paul

PS:  Does altitude affect X86_64 cache loads?

Anthony Martin

unread,
Oct 4, 2011, 1:38:30 AM10/4/11
to Paul Borman, ron minnich, Aaron Durbin, r...@golang.org, golang-nuts
Paul Borman <bor...@google.com> once said:
> How do I dump a 6.out on a Mac?

Pass 6l the '-a' flag. You'll also
want to redirect stdout to a file.

Cheers,
Anthony

brainman

unread,
Oct 4, 2011, 1:53:30 AM10/4/11
to golan...@googlegroups.com, ron minnich, Aaron Durbin, r...@golang.org
On Tuesday, 4 October 2011 14:43:29 UTC+10, Paul Borman wrote:

How do I dump a 6.out on a Mac?  gdb isn't my friend.  It is built with:


I think you want

6l -a ...

Alex

ron minnich

unread,
Oct 4, 2011, 1:54:20 AM10/4/11
to golang-nuts
Welcome to CPUs that rewrite the instruction stream internally and for
which very small changes lead to very large deltas in performance ...
the instructions you see are not necessarily strongly related to what
goes on. It's a common experience.

I sure hope you all figure it out and can tell me the answer :)

ron

Paul Borman

unread,
Oct 4, 2011, 1:56:30 AM10/4/11
to golan...@googlegroups.com
It would really be nice if 6g would generate assembly code that 6a could assemble...

Thanks to Alex and Anthony for the 6l flag.  I actually am finding on my Mac otool produces easier to read output (I wrote up a quick awk script that adds in labels to the output of otool by using 6nm).

    -Paul

Paul Borman

unread,
Oct 4, 2011, 2:01:09 AM10/4/11
to ron minnich, golang-nuts
Maybe if we could ignore the CISC layer of modern Intel CPUs and program directly at the RISC layer (I am told Intel now translates CISC into RISC internally...)  Hmm, if this is intel 64 bit why aren't there any r registers in there?  I was under the impression the reason 64 bit intel could be faster than 32 bit intel was because 64 bit intel was a different instruction set (adds in all those r registers and such).  Other processors which didn't redesign the instruction set from 32 to 64 (i.e., their 32 bit instruction sets had enough register to start with) typically performed better in 32 bit mode.  I really like PowerPC a lot more than Intel, but it has been years since I have looked at it (so maybe it is all messed up now too)

    -Paul

Tarmigan

unread,
Oct 4, 2011, 2:17:55 AM10/4/11
to Paul Borman, Aaron Durbin, r...@golang.org, golang-nuts
On Mon, Oct 3, 2011 at 9:34 PM, Paul Borman <bor...@google.com> wrote:
> Cache lines play part of it.
>
> package s
> var extra int
>
> func slow(b []byte) (i int) {
>     // extra = i
>     for i = 0; i < len(b); i++ {
>         if b[i] >= 0x80 {
>             break
>         }
>     }
>     return
> }
> func fast(b []byte) (i int) {
>     for i = 0; i < len(b) && b[i] < 0x80; i++ {
>     }
>     return
> }
>
> When I bench mark this I get:
> s.BenchmarkSlow     500   4634360 ns/op
> s.BenchmarkFast    1000   1992247 ns/op
> But when I uncomment the "extra" line I get:
> s.BenchmarkSlow     500   3974158 ns/op
> s.BenchmarkFast    1000   1996931 ns/op
> If I make a slow1 and slow2 in the same file (one with the extra line and
> one without) it lines things up different and both slow loops are nearly
> identical.
> So it is probably a combination of the extra check as well as the cache.
> I guess I should learn how to dump a 6.out

Do you have a complete example (including data)? I tried to recreate
from scratch (without "extra"), but got
s.BenchmarkFast 500 3194202 ns/op
s.BenchmarkSlow 500 3166650 ns/op
on an EC2 linux micro instance.

Thanks,
Tarmigan

Ian Lance Taylor

unread,
Oct 4, 2011, 2:26:02 AM10/4/11
to r...@golang.org, golang-nuts
Russ Cox <r...@golang.org> writes:

> One copy runs at 260 MB/s; the other runs at 600 MB/s.
> The difference between the two is that the slow one
> does this to test each element (rearranging code here
> to show executed instructions):
>
> 01cf51 80fb80 | (9) CMPB BL,$128
> 01cf54 7205 | (9) JCS ,1cf5b
> 01cf5b ffc0 | (8) INCL ,AX
> 01cf5d ebdd | (8) JMP ,1cf3c
> 01cf3c 89cb | (8) MOVL CX,BX
> 01cf3e 39c1 | (8) CMPL CX,AX
> 01cf40 7e14 | (8) JLE ,1cf56
> 01cf42 4863e8 | (9) MOVLQSX AX,BP
>
> and the fast one does:
> 01cf51 80fb80 | (8) CMPB BL,$128
> 01cf54 7308 | (8) JCC ,1cf5e
> 01cf56 ffc0 | (8) INCL ,AX
> 01cf58 89cb | (8) MOVL CX,BX
> 01cf5a 39c1 | (8) CMPL CX,AX
> 01cf5c 7fe4 | (8) JGT ,1cf42
> 01cf42 4863e8 | (9) MOVLQSX AX,BP

I note that in the slow case the default branch predictions are wrong
and in the fast case they are right. By default the processor predicts
a backward branch as taken and a forward branch as not taken. The slow
loop has a default branch misprediction leading to a mispredicted RET,
which is a particularly bad case. However, since this is in a loop it's
hard to believe it would make such a big difference.

I think that in the slow case the loop occupies two cache lines whereas
in the fast case the loop is entirely within one cache line. Again it's
hard to believe this would make such a big difference.

Why is the MOVL CX,BX instruction there?

Ian

unread,
Oct 4, 2011, 7:20:43 AM10/4/11
to golang-nuts
I agree with others who wrote that the performance difference is most
likely related to branch prediction.

Try running "perf stat -- program" in Linux.

Dmitry Vyukov

unread,
Oct 4, 2011, 8:17:43 AM10/4/11
to r...@golang.org, golang-nuts
I suspect it has to do with

Intel® 64 and IA-32 Architectures Optimization Reference Manual
2.1.2.4
The Loop Stream Detector (LSD)
The Loop Stream Detector was introduced in Intel® Core microarchitectures. The LSD detects small loops that fit in the micro-op queue and locks them down. The loop
streams from the micro-op queue, with no more fetching, decoding, or reading micro-ops from any of the caches, until a branch miss-prediction inevitably ends it.
The loops with the following attributes qualify for LSD/micro-op queue replay:
- Up to eight chunk fetches of 32-instruction-bytes
- Up to 28 micro-ops (~28 instructions)
- All micro-ops are also resident in the Decoded ICache
- Can contain no more than eight taken branches and none of them can be a CALL or RET
- Cannot have mismatched stack operations. For example, more PUSH than POP instructions.
Many calculation-intensive loops, searches and software string moves match these characteristics.
Use the loop cache functionality opportunistically. For high performance code, loop unrolling is generally preferable for performance even when it overflows the LSD capability.

Paulo Pinto

unread,
Oct 4, 2011, 8:48:40 AM10/4/11
to golang-nuts
This is a good example why when targeting modern processors writing
pure
assembly code might not bring the gains one might expect.

--
Paulo
> *The Loop Stream Detector (LSD)*
> *The Loop Stream Detector was introduced in Intel® Core microarchitectures.
> The **LSD detects small loops that fit in the micro-op queue and locks them
> down. The loop*
> *streams from the micro-op queue, with no more fetching, decoding, or
> reading **micro-ops from any of the caches, until a branch miss-prediction
> inevitably ends it.*
> *The loops with the following attributes qualify for LSD/micro-op queue
> replay:*
> *- Up to eight chunk fetches of 32-instruction-bytes*
> *- Up to 28 micro-ops (~28 instructions)*
> *- All micro-ops are also resident in the Decoded ICache*
> *- Can contain no more than eight taken branches and none of them can be a
> CALL or RET*
> *- Cannot have mismatched stack operations. For example, more PUSH **than
> POP instructions.*
> *Many calculation-intensive loops, searches and software string moves match
> these **characteristics.*
> *Use the loop cache functionality opportunistically. For high performance
> code, loop **unrolling is generally preferable for performance even when it
> overflows the LSD **capability.*

Miki Tebeka

unread,
Oct 4, 2011, 9:16:02 AM10/4/11
to golan...@googlegroups.com, r...@golang.org
Maybe you can use VTune (http://software.intel.com/en-us/articles/intel-vtune-amplifier-xe/) to profile the code. It's been a while since I used it but IIRC it can show low level CPU event information.

Russ Cox

unread,
Oct 4, 2011, 9:54:03 AM10/4/11
to Dmitry Vyukov, golang-nuts
On Tue, Oct 4, 2011 at 08:17, Dmitry Vyukov <dvy...@google.com> wrote:
> I suspect it has to do with ...

That's very interesting, but can you see why it would
fail to fit in one case but not the other?  It looks to
me like maybe it's the mispredict, but you'd think
after an iteration or two it would re-enter the LSD with
the correct predictions.  We're nowhere near the
32 instruction bytes or 28 micro-ops or 8 taken branches.
I'm going to invert the if branch anyway, so that if
nothing else we get the same code for
    for y { }
and
    for { if !y { break } }
but I don't fully understand what's going on.
I played with performance counters using Shark for
a while but there wasn't anything that seemed interesting.
It looks like the Core i7 added an LSD_UOPS counter
but the Core 2 didn't have it despite having an LSD.

Russ

Jan Mercl

unread,
Oct 4, 2011, 10:06:50 AM10/4/11
to golan...@googlegroups.com
On Tuesday, October 4, 2011 3:54:03 PM UTC+2, Russ Cox wrote:
On Tue, Oct 4, 2011 at 08:17, Dmitry Vyukov <dvy...@google.com> wrote:
> I suspect it has to do with ...

That's very interesting, but can you see why it would
fail to fit in one case but not the other?  It looks to
me like maybe it's the mispredict, but you'd think
after an iteration or two it would re-enter the LSD with
the correct predictions.

IIRC, the branch prediction used to be fully static (like fwd:not taken, back:taken). Don't know if that changed in LSD, but when I try to imagine the HW requirements [within a multicore CPU with multiple level caches], I guess it's still the same old story today.

Dmitry Vyukov

unread,
Oct 4, 2011, 11:33:25 AM10/4/11
to r...@golang.org, golang-nuts
On Tue, Oct 4, 2011 at 5:54 PM, Russ Cox <r...@golang.org> wrote:
On Tue, Oct 4, 2011 at 08:17, Dmitry Vyukov <dvy...@google.com> wrote:
> I suspect it has to do with ...

That's very interesting, but can you see why it would
fail to fit in one case but not the other?  It looks to
me like maybe it's the mispredict, but you'd think
after an iteration or two it would re-enter the LSD with
the correct predictions.  We're nowhere near the
32 instruction bytes or 28 micro-ops or 8 taken branches.

I think it's RET instruction.

Or maybe it's just 1 additional jump. Because in the fast case I see 3 jumps with time spread like 33/33/33, and in slow case it's 4 jumps with time spread like 25/25/25/25...

 
I'm going to invert the if branch anyway, so that if
nothing else we get the same code for
    for y { }
and
    for { if !y { break } }
but I don't fully understand what's going on.


                for i := 0; i < len(buf); i++ {
                        if buf[i] < 0x80 {
                                continue
                        }
                        break
                }

seems to fix the issue.

Ian Lance Taylor

unread,
Oct 4, 2011, 11:57:11 AM10/4/11
to golan...@googlegroups.com
Jan Mercl <jan....@nic.cz> writes:

> IIRC, the branch prediction used to be fully static (like fwd:not taken,
> back:taken). Don't know if that changed in LSD, but when I try to imagine
> the HW requirements [within a multicore CPU with multiple level caches], I
> guess it's still the same old story today.

Most modern Intel processors have a branch prediction cache, because
branch mispredictions are so expensive.

That said, Russ: what processor are you using?

Also I noticed this:

http://stackoverflow.com/questions/4531967/how-does-loop-address-alignment-affect-the-speed-on-intel-x86-64

which claims that loop alignment makes a measurable difference, although
I didn't see anything about that in the Intel optimization manual.

Ian

Paul Borman

unread,
Oct 4, 2011, 12:02:07 PM10/4/11
to Ian Lance Taylor, golan...@googlegroups.com
On Tue, Oct 4, 2011 at 8:57 AM, Ian Lance Taylor <ia...@google.com> wrote:

 which claims that loop alignment makes a measurable difference, although
I didn't see anything about that in the Intel optimization manual.

This explains where I saw, where the exact same loop, not changing the number of cache lines used, being moved a little bit in memory showed a a near 2X difference in performance.  It clearly is not just branch prediction logic.  As Ron alluded, these are very complex processors  and they are doing things which are non-obvious.  The next generation processor may take your highly optimized loop and make is slow again.

    -Paul

Dmitry Vyukov

unread,
Oct 4, 2011, 12:30:20 PM10/4/11
to Ian Lance Taylor, golan...@googlegroups.com
On Tue, Oct 4, 2011 at 7:57 PM, Ian Lance Taylor <ia...@google.com> wrote:
Jan Mercl <jan....@nic.cz> writes:

> IIRC, the branch prediction used to be fully static (like fwd:not taken,
> back:taken). Don't know if that changed in LSD, but when I try to imagine
> the HW requirements [within a multicore CPU with multiple level caches], I
> guess it's still the same old story today.

Most modern Intel processors have a branch prediction cache, because
branch mispredictions are so expensive.


Of course it has

2.1.2.3 Branch Prediction
Branch prediction predicts the branch target and enables the processor to begin
executing instructions long before the branch true execution path is known. All
branches utilize the branch prediction unit (BPU) for prediction. This unit predicts the
target address not only based on the EIP of the branch but also based on the execu-
tion path through which execution reached this EIP. The BPU can efficiently predict
the following branch types:
- Conditional branches
- direct calls and jumps
- indirect calls and jumps
- returns

Russ Cox

unread,
Oct 4, 2011, 12:31:33 PM10/4/11
to Ian Lance Taylor, golan...@googlegroups.com
> That said, Russ: what processor are you using?

The Core 2 T5600 in my Mac Mini is the one that cares
quite a lot about this difference. The Core i7 L 640 in
my Thinkpad X201s is unaffected.

Russ

Dmitry Vyukov

unread,
Oct 4, 2011, 12:41:44 PM10/4/11
to Ian Lance Taylor, golan...@googlegroups.com
On Tue, Oct 4, 2011 at 7:57 PM, Ian Lance Taylor <ia...@google.com> wrote:
3.4.1.5 Code Alignment
Careful arrangement of code can enhance cache and memory locality. Likely
sequences of basic blocks should be laid out contiguously in memory. This may
involve removing unlikely code, such as code to handle error conditions, from the
sequence. See Section 3.7, “Prefetching,” on optimizing the instruction prefetcher.
3-12 GENERAL OPTIMIZATION GUIDELINES
Assembly/Compiler Coding Rule 12. (M impact, H generality) All branch
targets should be 16-byte aligned.
 
Reply all
Reply to author
Forward
0 new messages