否定浮点变量的不同方法会生成不同的程序集
Different ways of negating a float variable generate different assembly
我有以下代码:
public static float F(float n)
{
return -n;
}
生成以下 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
另一方面,我有以下片段:
public static float G(float n)
{
return n * -1;
}
生成:
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
问题
- 正如您可能注意到的那样,输出有点不同。为什么我们有 2 个不同的程序集?这些代码执行的任务不完全相同吗?
- 这么大块的装配有什么原因吗?如果是,为什么?
- 如何获得更高效的版本。
备注
注意是针对最后一个问题的。
我想基本上得到像汇编这样的“C”。例如:
float
f(float n) {
return -n;
}
float
g(float n) {
return n * -1;
}
生成:
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
- 我尝试了以下方法,但效果不佳:
public static unsafe float F(float n)
{
uint i = *(uint*)&n;
i ^= 0x80000000;
n = *(float*)&i;
return n;
}
我得到了以下我认为效率更低的:
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
- 我犯了一个愚蠢的错误(见评论)。忘记切换
x64
模式:
现在我们有了一个“高效”的汇编代码:
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
但是我认为第一个和第二个问题是有效的:
Why do we have different ASM outputs here?
输出不同,原因很简单,编译器不像人类那样'think'而是遵循标准。
public static float F(float n)
{
return -n;
}
这意味着,要求编译器否定 n
和return 否定值。
这正是编译器所做的
vxorps xmm0, xmm0, xmm1 <--- change sign bit
public static float G(float n)
{
return n * -1;
}
这意味着执行乘法,而这正是编译器所做的
vmulss xmm0, xmm0, [C.G(Single)] <--- multiply
在你的世界里(-n) == (n * -1)
但是编译器对此有不同的看法。而这两个表达方式并不相同。所以 (-n) != (n * -1)
和汇编输出是不同的。
比那更糟,因为 float/double 值 (a * b * c) != (c * b * a)
...
嗯,至少默认情况下不是,你可以用特殊的编译器标志使它相等,抱歉不记得它们到底是什么了。
我有以下代码:
public static float F(float n)
{
return -n;
}
生成以下 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
另一方面,我有以下片段:
public static float G(float n)
{
return n * -1;
}
生成:
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
问题
- 正如您可能注意到的那样,输出有点不同。为什么我们有 2 个不同的程序集?这些代码执行的任务不完全相同吗?
- 这么大块的装配有什么原因吗?如果是,为什么?
- 如何获得更高效的版本。
备注
注意是针对最后一个问题的。
我想基本上得到像汇编这样的“C”。例如:
float
f(float n) {
return -n;
}
float
g(float n) {
return n * -1;
}
生成:
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
- 我尝试了以下方法,但效果不佳:
public static unsafe float F(float n)
{
uint i = *(uint*)&n;
i ^= 0x80000000;
n = *(float*)&i;
return n;
}
我得到了以下我认为效率更低的:
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
- 我犯了一个愚蠢的错误(见评论)。忘记切换
x64
模式:
现在我们有了一个“高效”的汇编代码:
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
但是我认为第一个和第二个问题是有效的:
Why do we have different ASM outputs here?
输出不同,原因很简单,编译器不像人类那样'think'而是遵循标准。
public static float F(float n)
{
return -n;
}
这意味着,要求编译器否定 n
和return 否定值。
这正是编译器所做的
vxorps xmm0, xmm0, xmm1 <--- change sign bit
public static float G(float n)
{
return n * -1;
}
这意味着执行乘法,而这正是编译器所做的
vmulss xmm0, xmm0, [C.G(Single)] <--- multiply
在你的世界里(-n) == (n * -1)
但是编译器对此有不同的看法。而这两个表达方式并不相同。所以 (-n) != (n * -1)
和汇编输出是不同的。
比那更糟,因为 float/double 值 (a * b * c) != (c * b * a)
...
嗯,至少默认情况下不是,你可以用特殊的编译器标志使它相等,抱歉不记得它们到底是什么了。