nal*_*zok 6 c x86 struct abi calling-convention
据说从函数返回过大的值(而不是返回指向 的指针)会在堆栈上产生不必要的复制。我所说的“超大”是指无法放入返回寄存器的值。structstructstruct
不过,引用维基百科
当需要超大的结构返回时,另一个指向调用者提供的空间的指针将作为第一个参数添加到前面,将所有其他参数向右移动一个位置。
和
返回结构/类时,调用代码分配空间并通过堆栈上的隐藏参数传递指向该空间的指针。被调用函数将返回值写入该地址。
看起来至少在x86架构上,所讨论struct的问题是由被调用者直接写入调用者指定的内存中,那么为什么会有一个副本呢?返回超大的structs 真的会在堆栈上产生副本吗?
如果函数内联,则可以完全优化通过返回值对象进行的复制。否则,也许不会,并且 arg 复制绝对不可能。
看起来至少在x86架构上,所讨论的结构体是由被调用者直接写入调用者指定的内存中,那么为什么会有一个副本呢?返回超大结构是否真的会在堆栈上产生复制?
这取决于调用者如何处理返回值;如果它被分配给一个可证明的私有对象(逃逸分析),则该对象可以是返回值对象,作为隐藏指针传递。
但如果调用者实际上想要将返回值分配给其他内存,那么它确实需要一个临时的。
struct large retval = some_func(); // no extra copying at all
*p = some_func() // caller will make space for a local return-value object & copy.
Run Code Online (Sandbox Code Playgroud)
(除非编译器知道这p只是指向 local struct large tmp;,并且转义分析可以证明某些全局变量不可能拥有指向同一tmpvar 的指针。)
长版,同样的内容,但有更多细节:
在 C 抽象机中,有一个“返回值对象”,并将return foo命名变量复制foo到该对象,即使它是一个大型结构。或者return (struct lg){1,2};复制一个匿名结构。返回值对象本身是匿名的;没有任何东西可以占据它的地址。(你不能int *p = &foo(123);)。这使得优化变得更加容易。
在调用者中,该匿名返回值对象可以分配给您想要的任何内容,如果编译器没有优化任何内容,这将是另一个副本。(所有这些都适用于任何类型,甚至int)。当然,不是完全垃圾的编译器将避免一些(最好是全部)复制,因为这样做不可能改变可观察的结果。这取决于调用约定的设计。正如你所说,大多数约定,包括所有主流 x86 和 x86-64 约定,都会传递一个“隐藏指针”arg 来返回它们选择不在寄存器中返回的值,无论出于何种原因(大小、C++ 具有一个不平凡的值)构造函数)。
struct large retval = foo(...);
Run Code Online (Sandbox Code Playgroud)
对于这样的调用约定,上面的代码有效地转换为
struct large retval;
foo(&retval, ...);
Run Code Online (Sandbox Code Playgroud)
所以它的 C 返回值对象实际上是其调用者的堆栈帧中的本地对象。 foo()允许在执行期间随时存储到该返回值对象中,包括在读取某些其他对象之前。foo这也允许在被调用者 () 内进行优化,因此可以优化 /以仅存储到返回值对象中。struct large tmp = ...return tmp
因此,当调用者只想将函数返回值分配给新声明的本地变量时,额外的复制为零。(或者通过转义分析可以证明它仍然是私有的局部变量。即没有被任何全局变量指向)。
但是如果调用者想要将返回值存储在其他地方怎么办?
void caller2(struct large *lgp) {
*lgp = foo();
}
Run Code Online (Sandbox Code Playgroud)
可以*lgp是返回值对象,还是需要引入一个本地临时对象?
void caller2(struct large *lgp) {
// foo_asm(lgp); // nope, possibly unsafe
struct large retval; foo(&retval); *lgp = retval; // safe
}
Run Code Online (Sandbox Code Playgroud)
如果您希望函数能够将大型结构写入任意位置,则必须通过在源代码中显示该效果来“签核”它。
什么阻止使用函数参数作为隐藏指针?*lgp有关为什么不能是返回值对象/隐藏指针的更多详细信息“允许函数假设其返回值对象(由隐藏指针指向)与其他对象不同”。还有关于是否struct large *restrict lgp会使其安全的详细信息:如果函数不 longjmp ,则可能是的(否则存储到所谓的匿名 retval 对象可能最终会产生可见的副作用而未达到return),但 GCC 不会寻求这种优化。
为什么不对类 MEMORY 类型执行尾调用优化? - return bar()bar 返回相同的结构应该可以作为优化的尾部调用,从而导致额外的复制。这甚至可能会引入整个结构的额外复制,以及无法优化call bar/ret到jmp bar.
C 编译器如何在 ASM 中处理函数的结构返回值- 在寄存器中返回的阈值。例如,i386 System V总是返回内存中的结构,甚至struct {int x;};.
C/C++ 在幕后按值返回结构是一个实际示例(但不幸的是使用调试模式编译器生成的 asm,因此它包含不必要的复制)。
对象在 x86 中如何在汇编级别工作?底部的示例说明了 x86-64 System V 如何将结构体的字节打包到 RDX:RAX 中,如果少于 8 个字节,则仅打包到 RAX 中。
(Godbolt 编译器浏览器上的所有源代码 + asm)
// more or less extra size will get compilers to copy it around with SSE2 or not
struct large { int first, second; char pad[0];};
int *global_ptr;
extern int a;
NOINLINE // __attribute__((noinline))
struct large foo() {
struct large tmp = {1,2};
if (a)
tmp.second = *global_ptr;
return tmp;
}
Run Code Online (Sandbox Code Playgroud)
(针对 GNU/Linux)clang-m32 -O3 -mregparm=1创建了一个实现,该实现在完成读取其他所有内容之前写入其返回值对象,正是这种情况使得调用者将指针传递到某些全局可访问的内存是不安全的。
asm 明确表示tmp已完全优化,或者是retval 对象。
# clang -O3 -m32 -mregparm=1
foo:
mov dword ptr [eax + 4], 2
mov dword ptr [eax], 1 # store tmp into the retval object
cmp dword ptr [a], 0
je .LBB0_2 # if (a == 0) goto ret
mov ecx, dword ptr [global_ptr] # load the global
mov ecx, dword ptr [ecx] # deref it
mov dword ptr [eax + 4], ecx # and store to the retval object
.LBB0_2:
ret
Run Code Online (Sandbox Code Playgroud)
(-mregparm=1意味着在 EAX 中传递第一个参数,与在堆栈上传递相比,噪音更小,并且更容易在视觉上快速区分堆栈空间。有趣的事实:i386 Linux 使用 编译内核-mregparm=3。但有趣的事实 #2:如果在堆栈(即没有 regparm),该 arg 是被调用者弹出,与其余部分不同。该函数将ret 4在将返回地址弹出到 EIP 后执行 ESP+=4。)
在简单的调用程序中,编译器仅保留一些堆栈空间,将指针传递给它,然后可以从该空间加载成员变量。
int caller() {
struct large lg = {4, 5}; // initializer is dead, foo can't read its retval object
lg = foo();
return lg.second;
}
Run Code Online (Sandbox Code Playgroud)
caller:
sub esp, 12
mov eax, esp
call foo
mov eax, dword ptr [esp + 4]
add esp, 12
ret
Run Code Online (Sandbox Code Playgroud)
但对于一个不那么简单的调用者:
int caller() {
struct large lg = {4, 5};
global_ptr = &lg.first;
// unknown(&lg); // or this: as a side effect, might set global_ptr = &tmp->first;
lg = foo(); // (except by inlining) the compiler can't know if foo() looks at global_ptr
return lg.second;
}
Run Code Online (Sandbox Code Playgroud)
caller:
sub esp, 28 # reserve space for 2 structs, and alignment
mov dword ptr [esp + 12], 5
mov dword ptr [esp + 8], 4 # materialize lg
lea eax, [esp + 8]
mov dword ptr [global_ptr], eax # point global_ptr at it
lea eax, [esp + 16] # hidden first arg *not* pointing to lg
call foo
mov eax, dword ptr [esp + 20] # reload from the retval object
add esp, 28
ret
Run Code Online (Sandbox Code Playgroud)
*lgp = foo();int caller2(struct large *lgp) {
global_ptr = &lgp->first;
*lgp = foo();
return lgp->second;
}
Run Code Online (Sandbox Code Playgroud)
# with GCC11.1 this time, SSE2 8-byte copying unlike clang
caller2: # incoming arg: struct large *lgp in EAX
push ebx #
mov ebx, eax # lgp, tmp89 # lgp needed after foo returns
sub esp, 24 # reserve space for a retval object (and waste 16 bytes)
mov DWORD PTR global_ptr, eax # global_ptr, lgp
lea eax, [esp+8] # hidden pointer to the retval object
call foo #
movq xmm0, QWORD PTR [esp+8] # 8-byte copy of both halves
movq QWORD PTR [ebx], xmm0 # *lgp_2(D), tmp86
mov eax, DWORD PTR [ebx+4] # lgp_2(D)->second, lgp_2(D)->second # reload int return value
add esp, 24
pop ebx
ret
Run Code Online (Sandbox Code Playgroud)
需要进行复制*lgp,但从那里而不是从 重新加载有点错过了优化[esp+12]。(以更多延迟为代价节省一个字节的代码大小。)
Clang 使用两个 4 字节整数寄存器加载/存储进行复制mov,但其中之一进入 EAX,因此它已经准备好返回值。
您可能还想查看分配给使用 malloc 新分配的内存的结果。编译器知道没有其他东西可以(合法地)指向新分配的内存:这将是释放后使用未定义的行为。malloc因此,如果尚未将指针传递给其他任何对象,则它们可能允许将指针作为返回值对象传递。
相关有趣的事实:按值传递大型结构总是需要一个副本(如果函数不是内联的)。但正如评论中所讨论的,细节取决于调用约定。Windows 与 i386 / x86-64 System V 调用约定(所有非 Windows 操作系统)的不同之处在于:
const struct large foo没有效果。)https://godbolt.org/z/ThMrE9rqT显示了针对 Linux 的 x86-64 GCC 与针对 Windows 的 x64 MSVC。
这实际上取决于您的编译器,但一般来说,其工作方式是调用者为结构返回值分配内存,但被调用者也为该结构的任何中间值分配堆栈空间。在函数运行时使用此中间分配,然后在函数返回时将结构复制到调用者的内存中。
作为参考,为什么您的解决方案并不总是有效,请考虑一个具有两个相同结构并根据某些条件返回一个的程序:
large_t returntype(int condition) {
large_t var1 = {5};
large_t var2 = {6};
// More intermediate code here
if(condition) return var1;
else return var2;
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,中间代码可能需要两者,但返回值在编译时未知,因此编译器不知道在调用者的堆栈空间上初始化哪一个。将其保存在本地并在返回时复制会更容易。
编辑:您的解决方案可能是简单函数的情况,但它实际上取决于每个单独的编译器执行的优化。如果你真的对此感兴趣,请查看https://godbolt.org/
| 归档时间: |
|
| 查看次数: |
1172 次 |
| 最近记录: |