结构变量:按值传递与按指针传递给函数

Struct variable passed by value vs. passed by pointer to a function

提问人:BobDeTunis 提问时间:2/6/2023 最后编辑:BobDeTunis 更新时间:2/6/2023 访问量:151

问:

假设我有以下结构:

typedef struct s_tuple{
    double  x;
    double  y;
    double  z;
    double  w;
}   t_tuple;

假设我有以下两个函数:

t_tuple tuple_sub_values(t_tuple a, t_tuple b)
{
    a.x -= b.x;
    a.y -= b.y;
    a.z -= b.z;
    a.w -= b.w;
    return (a);
}

t_tuple tuple_sub_pointers(t_tuple *a, t_tuple *b)
{
    t_tuple c;

    c.x = a->x - b->x;
    c.y = a->y - b->y;
    c.z = a->z - b->z;
    c.w = a->w - b->w;
    return (c);
}

函数之间会有性能差异吗?其中一个比另一个好吗? 基本上,当所有结构元素都被调用时,按值传递与按指针传递的优缺点是什么?

编辑:完全改变了我的结构和功能,以给出一个更精确的例子 我找到了这个与我的问题相关的帖子,但是为了C++:https://stackoverflow.com/questions/40185665/performance-cost-of-passing-by-value-vs-by-reference-or-by-pointer#:~:text=In%20short%3A%20It%20is%20almost,reference%20parameters%20than%20value%20parameters

上下文:在这个例子中,我的结构并不大,但我正在编写一个光线追踪器,一些大小约为 100B 的结构可以被调用数百万次,所以我想尝试优化这些调用。我的结构有点混乱,所以在这里复制它们会很混乱,这就是为什么我试图在一种一般示例中提出我的问题。

C 按值传递

评论

1赞 Vlad from Moscow 2/6/2023
第一个函数定义没有意义,因为它的参数没有被使用。它立即被覆盖。
1赞 Mark Benningfield 2/6/2023
这两个版本在实际使用中都会很尴尬。
0赞 Olaf Dietsche 2/6/2023
也许看看标签信息按值传递,尤其是最后一句话:“但是,按值传递较大的结构也需要更多的资源”
1赞 BobDeTunis 2/6/2023
我的问题不是关于函数实现什么,而是处理器将如何执行这些函数。其中一个会比另一个更快吗?传递指针好还是值好?
2赞 Eric Postpischil 2/6/2023
性能问题通常很大程度上取决于环境。重要的是结构的实际组成是什么,而不仅仅是一个例子。重要的是对结构做了什么,是否需要更改原件,以及是否需要在更改原件之前阅读原件的任何部分。目标处理器型号很重要。您使用的 ABI 很重要。作为第一个近似值,通过引用传递一个大型结构(传递其地址)。如果被调用的函数不会更改它,请将参数设为指向 的指针。const

答:

0赞 AggelosT 2/6/2023 #1

在性能方面,这很可能是特定于实现的,原因与本文相去甚远,但最有可能的是,在最坏的情况下,我们谈论的是微秒。现在说到利弊:

  • 按值传递只会给你一个该结构的副本,并且修改将只是本地的。换句话说,您的函数将收到结构的全新副本,并且它只能修改该副本。

  • 相比之下,通过引用传递使您能够直接从函数修改给定的结构,并且经常在需要从函数返回多个值时出现。

完全由您决定选择适合您情况的哪一个。但要添加一些额外的帮助:

  • 按引用传递将减少函数调用开销,因为您不必从头开始将 32 个字节复制到新函数。如果您计划保持较低的内存占用量,如果您计划多次调用该函数,它也将有很大帮助。为什么?因为您无需为这些调用创建多个不同的结构,只需告诉每个调用重用相同的结构即可。这主要出现在游戏中,其中结构可能有数千字节大。

评论

0赞 BobDeTunis 2/6/2023
我在这里举了一个小例子,因为如果我复制整个代码,它太大了,但这正是我的立场。我正在编写光线追踪器,如果只是在 1000x1000 的屏幕上运行,某些结构将被调用数百万次
1赞 AggelosT 2/6/2023
在这种情况下,按引用传递应该可以正常工作。请记住,像这样的重型项目通常依赖于积极的优化,这可能是您对性能的最佳选择。
1赞 Petr Skocik 2/6/2023 #2

进入问题的核心:为了获得最佳的 arg 传递/值返回性能,您基本上希望遵循平台的 ABI,以尝试确保事物在寄存器中并保留在寄存器中。如果它们不在寄存器中,或者不能保留在寄存器中,那么通过指针传递大于指针大小的数据可能会节省一些复制(除非复制无论如何都需要在被调用者中完成:对于编码器来说,实际上可能比 }' 好一点)。void pass_copy(struct large x){ use(&x); }void pass_copy2(struct large const*x){ struct large cpy=*x; use(&cpy);

例如,sysv x86-64 ABI 的具体规则有点复杂(请参阅调用约定一章)。 但一个简短的版本可能是:args/return-vals 通过寄存器,只要它们的类型“足够简单”并且适当的参数传递寄存器可用(6 用于整数值,6 用于双精度)。最多两个 8 字节的结构可以通过寄存器(作为参数或返回值),前提是它们“足够简单”。

假设你的 double 已经加载到寄存器中(或者没有聚合到你可以指向被调用方的寄存器中),将它们传递到 x86-64 SysV ABI 的最有效方法是单独传递或通过每个两个 double 的结构,但你仍然需要通过内存返回它们,因为 ABI 只能容纳带有寄存器的两个双精度 retvals, 不是 4 双 retvals。如果返回 fourdouble,编译器将在调用方中堆栈分配内存,并将指向它的指针作为隐藏的第一个参数传递,然后返回指向分配的内存的指针(在后台)。更灵活的方法是不返回如此大的聚合,而是显式传递指向要填充的结构的指针。这样,结构可以位于您想要的任何位置(而不是由编译器自动分配到堆栈上)。t_tuples

所以像这样

void tuple_sub_values(t_tuple *retval, 
      t_twodoubles a0, t_twodoubles a1, 
      t_twodoubles b0, t_twodoubles b1);

将是一个更好的 API,用于避免 x86-64 SysV ABI(Linux、MacOS、BSD......

如果您的测量结果显示代码大小节省/性能提升对您来说是值得的,您可以将其包装在一个内联函数中,该函数将执行结构拆分。