了解 double 到 int64_t 的转换
Understanding double to int64_t conversion
所以我有两个函数,一个只是从 double
转换为 int64_t
,另一个调用 std::round
:
std::int64_t my_cast(double d)
{
auto t = static_cast<std::int64_t>(d);
return t;
}
std::int64_t my_round(double d)
{
auto t = std::round(d);
return t;
}
它们正常工作:cast(3.64)
= 3
和 round(3.64)
= 4
。但是,当我查看集会时,他们似乎在做同样的事情。所以想知道他们如何得到不同的结果?
$ g++ -std=c++1y -c -O3 ./round.cpp -o ./round.o
$ objdump -dS ./round.o
./round.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z7my_castd>:
0: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
5: c3 retq
6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
d: 00 00 00
0000000000000010 <_Z8my_roundd>:
10: 48 83 ec 08 sub [=12=]x8,%rsp
14: e8 00 00 00 00 callq 19 <_Z7my_castd+0x19> <========!!!
19: 48 83 c4 08 add [=12=]x8,%rsp
1d: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
22: c3 retq
Disassembly of section .text.startup:
0000000000000030 <_GLOBAL__sub_I__Z7my_castd>:
30: 48 83 ec 08 sub [=12=]x8,%rsp
34: bf 00 00 00 00 mov [=12=]x0,%edi
39: e8 00 00 00 00 callq 3e <_GLOBAL__sub_I__Z7my_castd+0xe>
3e: ba 00 00 00 00 mov [=12=]x0,%edx
43: be 00 00 00 00 mov [=12=]x0,%esi
48: bf 00 00 00 00 mov [=12=]x0,%edi
4d: 48 83 c4 08 add [=12=]x8,%rsp
51: e9 00 00 00 00 jmpq 56 <_Z8my_roundd+0x46>
我不确定 14
行 callq
的目的是什么,但是,即便如此,my_cast
和 my_round
似乎只是做一个 cvttsd2si
,我认为这是带截断的转换。
然而,正如我之前提到的,这两个函数在同一输入上产生不同的(正确的)值(比如 3.64
)
发生了什么事?
汇编输出更有用(g++ ... -S && cat round.s
):
...
_Z7my_castd:
.LFB225:
.cfi_startproc
cvttsd2siq %xmm0, %rax
ret
.cfi_endproc
...
_Z8my_roundd:
.LFB226:
.cfi_startproc
subq , %rsp
.cfi_def_cfa_offset 16
call round <<< This is what callq 19 means
addq , %rsp
.cfi_def_cfa_offset 8
cvttsd2siq %xmm0, %rax
ret
.cfi_endproc
如您所见,my_round
调用std::round
然后执行cvttsd2siq
指令。这是因为std::round(double)
returnsdouble
,所以它的结果还是要转成int64_t
。这就是 cvttsd2siq
在您的两个函数中所做的。
使用 g++,您可以使用 -fdump-tree-optimized
开关更高级地了解正在发生的事情:
$ g++ -std=c++1y -c -O3 -fdump-tree-optimized ./round.cpp
生成 round.cpp.165t.optimized
文件:
;; Function int64_t my_cast(double) (_Z7my_castd, funcdef_no=224, decl_uid=4743$
int64_t my_cast(double) (double d)
{
long int t;
<bb 2>:
t_2 = (long int) d_1(D);
return t_2;
}
;; Function int64_t my_round(double) (_Z8my_roundd, funcdef_no=225, decl_uid=47$
int64_t my_round(double) (double d)
{
double t;
int64_t _3;
<bb 2>:
t_2 = round (d_1(D));
_3 = (int64_t) t_2;
return _3;
}
这里的差异非常明显(并且对 round
函数的调用很明显)。
当使用 objdump -d
转储目标文件时,添加选项 -r
非常重要,它命令实用程序也转储重定位:
$ objdump -dr round.o
...
0000000000000010 <_Z8my_roundd>:
10: 48 83 ec 28 sub [=10=]x28,%rsp
14: e8 00 00 00 00 callq 19 <_Z8my_roundd+0x9>
15: R_X86_64_PC32 _ZSt5roundd
19: 48 83 c4 28 add [=10=]x28,%rsp
1d: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
现在,请注意出现的新行。那是包含在目标文件中的重定位指令。它指示链接器将 _Z8my_roundd+0x9
和 _ZSt5roundd
之间的距离添加到在偏移量 15 处找到的值。
偏移量14处的e8
是相对调用的操作码。接下来的 4 个字节必须包含被调用函数的 IP 相对偏移量(执行时指向下一条指令的 IP)。因为编译器不知道那个距离,所以它用零填充它并插入一个重定位以便链接器可以稍后填充它。
在没有 -r
选项的情况下进行反汇编时,重定位将被忽略,这会产生一种错觉,即函数 _Z8my_roundd
调用了自身的中间部分。
所以我有两个函数,一个只是从 double
转换为 int64_t
,另一个调用 std::round
:
std::int64_t my_cast(double d)
{
auto t = static_cast<std::int64_t>(d);
return t;
}
std::int64_t my_round(double d)
{
auto t = std::round(d);
return t;
}
它们正常工作:cast(3.64)
= 3
和 round(3.64)
= 4
。但是,当我查看集会时,他们似乎在做同样的事情。所以想知道他们如何得到不同的结果?
$ g++ -std=c++1y -c -O3 ./round.cpp -o ./round.o
$ objdump -dS ./round.o
./round.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z7my_castd>:
0: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
5: c3 retq
6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
d: 00 00 00
0000000000000010 <_Z8my_roundd>:
10: 48 83 ec 08 sub [=12=]x8,%rsp
14: e8 00 00 00 00 callq 19 <_Z7my_castd+0x19> <========!!!
19: 48 83 c4 08 add [=12=]x8,%rsp
1d: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
22: c3 retq
Disassembly of section .text.startup:
0000000000000030 <_GLOBAL__sub_I__Z7my_castd>:
30: 48 83 ec 08 sub [=12=]x8,%rsp
34: bf 00 00 00 00 mov [=12=]x0,%edi
39: e8 00 00 00 00 callq 3e <_GLOBAL__sub_I__Z7my_castd+0xe>
3e: ba 00 00 00 00 mov [=12=]x0,%edx
43: be 00 00 00 00 mov [=12=]x0,%esi
48: bf 00 00 00 00 mov [=12=]x0,%edi
4d: 48 83 c4 08 add [=12=]x8,%rsp
51: e9 00 00 00 00 jmpq 56 <_Z8my_roundd+0x46>
我不确定 14
行 callq
的目的是什么,但是,即便如此,my_cast
和 my_round
似乎只是做一个 cvttsd2si
,我认为这是带截断的转换。
然而,正如我之前提到的,这两个函数在同一输入上产生不同的(正确的)值(比如 3.64
)
发生了什么事?
汇编输出更有用(g++ ... -S && cat round.s
):
...
_Z7my_castd:
.LFB225:
.cfi_startproc
cvttsd2siq %xmm0, %rax
ret
.cfi_endproc
...
_Z8my_roundd:
.LFB226:
.cfi_startproc
subq , %rsp
.cfi_def_cfa_offset 16
call round <<< This is what callq 19 means
addq , %rsp
.cfi_def_cfa_offset 8
cvttsd2siq %xmm0, %rax
ret
.cfi_endproc
如您所见,my_round
调用std::round
然后执行cvttsd2siq
指令。这是因为std::round(double)
returnsdouble
,所以它的结果还是要转成int64_t
。这就是 cvttsd2siq
在您的两个函数中所做的。
使用 g++,您可以使用 -fdump-tree-optimized
开关更高级地了解正在发生的事情:
$ g++ -std=c++1y -c -O3 -fdump-tree-optimized ./round.cpp
生成 round.cpp.165t.optimized
文件:
;; Function int64_t my_cast(double) (_Z7my_castd, funcdef_no=224, decl_uid=4743$
int64_t my_cast(double) (double d)
{
long int t;
<bb 2>:
t_2 = (long int) d_1(D);
return t_2;
}
;; Function int64_t my_round(double) (_Z8my_roundd, funcdef_no=225, decl_uid=47$
int64_t my_round(double) (double d)
{
double t;
int64_t _3;
<bb 2>:
t_2 = round (d_1(D));
_3 = (int64_t) t_2;
return _3;
}
这里的差异非常明显(并且对 round
函数的调用很明显)。
当使用 objdump -d
转储目标文件时,添加选项 -r
非常重要,它命令实用程序也转储重定位:
$ objdump -dr round.o
...
0000000000000010 <_Z8my_roundd>:
10: 48 83 ec 28 sub [=10=]x28,%rsp
14: e8 00 00 00 00 callq 19 <_Z8my_roundd+0x9>
15: R_X86_64_PC32 _ZSt5roundd
19: 48 83 c4 28 add [=10=]x28,%rsp
1d: f2 48 0f 2c c0 cvttsd2si %xmm0,%rax
现在,请注意出现的新行。那是包含在目标文件中的重定位指令。它指示链接器将 _Z8my_roundd+0x9
和 _ZSt5roundd
之间的距离添加到在偏移量 15 处找到的值。
偏移量14处的e8
是相对调用的操作码。接下来的 4 个字节必须包含被调用函数的 IP 相对偏移量(执行时指向下一条指令的 IP)。因为编译器不知道那个距离,所以它用零填充它并插入一个重定位以便链接器可以稍后填充它。
在没有 -r
选项的情况下进行反汇编时,重定位将被忽略,这会产生一种错觉,即函数 _Z8my_roundd
调用了自身的中间部分。