Different ways of negating a float variable generate different assembly

Viewed 130

I have the following code:

public static float F(float n) 
{
    return -n;
}

which generates following asm:

Program.F(Single)
    L0000: push eax
    L0001: vzeroupper
    L0004: vmovss xmm1, [esp+8]
    L000a: vmovss xmm0, [Program.F(Single)]  ; remember this line
    L0012: vxorps xmm0, xmm0, xmm1           ; remember this line
    L0016: vmovss [esp], xmm0
    L001b: fld st, dword ptr [esp]
    L001e: pop ecx
    L001f: ret 4

on the other hand I have following snippet:

public static float G(float n) 
{
    return n * -1;
}

which generates:

Program.G(Single)
    L0000: push eax
    L0001: vzeroupper
    L0004: vmovss xmm0, [esp+8]
    L000a: vmulss xmm0, xmm0, [Program.G(Single)] ; remember this line
    L0012: vmovss [esp], xmm0
    L0017: fld st, dword ptr [esp]
    L001a: pop ecx
    L001b: ret 4

Questions

  • As you may noticed the outputs are a little bit different. Why do we have 2 different assemblies? Do the codes not perform exactly the same task?
  • Is there a reason for such big chunk of assembly? If yes why?
  • How can I get more efficient version of it.

Note

The NOTEs are for the last question.

I want to get basically the "C" like assembly. E.g.:

float 
f(float n) {
        return -n; 
}

float
g(float n) {
        return n * -1;
}

which generates:

f:
        xorps   xmm0, XMMWORD PTR .LC0[rip]
        ret
g:
        xorps   xmm0, XMMWORD PTR .LC0[rip]
        ret
.LC0:
        .long   -2147483648
        .long   0
        .long   0
        .long   0
  • I tried following but it didn't work quite well:
public static unsafe float F(float n) 
{
    uint i = *(uint*)&n;
    
    i ^= 0x80000000;
    
    n = *(float*)&i;
    
    return n;
}

I got the following which I think is even less efficient:

Program.F(Single)
    L0000: sub esp, 8
    L0003: vzeroupper
    L0006: mov eax, [esp+0xc]
    L000a: mov [esp], eax
    L000d: mov eax, [esp]
    L0010: xor eax, 0x80000000
    L0015: mov [esp], eax
    L0018: vmovss xmm0, [esp]
    L001d: vmovss [esp+0xc], xmm0
    L0023: vmovss xmm0, [esp+0xc]
    L0029: vmovss [esp+4], xmm0
    L002f: fld st, dword ptr [esp+4]
    L0033: add esp, 8
    L0036: ret 4
  • I made a silly mistake (see the comment). Forgot to switch x64 mode:

Now we have an "efficient" assembly code:

public static float F(float n) 
{
    return -n;
}
    
public static float G(float n) 
{
    return n * -1;
}
C.F(Single)
    L0000: vzeroupper
    L0003: vmovss xmm1, [C.F(Single)]
    L000b: vxorps xmm0, xmm0, xmm1
    L000f: ret

C.G(Single)
    L0000: vzeroupper
    L0003: vmulss xmm0, xmm0, [C.G(Single)]
    L000b: ret

BUT the 1st and 2nd question are valid I think:

Why do we have different ASM outputs here?

1 Answers

Output is different, for the simple reason that compiler does not 'think' like human but follows standard.

public static float F(float n) 
{
    return -n;
}

this means, ask compiler to negate n and return negated value.
And that's exactly what compiler does

vxorps xmm0, xmm0, xmm1    <--- change sign bit

public static float G(float n) 
{
    return n * -1;
}

This means to do multiplication, and that exactly what compiler does

vmulss xmm0, xmm0, [C.G(Single)] <--- multiply

In your world (-n) == (n * -1) but compiler has different opinion on that. And these 2 expressions are not same. So (-n) != (n * -1) and assembly outputs are different.

It's even worse than that, for float/double values (a * b * c) != (c * b * a)...

Well at least not by default, you could make it equal with special compiler flags, sorry don't remember what exactly they are.

Related