如何在堆栈上返回超大结构? 显示早期存储到返回值对象而不是复制的示例

如何解决如何在堆栈上返回超大结构? 显示早期存储到返回值对象而不是复制的示例

said 从函数返回超大的 struct 值(而不是返回指向 struct 的指针)会导致堆栈上不必要的复制。 “过大”是指无法放入返回寄存器的 struct

然而,引用 Wikipedia

当需要一个超大的结构体返回时,另一个指向调用者提供的空间的指针作为第一个参数,将所有其他参数向右移动一个位置。

在返回结构体/类时,调用代码分配空间并通过堆栈上的隐藏参数传递指向该空间的指针。被调用的函数将返回值写入该地址。

看来至少在 x86 架构上,有问题的 struct 是由被调用者直接写入调用者指定的内存中的,那为什么会有副本呢?返回超大的 struct 真的会在堆栈上产生副本吗?

解决方法

这实际上取决于您的编译器,但通常的工作方式是调用者为结构体返回值分配内存,但被调用者也分配堆栈该结构的任何中间值的空间。这个中间分配在函数运行时使用,然后在函数返回时将结构复制到调用者的内存中。

有关为什么您的解决方案并不总是有效的参考,请考虑一个具有两个相同结构并根据某些条件返回一个的程序:

large_t returntype(int condition) {
  large_t var1 = {5};
  large_t var2 = {6};

  // More intermediate code here

  if(condition) return var1;
  else return var2;
}

在这种情况下,中间代码可能需要两者,但在编译时不知道返回值,因此编译器不知道在调用者的堆栈空间上初始化哪个。将其保留在本地并在返回时复制更容易。

编辑:您的解决方案可能适用于简单函数,但这实际上取决于每个编译器执行的优化。如果您真的对此感兴趣,请查看 https://godbolt.org/

,

如果函数内联,通过返回值对象的复制可以完全优化掉。否则,也许不会,并且 arg 复制绝对不可能。

看来至少在 x86 架构上,有问题的 struct 是由被调用者直接写入调用者指定的内存中的,那为什么会有副本呢?返回超大结构真的会导致堆栈上的复制吗?

这取决于调用者如何处理返回值,;如果它被分配给一个可证明的私有对象(转义分析),该对象可以作为返回值对象,作为隐藏指针传递。
但是如果调用者真的想将返回值分配给其他内存,那么它确实需要一个临时的。

struct large retval = some_func();   // no extra copying at all

*p = some_func()       // caller will make space for a local return-value object & copy.

(除非编译器知道 p 只是指向局部 struct large tmp;,并且转义分析可以证明某些全局变量不可能有指向同一个 tmp 的指针变种)


长版,同样的东西,更多细节:

在 C 抽象机中,有一个“返回值对象”,return foo 将命名变量 foo 复制到该对象,即使它是一个大结构。或者 return (struct lg){1,2}; 复制一个匿名结构。返回值对象本身是匿名的;没有什么可以取它的地址。 (你不能int *p = &foo(123);)。这样可以更轻松地进行优化。

在调用者中,该匿名返回值对象可以分配给您想要的任何内容,如果编译器没有优化任何内容,这将是另一个副本。 (所有这些都适用于任何类型,甚至 int)。当然,并非完全垃圾的编译器会避免部分(理想情况下是全部)复制,因为这样做不可能改变可观察到的结果。这取决于调用约定的设计。正如你所说,大多数约定,包括所有主流的 x86 和 x86-64 约定,都会传递一个“隐藏指针”arg 来表示他们出于任何原因选择不在寄存器中返回的返回值(大小、C++ 具有非平凡的构造函数)。

struct large retval = foo(...);

对于这样的调用约定,上面的代码有效转化为

struct large retval;
foo(&retval,...);

所以它的 C 返回值对象实际上 在其调用者的堆栈帧中是一个局部的。 foo() 可以在执行期间随时存储到该返回值对象中,包括在读取其他对象之前。这也允许在被调用者 (foo) 内进行优化,因此可以优化 struct large tmp = ... / return tmp 以仅存储到返回值对象中。

因此,当调用者只想将函数返回值分配给新声明的局部变量时,额外的复制为零。 (或者通过转义分析可以证明它仍然是私有的本地变量。即没有被任何全局变量指向)。


但是如果调用者想要将返回值存储在其他的地方怎么办?

void caller2(struct large *lgp) {
    *lgp = foo();
}

*lgp 可以作为返回值对象,还是需要引入一个本地临时对象?

void caller2(struct large *lgp) {
    // foo_asm(lgp);                        // nope,possibly unsafe
    struct large retval;  foo(&retval);  *lgp = retval;    // safe
}

如果您希望函数能够将大型结构写入任意位置,您必须通过在源代码中显示该效果来“签署”它。


显示早期存储到返回值对象(而不是复制)的示例

(所有源 + asm on the Godbolt compiler explorer)

// 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;
}

(针对 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

(-mregparm=1 表示传递 EAX 中的第一个参数,less noisy 并且比传递堆栈更容易快速直观地从堆栈空间中区分。有趣的事实:i386 Linux 使用 {{1} 编译内核但是有趣的事实 #2:如果在堆栈上传递了一个隐藏的指针(即没有 regparm),则该 arg 是被调用者弹出,与其余的不同。该函数将在弹出后使用 -mregparm=3 执行 ESP+=4将地址返回到 EIP。)

在一个简单的调用者中,编译器只保留一些堆栈空间,传递一个指向它的指针,然后可以从该空间加载成员变量。

ret 4
int caller() {
    struct large lg = {4,5};   // initializer is dead,foo can't read its retval object
    lg = foo();
    return lg.second;
}

但是有一个不那么琐碎的调用者:

caller:
        sub     esp,12
        mov     eax,esp
        call    foo
        mov     eax,dword ptr [esp + 4]
        add     esp,12
        ret
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;
}

使用 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 进行额外复制

*lgp = foo();
int caller2(struct large *lgp) {
    global_ptr = &lgp->first;
    *lgp = foo();
    return lgp->second;
}

复制到 # 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 需要发生,但从那里重新加载,而不是从 *lgp 重新加载有点错过了优化。 (以更多延迟为代价节省了一个字节的代码大小。)

Clang 使用两个 4 字节整数寄存器 [esp+12] 加载/存储进行复制,但其中一个是到 EAX 中,因此它已经准备好返回值。


您可能还想查看分配给使用 malloc 新分配的内存的结果。编译器知道没有其他东西可以(合法地)指向新分配的内存:这将是释放后使用的未定义行为。因此,如果尚未将其传递给其他任何对象,则它们可能允许将来自 mov 的指针作为返回值对象传递。


相关有趣的事实:按值传递大型结构总是需要一个副本(如果函数没有内联)。但正如评论中所讨论的,细节取决于调用约定。 Windows 不同于 i386 / x86-64 System V 调用约定(所有非 Windows 操作系统):

  • SysV 调用约定将整个结构复制到堆栈中。 (如果它们太大而无法放入一对 x86-64 寄存器中)
  • Windows x64 生成一个副本并传递(像普通 arg 一样)指向该副本的指针。被调用者“拥有” arg 并且可以修改它,因此仍然需要一个 tmp 副本。 (不,malloc 没有效果。)

https://godbolt.org/z/ThMrE9rqT 显示了针对 Linux 的 x86-64 GCC 与针对 Windows 的 x64 MSVC。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)> insert overwrite table dwd_trade_cart_add_inc > select data.id, > data.user_id, > data.course_id, > date_format(
错误1 hive (edu)> insert into huanhuan values(1,'haoge'); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive> show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 <configuration> <property> <name>yarn.nodemanager.res