[llvm] r195496 - X86: Perform integer comparisons at i32 or larger.

Chandler Carruth chandlerc at google.com
Sat Dec 28 15:39:00 PST 2013


Well, after some more prodding, I dug into this *much* more deeply. I can
now explain rather precisely the change to bzip2 which causes this patch to
improve performance, and interestingly I no longer think this patch is the
correct approach. Read on, because this was fun. However, be forewarned,
this is more of a blog post than an email. =]


So first, let's all get on the same page about exactly what performance
problem I'm seeing this patch address (Jim, I *think* when we talked this
was the one you were looking at as well, but lemme know if not, I wasn't
100% certain). Here is a snippet of code produced by Clang with this patch
reverted for the really hot inner function in the main loop of bzip2 (I've
based all my benchmarks and measurements on v1.0.6, the latest open source
release):

http://paste2.org/0YGNdVJV

   1. # BB#2: # %if.end
   2. leal 1(%rdi), %eax
   3. leal 1(%rsi), %ebp
   4. movb (%rdx,%rax), %al
   5. movb (%rdx,%rbp), %bl
   6. cmpb %bl, %al
   7. jne .LBB27_1


Now, originally the theory was that cmpb was being slow because if you make
it a cmpl it sped up (for me, by around 3.5%). This is what this patch does:

http://paste2.org/Y7D8cIcV

   1. # BB#3: # %if.end
   2. leal 1(%rdi), %eax
   3. leal 1(%rsi), %ebp
   4. movzbl (%rdx,%rax), %eax
   5. movzbl (%rdx,%rbp), %ebx
   6. cmpl %ebx, %eax
   7. jne .LBB27_1


Ok, cool, this goes faster, but *why*? I was thinking it was around a
partial register stall, but I didn't really understand where the stall came
from. So I tried switching the above code back to 'cmpb':

http://paste2.org/h0MbL2jM

   1. # BB#3: # %if.end
   2. leal 1(%rdi), %eax
   3. leal 1(%rsi), %ebp
   4. movzbl (%rdx,%rax), %eax
   5. movzbl (%rdx,%rbp), %ebx
   6. cmpb %bl, %al
   7. jne .LBB27_1


What I didn't expect was that this was just as fast (at least for me on the
sandybridge machine I was testing on primarily). This got me thinking about
how partial register stalls come to be. You write to one portion of the
register, and then it gets merged with whatever was already in the high
bytes. Well, there are kind of two places where that could happen in the
original example (movb, movb, cmpb): either when we write the partial
register with movb, or when we read it with cmpb. For more discussion of
this from Intel, see their Optimization Reference Manual section 3.5.2.4.

But because we read it with the 'b' suffix, we don't actually read the high
bytes! So we shouldn't be seeing a stall on the read side. So it seems like
(at least on sandybrige) there is merging of partial registers on *write*
even without a read. Then the movb would be enough to cause the slowdown
regardless of how we read it. This at least explains the measured
performance shifts I see on my sandybridge. When I take the original
assembly for all of bzip2, and I modify just these movb instructions to be
movzbl, I see roughly the same performance uplift as we get with ToT and
Jim's patch.

Ok, so it seems that on sandybridge (and I think haswell but I don't have
one handy to test as thoroughly) it is important that when we don't need
the high bytes of a register for an operation and we are going to movb or
movw into it, we use movzbl or movzwl (resp.) to remove the false
dependency at write time on the high bytes. I think that would accomplish
what this patch set out to do, but in a much more targeted way. It will
also catch many more of these issues because movb has the potential to
cause this, not just those feeding 'cmp'.

Also, the measurements I dave done indicate that once we do the above, this
patch should be reverted. I can't find any performance problems with either
8-bit cmpb (or 16-bit cmpw other than the 1-byte prefix cost in encoding),
and using the more precisely sized cmp instructions lets us fold one of the
two memory operands above into the cmp instruction. One thing I'm still
confused by is why the original code didn't do that fold.

Here is what I would intuitively expect:

http://paste2.org/KgLfPD8g

   1. # BB#2: # %if.end
   2. leal 1(%rdi), %ebx
   3. leal 1(%rsi), %ebp
   4. movzbl (%rdx,%rbp), %eax
   5. cmpb %al, (%rdx,%rbx)
   6. jne .LBB27_1


Note that I started from the original version (without this patch) and
manually applied this (and all of the other stuff I'm about to discuss).
I'm also re-register allocating here. It'll become more obvious later why.
=] There is no effective change. we're now loading only one side of the
comparison into a register, zext-ing it to ensure no false dependency, and
directly pulling the other byte from memory.

However, there is a lot more wrong. Let's start off with the simple and
obvious stuff. Why are we taking up two more registers and doing lea's just
to add a displacement to the addresses? The addressing modes using the
results have an empty displacement! So now we have this:

http://paste2.org/mXZmU0df

   1. # BB#2: # %if.end
   2. movzbl 1(%rdx,%rsi), %eax
   3. cmpb %al, 1(%rdx,%rdi)
   4. jne .LBB27_1


Cool, this looks really nice now, and benchmarking confirms it. But before
we can actually do either of these transforms, we'll also have to fix how
this is being used. Time for a more complete example:

http://paste2.org/MjW043aZ

   1. # BB#2: # %if.end
   2. leal 1(%rdi), %eax
   3. leal 1(%rsi), %ebp
   4. movb (%rdx,%rax), %al
   5. movb (%rdx,%rbp), %bl
   6. cmpb %bl, %al
   7. jne .LBB27_1
   8.
   9. # ... snip ...
   10.
   11. .LBB27_1: # %if.then
   12. cmpb %al, %bl
   13. setb %al
   14. .LBB27_37: # %return
   15. movzbl %al, %eax
   16. popq %rbx
   17. popq %rbp
   18. ret


Ok, WTF. We 'cmpb %bl, %al', jump, and then 'cmpb %al, %bl'???? This makes
no sense. *every* predecessor looks like this. The reason for the LB27_37
label sneaking around there is because there are even more blocks which end
in essentially the same thing but with different registers instead of %al
and %bl. Whatever the registers, its always the same pattern: "cmp X, Y;
 jne ....;  cmp Y, X;  setb %al". And if we didn't re-do the "cmp", it
wouldn't have mattered that they were different registers!!! So lets fix
that:

http://paste2.org/z2gwXvAY

   1. # BB#2: # %if.end
   2. movzbl 1(%rdx,%rsi), %eax
   3. cmpb %al, 1(%rdx,%rdi)
   4. jne .LBB27_1
   5. # ... <snip> ...
   6.
   7. .LBB27_1: # %if.then
   8. seta %al
   9. movzbl %al, %eax
   10. ret


Yea, that's a bit more plausible. =] Note that the 'popq' are gone because
while when we fold away all of the lea's we no longer need the two scratch
registers. The .LBB27_37 is cone because we no longer have 6 copies
re-doing a comparison with different registers.

One final tweak that is totally unrelated to the rest of this. We're
missing a DAG combine for the latch of the inner loop here which looks like
this:

http://paste2.org/aO0hBw1O

   1. # BB#36: # %if.end396
   2. # in Loop: Header=BB27_14 Depth=1
   3. addl $8, %edi
   4. addl $8, %esi
   5. xorl %eax, %eax
   6. cmpl %r8d, %edi
   7. movl %r8d, %ebp
   8. cmovbl %eax, %ebp
   9. subl %ebp, %edi
   10. cmpl %r8d, %esi
   11. movl %r8d, %ebp
   12. cmovbl %eax, %ebp
   13. subl %ebp, %esi
   14. decl (%r9)
   15. addl $-8, %r10d
   16. jns .LBB27_14
   17. jmp .LBB27_37
   18.
   19. # ... <snip> ...
   20.
   21. .LBB27_37: # %return
   22. movzbl %al, %eax
   23. popq %rbx
   24. popq %rbp
   25. ret


I'm always really suspicious when I see cmp followed closely by sub.
Indeed, this is a case of us not realizing that the cmp and sub can be
combined. When we do that, we also remove this last use of a scratch
register (%ebp), confirming that we can get rid of all the popq business.
When we do, we also no longer need to jmp, we can just directly ret:

http://paste2.org/v5K9COgf

   1. # BB#36: # %if.end396
   2. # in Loop: Header=BB27_14 Depth=1
   3. addl $8, %edi
   4. addl $8, %esi
   5. movl %edi, %eax
   6. subl %r8d, %eax
   7. cmovae %eax, %edi
   8. movl %esi, %eax
   9. subl %r8d, %eax
   10. cmovae %eax, %esi
   11. decl (%r9)
   12. addl $-8, %r10d
   13. jns .LBB27_14
   14. xorl %eax, %eax
   15. ret


Yea, that looks more like sane code. =]

All of this combines for another 3.5% on bzip2 on sandybridge, despite just
changing the mainGtU function manually by hand.

I'll probably break out some of these issues into bugs and such once I have
more minimal test cases. I think the most important thing is to get the
correct fix for partial register stalls on sandybridge committed and revert
this patch so we don't keep paying a tax for a halfway fix. The rest is
gravy, if very interesting all the same.

Just for reference, here is the complete asm before and after:

Before: http://paste2.org/CC50nde3
After: http://paste2.org/J3DHbPJC
- 100 fewer instructions
- 30% smaller encoded size after assembling
- 7% faster on sandybridge, 3.5% faster than with this patch (r195496)


Also for context, the benchmarking was done with a little compression
algorithm benchmarking tool that I'm working to open source and will
hopefully add to the test suite soon after. It's based on bzip2 1.0.6
(latest sources) and I primarily measured performance by compressing
linux-3.12.tar with it.

-Chandler
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-commits/attachments/20131228/182454e0/attachment.html>


More information about the llvm-commits mailing list