为什么在嵌入式系统代码中通过memcpy复制结构?

时间:2011-07-27 09:48:29

标签: c struct embedded

在用于复制相同类型结构的嵌入式软件域中,人们不使用直接赋值,而是通过memcpy()函数或每个元素复制来执行此操作。

让我们举个例子

struct tag
{

int a;

int b;
};

struct tag exmple1 = {10,20};

struct tag exmple2;

将exmple1复制到exmple2 .. 而不是直接写

exmple2=exmple1;

人们使用

memcpy(exmple2,exmple1,sizeof(struct tag));

exmple2.a=exmple1.a; 
exmple2.b=exmple1.b;

为什么????

9 个答案:

答案 0 :(得分:15)

对于嵌入式系统,没有任何具体内容可以使这一点变得危险,语言语义对于所有平台都是相同的。

C已经在嵌入式系统中使用了很多年,早期的C编译器在ANSI / ISO标准化之前不支持直接结构分配。许多从业者要么来自那个时代,要么是由那些曾经或正在使用这些从业者编写的遗留代码的人教授的。这可能是疑问的根源,但它不是ISO兼容实现的问题。在一些非常受资源限制的目标上,由于多种原因,可用的编译器可能不完全符合ISO标准,但我怀疑这个功能会受到影响。

一个问题(适用于嵌入式和非嵌入式)是在分配结构时,实现不需要复制任何未定义的填充位的值,因此如果执行了结构赋值,然后执行了{ {1}}而不是逐个成员比较来测试相等性,不能保证它们是平等的。但是,如果执行memcmp(),则将复制任何填充位,以便memcpy()和逐个成员比较将产生相等性。

因此,在所有情况下(不仅仅是嵌入式)使用memcmp()可能更安全,但改进是微不足道的,并且不利于可读性。这是一个奇怪的实现,没有使用最简单的结构赋值方法,这是一个简单的memcpy(),因此不太可能发生理论上的不匹配。

答案 1 :(得分:4)

在您给定的代码中,即使您写下来也没有问题:

example2 = example1;

但是假设将来,struct定义更改为:

struct tag
{
  int a[1000];
  int b;
};

现在,如果您执行上面的赋值运算符,那么(某些)编译器可能会逐字节(或int by int)复制内联代码。即。

example1.a[0] = example.a[0];
example1.a[1] = example.a[1];
example1.a[2] = example.a[2];
...

会在您的代码段中生成code bloat。这种记忆错误并不容易找到。这就是人们使用memcpy的原因。

[但是,我听说现代编译器有足够的能力在内部使用memcpy时遇到这样的指令,特别是对于POD。]

答案 2 :(得分:4)

通过memcpy()复制C结构经常被几十年前学过C并且之后没有遵循标准化过程的程序员使用。他们很简单,不知道C支持结构分配(所有ANSI-C89前编译器都没有直接结构分配)。

当他们了解此功能时,有些人仍然坚持memcpy()方式,因为这是他们的习惯。还有一些动机源于cargo cult programming,例如据称memcpy只是更快 - 当然 - 没有能够通过基准测试案例支持这一点。

结构也是memcpy()一些新手程序员,因为它们要么将结构赋值与结构指针的分配混淆 - 要么只是过度使用memcpy()(它们通常也使用memcpy()其中strcpy()更合适。

还有memcmp()结构比较反模式,有些程序员有时会引用它来使用memcpy()而不是结构赋值。这背后的原因如下:由于C不会自动为结构生成==运算符并且编写自定义结构比较函数很繁琐,memcmp()用于比较结构。在下一步 - 为了避免比较结构的padding位的差异 - memset(...,0,...)用于初始化所有结构(而不​​是使用C99初始化器语法或单独初始化所有字段)和{{1}用于复制结构!因为memcpy()也复制了填充位的内容......

但请注意,由于以下几个原因,这种推理存在缺陷:

  • 使用memcpy() / memcpy() / memcmp()会引入新的错误可能性 - 例如提供错误的尺寸
  • 当结构包含整数字段时,memset()下的排序在大端架构和小端架构之间发生变化
  • 大小为memcmp()的{​​{1}}的字符数组字段n - 在位置0处终止也必须在位置x之后将所有元素归零 - 否则为2否则相等的结构比较不等
  • 从寄存器到字段的赋值也可以将相邻填充位设置为不等于0的值,因此,在与其他相同结构进行比较之后会产生不相等的结果

最后一点最好用一个小例子说明(假设架构X):

x

最后一个断言的失败可能取决于代码重新排序,编译器的更改,编译器选项的更改以及类似的东西。

结论

作为一般规则:增加代码的正确性和可移植性使用直接结构赋值(而不是struct S { int a; // on X: sizeof(int) == 4 char b; // on X: 24 padding bits are inserted after b int c; }; typedef struct S S; S s1; memset(&s1, 0, sizeof(S)); s1.a = 0; s1.b = 'a'; s1.c = 0; S s2; memcpy(&s2, &s1, sizeof(S)); assert(memcmp(&s1, &s2, sizeof(S)==0); // assertion is always true s2.b = 'x'; assert(memcmp(&s1, &s2, sizeof(S)!=0); // assertion is always true // some computation char x = 'x'; // on X: 'x' is stored in a 32 bit register // as least significant byte // the other bytes contain previous data s1.b = x; // the complete register is copied // i.e. the higher 3 register bytes are the new // padding bits in s1 assert(memcmp(&s1, &s2, sizeof(S)==0); // assertion is not always true ),C99结构初始化语法(而不是memcpy())和自定义比较函数(而不是{ {1}})。

答案 3 :(得分:2)

在C中,人们可能会这样做,因为他们认为memcpy会更快。但我不认为这是真的。编译器优化会解决这个问题。

在C ++中,由于用户定义的赋值运算符和复制构造函数,它也可能具有不同的语义。

答案 4 :(得分:2)

除了其他人写了一些额外的观点之外:

  • 使用memcpy而不是简单的赋值给维护代码的人提示操作可能很昂贵。在这些情况下使用memcpy将提高对代码的理解。

  • 嵌入式系统通常在编写时考虑了可移植性和性能。可移植性非常重要,因为即使原始设计中的CPU不可用或者更便宜的微控制器可以执行相同的工作,您也可能希望重新使用代码。

    现在,低端微控制器的发展速度比编译器开发人员赶上的要快,因此使用简单的字节复制循环而不是针对结构分配优化的编译器的情况并不少见。随着向32位ARM内核迁移,对于大部分嵌入式开发人员来说并非如此。然而,很多人都在制造针对模糊8位和16位微控制器的产品。

  • 针对特定平台调整的memcpy可能比编译器生成的更优化。例如,在具有闪存中的结构的嵌入式平台上是常见的。从闪存读取并不像写入它那么慢,但它仍然比从RAM到RAM的普通拷贝慢很多。优化的memcpy功能可以使用DMA或闪存控制器的特殊功能来加速复制过程。

答案 5 :(得分:1)

这完全是胡说八道。使用您喜欢的任何方式。最简单的是:

exmple2=exmple1;

答案 6 :(得分:1)

无论你做什么,都不要这样做:

exmple2.a=exmple1.a; 
exmple2.b=exmple1.b;

它带来了可维护性问题,因为任何人在结构中添加成员时,都必须添加一行代码来执行该成员的副本。有人会忘记这样做,这将导致很难找到错误。

答案 7 :(得分:1)

在某些实现中,执行memcpy()的方式可能与执行“正常”结构分配的方式不同,其方式在某些狭窄的上下文中可能很重要。例如,一个或另一个结构操作数可能是未对齐的,并且编译器可能不知道它(例如,一个存储器区域可能具有外部链接并且在用不同语言编写的模块中定义,该模块没有强制对齐的手段)。如果编译器支持这种声明,则使用__packed声明会更好,但并非所有编译器都支持。

使用除结构赋值之外的其他东西的另一个原因可能是特定实现的memcpy可以按照与某些类型的volatile源或目标正确工作的顺序访问其操作数,而该实现的struct assignment可能使用不起作用的不同序列。这通常不是使用memcpy的好理由,但是,除了对齐问题(memcpy在任何情况下都需要正确处理),memcpy的规范不会很多关于如何执行操作的承诺。最好使用一个特殊编写的例程,该例程完全按照要求执行操作(例如,如果目标是一块需要使用四个8位写入而不是一个32位写入4字节结构数据的硬件-bit写道,应该编写一个执行该操作的例程,而不是希望未来版本的memcpy决定“优化”操作。)

在某些情况下使用memcpy的第三个原因是编译器通常使用直接的加载和存储序列执行小型结构分配,而不是使用库例程。在某些控制器上,这需要的代码量可能会有所不同,具体取决于结构在内存中的位置,以及加载/存储序列最终可能比memcpy调用更大的程度。例如,在具有1K字代码空间和192字节RAM的PICmicro控制器上,从bank 1到bank 0处理4字节结构将需要16条指令。 memcpy调用需要八到九次(取决于countunsigned char还是int [只有192字节的RAM,unsigned char应该绰绰有余!但是请注意,调用memcpy-ish例程假定硬编码大小并且要求两个操作数都在RAM而不是代码空间中只需要五个指令来调用,并且可以通过使用a来减少到四个全局变量。

答案 8 :(得分:0)

第一版是完美的。
第二个可用于速度(没有理由你的尺寸) 仅当目标和源的填充不同时才使用第3个。