mixed integer/float ops slower?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

How is the function dum() twice as slow as dumer()?

Code: Select all

#include <stdio.h>

float array[80000000];
float dumarray[80000000];

void dum ()
{
    unsigned i;
    union {

        float x;
        struct {
            unsigned m : 23;
            unsigned e : 8;
            unsigned s : 1;
        };
    } f;

    f.x = 1.f;

    for (i=0; i<80000000; i++)
    {
    array[i] = f.x;
    }

    for (i=0; i<80000000; i++)
    {
    f.x = array[i];
    f.e--;
    array[i] = f.x;
    }
}

void dumer ()
{
    unsigned i;
    float x;

    x = 1.f;

    for (i=0; i<80000000; i++)
    {
    dumarray[i] = x;
    }

    for (i=0; i<80000000; i++)
    {
    x = dumarray[i];
    x = x * .5f;
    dumarray[i] = x;
    }
}


int main ()
{

dumer();
dum();
printf("dum = %f and %f\n", array[0], array[1]);
printf("dumer = %f and %f\n", dumarray[0], dumarray[1]);
system("pause");
return 0;
}

Is there a way to do the same thing dum() is doing faster?

Post

[oops tired eyes.. hope you didn't see that..]

dum is doing a /lot/ of bit masks, and across a byte boundary.. However, dumer is doing a /lot/ of fp access :shrug: clearly fld/fpop are cheaper than and + xor, you got any idea on the assembly output?
Image

Post

Code: Select all

0000000000000000 <dum>:
   0:   e8 00 00 00 00          callq  5 <dum+0x5>
   5:   55                      push   %rbp
   6:   48 89 e5                mov    %rsp,%rbp
   9:   48 83 ec 10             sub    $0x10,%rsp
   d:   b8 00 00 80 3f          mov    $0x3f800000,%eax
  12:   89 45 f0                mov    %eax,-0x10(%rbp)
  15:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  1c:   eb 1c                   jmp    3a <dum+0x3a>
  1e:   8b 55 f0                mov    -0x10(%rbp),%edx
  21:   8b 45 fc                mov    -0x4(%rbp),%eax
  24:   48 8d 0c 85 00 00 00    lea    0x0(,%rax,4),%rcx
  2b:   00
  2c:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 33 <dum+0x33>
  33:   89 14 01                mov    %edx,(%rcx,%rax,1)
  36:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  3a:   81 7d fc ff b3 c4 04    cmpl   $0x4c4b3ff,-0x4(%rbp)
  41:   76 db                   jbe    1e <dum+0x1e>
  43:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  4a:   eb 58                   jmp    a4 <dum+0xa4>
  4c:   8b 45 fc                mov    -0x4(%rbp),%eax
  4f:   48 8d 14 85 00 00 00    lea    0x0(,%rax,4),%rdx
  56:   00
  57:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 5e <dum+0x5e>
  5e:   8b 04 02                mov    (%rdx,%rax,1),%eax
  61:   89 45 f0                mov    %eax,-0x10(%rbp)
  64:   0f b7 45 f2             movzwl -0xe(%rbp),%eax
  68:   66 c1 e8 07             shr    $0x7,%ax
  6c:   80 e4 ff                and    $0xff,%ah
  6f:   83 e8 01                sub    $0x1,%eax
  72:   0f b6 c0                movzbl %al,%eax
  75:   89 c2                   mov    %eax,%edx
  77:   c1 e2 07                shl    $0x7,%edx
  7a:   0f b7 45 f2             movzwl -0xe(%rbp),%eax
  7e:   66 25 7f 80             and    $0x807f,%ax
  82:   09 d0                   or     %edx,%eax
  84:   66 89 45 f2             mov    %ax,-0xe(%rbp)
  88:   8b 55 f0                mov    -0x10(%rbp),%edx
  8b:   8b 45 fc                mov    -0x4(%rbp),%eax
  8e:   48 8d 0c 85 00 00 00    lea    0x0(,%rax,4),%rcx
  95:   00
  96:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 9d <dum+0x9d>
  9d:   89 14 01                mov    %edx,(%rcx,%rax,1)
  a0:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  a4:   81 7d fc ff b3 c4 04    cmpl   $0x4c4b3ff,-0x4(%rbp)
  ab:   76 9f                   jbe    4c <dum+0x4c>
  ad:   48 83 c4 10             add    $0x10,%rsp
  b1:   5d                      pop    %rbp
  b2:   c3                      retq

00000000000000b3 <dumer>:
  b3:   e8 00 00 00 00          callq  b8 <dumer+0x5>
  b8:   55                      push   %rbp
  b9:   48 89 e5                mov    %rsp,%rbp
  bc:   48 83 ec 10             sub    $0x10,%rsp
  c0:   b8 00 00 80 3f          mov    $0x3f800000,%eax
  c5:   89 45 f8                mov    %eax,-0x8(%rbp)
  c8:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  cf:   eb 1c                   jmp    ed <dumer+0x3a>
  d1:   8b 45 fc                mov    -0x4(%rbp),%eax
  d4:   48 8d 0c 85 00 00 00    lea    0x0(,%rax,4),%rcx
  db:   00
  dc:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # e3 <dumer+0x30>
  e3:   8b 55 f8                mov    -0x8(%rbp),%edx
  e6:   89 14 01                mov    %edx,(%rcx,%rax,1)
  e9:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
  ed:   81 7d fc ff b3 c4 04    cmpl   $0x4c4b3ff,-0x4(%rbp)
  f4:   76 db                   jbe    d1 <dumer+0x1e>
  f6:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  fd:   eb 4a                   jmp    149 <dumer+0x96>
  ff:   8b 45 fc                mov    -0x4(%rbp),%eax
 102:   48 8d 14 85 00 00 00    lea    0x0(,%rax,4),%rdx
 109:   00
 10a:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 111 <dumer+0x5e>
 111:   8b 04 02                mov    (%rdx,%rax,1),%eax
 114:   89 45 f8                mov    %eax,-0x8(%rbp)
 117:   f3 0f 10 4d f8          movss  -0x8(%rbp),%xmm1
 11c:   f3 0f 10 05 2c 00 00    movss  0x2c(%rip),%xmm0        # 150 <dumer+0x9d>
 123:   00
 124:   f3 0f 59 c1             mulss  %xmm1,%xmm0
 128:   f3 0f 11 45 f8          movss  %xmm0,-0x8(%rbp)
 12d:   8b 45 fc                mov    -0x4(%rbp),%eax
 130:   48 8d 0c 85 00 00 00    lea    0x0(,%rax,4),%rcx
 137:   00
 138:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # 13f <dumer+0x8c>
 13f:   8b 55 f8                mov    -0x8(%rbp),%edx
 142:   89 14 01                mov    %edx,(%rcx,%rax,1)
 145:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
 149:   81 7d fc ff b3 c4 04    cmpl   $0x4c4b3ff,-0x4(%rbp)
 150:   76 ad                   jbe    ff <dumer+0x4c>
 152:   48 83 c4 10             add    $0x10,%rsp
 156:   5d                      pop    %rbp
 157:   c3                      retq

Post

camsr wrote:Is there a way to do the same thing dum() is doing faster?
Yes: use dumer()! :)

Reinterpreting between float and union is going to be slow, most compilers will go through memory, in addition to needing some bit manipulation.

Even faster than dumer(): use SSE/AVX float compiler intrinsics

Digital CEM

Post

Just noticed the use of SSE, but I didn't specify it's use. The compilation is unoptimized.

Post

When you compile for 64 bit, SSE is assumed. What I meant for SSE intrinsics is instructions like _mm_load_ps() to process 4 floats in parallel (8 for AVX) for each loop iteration.

The compiler code for the integer case is pretty bad, you may do better by doing the arithmetic and masking yourself, something like (untested):

Code: Select all

*((unsigned int*)(array + i)) -= 0x00800000;
With the integer version you may also risk encountering numeric issues (if the exponent wraps around (in my simple implementation above wraparound would also invert the sign!), if the input number is a NaN, etc.)

Digital CEM

Post

You are correct, the SSE registers are by default in x86-64. Does this mean internal precision is not 80-bit? Or only when the intrinsics are used? Somewhat stuck in the Pentium mindset :)

I don't think the problem is SIMD solvable. This code was just to profile the usage of the union/struct versus a simple float op. The end goal is using integer math and lookup tables to control each float segment. Check out my plugin Float Crush for the idea.

Kinda OT but does the FPUCW still function the same with x86-64?

Post

Bit unions are kinda esoteric yeah, the compiler probably likes it a lot better when you just use normal integer bit operators.

When your data is always 32bits, this prevents your pointers from aliasing (due to strict aliasing rules I think), which prevents weird corner cases in pointer addresses and makes it a lot easier for the compiler to reorder everything for better speed.

Post

You get 80 bit intermediate results for x86, but assume 32 bit intermediate results for floats on x64.

FPU and SSE control words are separate.

I guess the end result is: don't use bit unions, especially with your compiler. If you really want to use integer math for floats, do your own shifts, masking and lookup tables.
I suggest using 32 bit integers for intermediate results and computations when possible (compilers often add unnecessary instructions when using smaller types like bytes, and the CPU pipeling doesn't like partial registers like AL).

Digital CEM

Post

It may or may not be critical, union or bitmask. I will do some more profiling. Using union will make the coding dead simple, yet bitmasks may be faster and have some interesting properties to exploit as well. Can you suggest any reading for bitwise stuff?

Thanks.

Post

hi camsr,

you seem already knowledgeable about integer and FP representations. Depending on your specific problem you may find tricks on the web, for example samples of how to approximate log() or sqrt() by operating on floats by using integer arithmetic and lookups. Easiest perf trick used to be to use & to perform fabs(), but nowadays (most) compilers are smarter and will produce even better code.

Like always, implement the readable code first (in this case the union), only optimize if necessary.

The only resource I know specific to bitwise and integer manipulation tricks is the book and web site "Hacker's Delight". Code samples are here: http://www.hackersdelight.org/hdcode.htm
Most of these were relevant at the time of 8 bit computers, less useful today although one can find some interesting tidbits.

For optimization in general and the use of C/C++ to generate fast code, the best resource is probably the material on Agner Fogs' web site.
Intel also has some optimization guides, but not as useful in my opinion.

Digital CEM

Post

DigitalCEM wrote:You get 80 bit intermediate results for x86, but assume 32 bit intermediate results for floats on x64.
What do you mean by 'assume'? I would hope typically defined float ops are working the same as they did with x86. But I also know the SSE registers are not using 80 bits when they are SIMD.

Post

Just a couple of notes.

If you aren't setting the compile options for speed, there's no point benchmarking.

Bit mask and shift operations generally aren't free. Your 8 bit field that you're manipulating would likely be faster if it fell on an 8 bit boundary.

Since you seem to be comparing on Intel architecture, code runs differently on AMD vs. Intel. Important when building for speed. You may need 2 versions to run as fast as possible. You would need to do this even if you wrote entirely in assembly.

Floating point operations are 64 bit on the FPU. It's easier for the chip maker to lop off 32 bits to support floats than double the silicon to add a 32 bit FPU. Using floats rather than doubles means it will do 64 bit math and convert to 32. Conversion is fast but not free. Working with doubles generally means faster but you pay in twice the memory access and twice the data size.

As the other poster suggested, use intrinsics where possible when you need speed. And always align your data, not only on type boundaries but cache boundaries as well when reasonable.

Lastly, don't spend time optimizing a section of code unless you know it's a bottleneck.

Cheers

Post

Thanks ChocoBilly. The FPU intermediate precision on x64 IS 64-bit.

I didn't expect the dum() function to be faster, but figured since my code will be doing the mixed datatype processing already (or better distortion of), that when the float gets inside the struct it would be faster to have a coarse gain trim by decrementing the exponent value. If one thinks carefully in terms of the FPU, the result of that operation is a truncation of the lowest exponent mantissa values.

Crossing byte boundaries is what this is all about. The end result is 'free' mathematical manipulation of the waveform for only the mixed datatype. So the loss of speed is made up by the actual result in comparison with more explicit methods.

If I were to instead declare a bitfield on byte boundaries, would this be faster? Assuming the processing of the values are the same, just having the CPU address a contiguous byte results in less overhead?

Post

ChocoBilly wrote:Just a couple of notes.

If you aren't setting the compile options for speed, there's no point benchmarking.

Bit mask and shift operations generally aren't free. Your 8 bit field that you're manipulating would likely be faster if it fell on an 8 bit boundary.
I guess movzx/movsx is just one instruction, while each extra bitshift or mask is one extra instruction for the decoder. But that's just if you're ALU-throughput bound. If your execution speed is bound by anything else (like cache load/store unit, instruction decoding, memory bandwidth, etc), you're not going to see any difference, since the pipeline will just stuff in your extra bit operations while some other part of the CPU is churning.
ChocoBilly wrote: Since you seem to be comparing on Intel architecture, code runs differently on AMD vs. Intel. Important when building for speed. You may need 2 versions to run as fast as possible. You would need to do this even if you wrote entirely in assembly.
This makes sense if you're writing for in-order CPUs such as the Arm A8, where if you read the result of a calculation too soon, the entire pipeline will stall (so you want to have the cpu cycle latency manual with you as you code). But on crazy out-of-order CPUs like all the P2 and Athlon derivatives, I'm not sure this really makes sense - one of the CPU resources will max out, but it can be hard to tell which one, and it varies from version to version of the same chip as they add or remove calculation units and cache levels and ports on reorder buffers and whatnot. Maybe you'll see a difference if you use bleeding edge stuff like AVX, but I think the gains you'd see for other specific optimizations will be small.
ChocoBilly wrote: Floating point operations are 64 bit on the FPU. It's easier for the chip maker to lop off 32 bits to support floats than double the silicon to add a 32 bit FPU. Using floats rather than doubles means it will do 64 bit math and convert to 32. Conversion is fast but not free. Working with doubles generally means faster but you pay in twice the memory access and twice the data size.
That's not true... the FPU is 80 bits, so it converts both 32bit and 64bit floats. But the conversion is done in the memory load/store operations, so it's actually free (since the data path to add/remove unused bits can be hardwired). In fact, the 80bit load/stores without conversion are quite a bit slower (4..16 microps with 4..19 cycles of latency) than the normal 32/64 ones (1 microp with 3..8 cycles of latency). So yeah, on the FPU there's actually no difference in cost between 32bit vs 64bit ops, aside from 64bit stuff using twice as much cache/memory bandwidth.
ChocoBilly wrote: As the other poster suggested, use intrinsics where possible when you need speed. And always align your data, not only on type boundaries but cache boundaries as well when reasonable.

Lastly, don't spend time optimizing a section of code unless you know it's a bottleneck.

Cheers

Post Reply

Return to “DSP and Plugin Development”