在C中通过值传递结构而不是传递指针有不利之处吗?

时间:2020-03-06 15:01:14  来源:igfitidea点击:

在C中通过值传递结构而不是传递指针有不利之处吗?

如果结构很大,显然有复制大量数据的性能方面,但是对于较小的结构,它基本上应该与将多个值传递给函数相同。

当用作返回值时,它甚至可能更有趣。 C仅具有从函数返回的单个值,但我们通常需要多个返回值。因此,一个简单的解决方案是将它们放在结构中并返回该结构。

是否有任何理由支持或者反对?

由于可能不是所有人都知道我在这里说的是什么,所以我举一个简单的例子。

如果我们使用C进行编程,则迟早会开始编写如下所示的函数:

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

这不是问题。唯一的问题是,我们必须与同事达成共识,在该同事中,参数应为顺序,以便在所有功能中使用相同的约定。

但是,当我们想返回相同类型的信息时会发生什么呢?我们通常会得到以下内容:

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

这可以很好地工作,但是存在更多的问题。返回值是一个返回值,除了在此实现中不是。从上面无法得知,不允许函数get_data查看len指向什么。没有什么可以使编译器检查通过该指针实际返回的值。因此,下个月,当其他人修改了代码却没有正确理解它(因为他没有阅读文档?)时,它就崩溃了,没有人注意,或者它开始崩溃。

所以,我提出的解决方案是简单的结构

struct blob { char *ptr; size_t len; }

可以这样重写示例:

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

出于某种原因,我认为大多数人会本能地使inspect_data指向结构blob的指针,但是我不明白为什么。它仍然有一个指针和一个整数,更清楚的是它们在一起。在get_data的情况下,不可能以我之前描述的方式弄乱,因为长度没有输入值,必须有返回的长度。

解决方案

我要说的是,按值传递(不是太大)结构(既作为参数又作为返回值)是一种完全合法的技术。当然,必须注意该结构是POD类型,或者复制语义已明确指定。

更新:对不起,我的C ++思维上限很高。我记得有一次在C中从函数返回结构是不合法的,但是从那时起这可能已经改变了。只要我们希望使用的所有编译器都支持这种做法,我仍然会说它是有效的。

我认为问题已经很好地总结了一切。

通过值传递结构的另一个优点是内存所有权是显式的。毫无疑问,该结构是否来自堆,谁负责释放它。

简单的解决方案是将错误代码作为返回值返回,而将其他所有内容作为函数中的参数返回,
当然,此参数可以是一个结构,但看不到任何通过值传递此值的特殊优势,只是发送了一个指针。
通过值传递结构是危险的,我们需要非常小心传递的内容是什么,请记住C中没有复制构造函数,如果结构参数之一是指针,那么将要复制的指针值可能会非常令人困惑并且难以理解。维持。

只是为了完整地回答问题(对Roddy的信任),堆栈使用率是不按值传递结构的另一个原因,相信我调试堆栈溢出是真正的PITA。

重播以发表评论:

通过指针传递struct意味着某些实体对该对象拥有所有权,并且对什么以及何时释放应该有完全的了解。通过值传递struct会创建一个对struct内部数据的隐藏引用(指向另一个结构等的指针),这在此情况下很难维护(可能,但为什么呢?)。

对于较小的结构(例如,点,矩形),按值传递是完全可以接受的。但是,除了速度外,还有另一个原因要引起我们注意,必须按值传递/返回大型结构:堆栈空间。

许多C编程都是针对嵌入式系统的,这些系统的内存非常宝贵,堆栈大小可能以KB甚至字节为单位进行度量...如果按值传递或者返回结构,则这些结构的副本将被放置在堆栈,可能导致此站点以...命名的情况。

如果我看到一个应用程序似乎有过多的堆栈使用情况,那么按值传递的结构是我首先要寻找的东西之一。

到目前为止,这里的人们已经忘记提及(或者我忽略了它)的一件事是,结构通常具有填充!

struct {
  short a;
  char b;
  short c;
  char d;
}

每个字符为1个字节,每个短为2个字节。结构有多大?不,不是6个字节。至少没有在任何更常用的系统上。在大多数系统上,它是8. 问题是,对齐方式不是恒定的,它取决于系统,因此相同的结构在不同的系统上将具有不同的对齐方式和大小。

填充不仅会进一步消耗堆栈,还增加了无法提前预测填充的不确定性,除非我们知道系统如何填充然后查看应用程序中的每个结构并计算大小为了它。传递指针需要可预测的空间量-没有不确定性。指针的大小对于系统是已知的,无论结构看起来如何,指针总是相等的,并且指针大小始终以对齐方式进行选择,而无需填充。

不这样做的一个未提及的原因是,这可能会导致二进制兼容性很重要的问题。

根据所使用的编译器,可以根据堆栈选项或者实现通过堆栈或者寄存器传递结构。

请参阅:http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return
  
  -freg-struct-return

如果两个编译器不同意,事情可能会崩溃。不用说,未说明这样做的主要原因是堆栈消耗和性能原因。

要真正回答这个问题,需要深入了解组装领域:

(以下示例在x86_64上使用gcc。欢迎任何人添加其他架构,例如MSVC,ARM等。)

让我们看一下示例程序:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

进行全面优化编译

gcc -Wall -O3 foo.c -o foo

看一下程序集:

objdump -d foo | vim -

这是我们得到的:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    ##代码##x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    ##代码##x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

除nopl填充外,give_two_doubles()具有27个字节,而give_point()具有29个字节。另一方面," give_point()"产生的指令比" give_two_doubles()"少

有趣的是,我们注意到编译器已经能够将" mov"优化为更快的SSE2变体" movapd"和" movsd"。而且,give_two_doubles()实际上是将数据移入和移出内存,这会使事情变慢。

显然,其中许多可能不适用于嵌入式环境(当今大多数时间都是C的竞争环境)。我不是组装向导,因此欢迎任何评论!