Dlang - 在汇编中理解 std.cycle()

Dlang - Understaning std.cycle() in assembly

import std.range : cycle;
void foo() pure @safe {
    cycle([1, 2]);
}

今天遇到一个D语言写的程序。我试图从一个简单的函数开始理解它的汇编代码。

来自the asm output on the D compiler explorer

pure nothrow @nogc @safe std.range.Cycle!(int[]).Cycle std.range.cycle!(int[]).cycle(int[]):
 push   rbp
 mov    rbp,rsp
 sub    rsp,0x40
 mov    QWORD PTR [rbp-0x20],rdi
 mov    QWORD PTR [rbp-0x10],rsi
 mov    QWORD PTR [rbp-0x8],rdx
 ... rest of the function

我试过好几次了,但不明白为什么 std.range.cycle() 得到 3 个参数(RDIRSIRDX),或者我的范围是 ([1, 2])。不是类C的结构吗?

还是我遗漏了什么?

看起来您正在使用基于 rdi 和 rsi 的 x86-64 SystemV ABI 进行 arg 传递,因为 Windows 64 位 ABI 使用不同的 regs。参见 tag wiki for links to ABI docs, or see the current revision here.

按值传递的小对象(如结构)进入多个整数寄存器。按值返回大对象(超过 128 位)也使用调用者分配的指向 space 的隐藏指针,而不是打包到 RDX:RAX。这就是您的函数中发生的事情。

基于asm和docs,我认为一个Cycle对象具有三个值:start、end和index。我根本不知道 D,但这很有意义。由于它们都是 64 位的,这使得它太大而无法放入 RDX:RAX,因此它由隐藏指针 return 编辑。

进入 Cycle() 的参数传递寄存器是

  • RDI:"hidden" 指向 return 值的指针(它是三个 64 位整数的结构)
  • RSI:范围 arg 的第一个成员(我称之为 range_start)
  • RDX:Range arg 的第二个成员(我称之为 range_end)

我启用了优化以获得更具可读性的 asm 而没有那么多噪音,但不幸的是,看起来这个 D 编译器不如 clang 或 gcc 复杂。使用 -O -release -inline (as recommended by this page),它仍然对堆栈执行 lot of store/reload。

pure nothrow @nogc @safe std.range.Cycle!(int[]).Cycle std.range.cycle!(int[]).cycle(int[]):
 sub    rsp,0x28
 mov    QWORD PTR [rsp+0x20],rdi        # hidden first arg (return-value pointer).
 mov    QWORD PTR [rsp+0x8],0x0         # totally useless: overwritten without read

 mov    QWORD PTR [rsp+0x10],0x0        # totally useless: same.

 mov    QWORD PTR [rsp+0x8],rsi         # first "real" arg
 mov    QWORD PTR [rsp+0x10],rdx        # second "real" arg
 xor    eax,eax
 xor    edx,edx                         # zero rax:rdx.  Perhaps from the index=0 default when you only use one arg?
 div    QWORD PTR [rsp+0x8]             # divide 0 by first arg of the range.
 mov    QWORD PTR [rsp+0x18],rdx        # remainder of (index / range_start), I guess.
 lea    rsi,[rsp+0x8]                   # RSI=pointer to where range_start, range_end, and index/range_start were stored on the stack.
 movs   QWORD PTR es:[rdi],QWORD PTR ds:[rsi]  # copy to the dst buffer.  A smart compiler would have stored there in the first place, instead of to local scratch and then copying.
 movs   QWORD PTR es:[rdi],QWORD PTR ds:[rsi]  # movs is not very efficient, this is horrible code.
 movs   QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
 mov    rax,QWORD PTR [rsp+0x20]        # mov rax, rdi  before those MOVS instructions would have been much more efficient.
 add    rsp,0x28
 ret    

ABI 需要 return 大对象到 return RAX 中隐藏指针的函数,因此调用者不必单独保留指向 return 缓冲。这就是该函数完全设置 RAX 的原因。


一个好的编译器会这样做:

std.range.Cycle...:
   mov    [rdi], rsi           # cycle_start
   mov    [rdi+0x8], rdx       # cycle_end
   mov    [rdi+0x10], 0        # index
   mov    rax, rdi
   ret

或者完全内联对 Cycle 的调用,因为它很简单。实际上,我认为它 确实 内联到 foo() 中,但是仍然发出了 cycle() 的独立定义。

我们无法判断 foo() 调用了哪两个函数,因为编译器资源管理器似乎在不解析符号的情况下反汇编 .o(不是链接的二进制文件)。所以调用偏移量是00 00 00 00,链接器的占位符。但它可能正在调用内存分配函数,因为它使用 esi=2 和 edi=0 进行调用。 (). The call target shows as the next instruction, because that's where call's rel32 displacement 数自.

希望 LDC or GDC do a better job, since they're based on modern optimizing backends (LLVM and gcc), but the compiler-explorer site you linked doesn't have those compilers installed. If there's another site based on Matt Godbolt's compiler explorer code,但对于其他 D 编译器,那会很酷。