【问题标题】:Is there still a performance advantage to redefine standard like memcpy?像 memcpy 这样重新定义标准是否还有性能优势?
【发布时间】:2019-07-16 08:31:04
【问题描述】:

我的问题很简单,但我找不到明确的答案,所以我来了。

如今的 C 编译器比几年前更高效。在新项目中重新定义 memcpy 或 memset 之类的函数还有什么好处吗?

更具体地说,我们假设项目中的目标 MCU 是 32 位 ARM 内核,例如 Cortex M 或 A。并且使用了 GNU ARM 工具链。

谢谢

【问题讨论】:

  • 您是否查看过编译器生成的汇编器是否可以做得更好?
  • 在现代平台/实现上只使用memcpy,编译器应该负责任何优化,包括在必要时省略对memcpy的实际调用。查看生成的汇编代码。你可能会发现这个网站很有用:godbolt.org
  • memcpymemset 是标准 C 库的一部分。看看他们的源代码。对于大多数处理器架构,这两个函数都具有高度优化的汇编器实现。
  • 例如:GNU ARM 工具链使用 newlib 标准 C 实现。它包含几个用于 ARM 32 位架构的 memcpy 汇编器实现:chromium.googlesource.com/native_client/nacl-newlib/+/refs/…
  • 我对这里的“仍然”一词有疑问。滚动自己有性能优势从来都不是普遍正确的。这取决于您如何实现它以及您的实现是否比库实现者更好。总是有可用的用汇编程序编写的手动优化的目标特定库。 Newlib 不是一个。

标签: c optimization embedded


【解决方案1】:

不,重新定义memcpy 没有好处。问题是您的自己的函数不能像标准库memcpy那样工作,因为C编译器知道名称为memcpy的函数是(C11 7.24.2.1p2)的函数

[...] 将n 字符从s2 指向的对象复制到s1 指向的对象中。如果复制发生在重叠的对象之间,则行为未定义。

并且明确允许构造任何表现as if的等效程序,这样的函数被调用。有时甚至会导致代码甚至不接触内存,memcpy 被寄存器副本替换,或者使用未对齐的加载指令将值从内存加载到寄存器中。

如果您在汇编程序中定义自己的superduperfastmemcpy,C 编译器将不知道它的作用,并且会在被要求时随意调用它。


的好处是有一个特殊的例程来复制 large 内存块,例如众所周知,源地址和目的地址都可以被 1k 整除,并且所有长度总是可以被 1k 整除;在这种情况下,可能有几个替代例程可以在程序启动时计时,并选择使用最快的一个。当然,到处复制大量内存是糟糕的设计...

【讨论】:

  • 但是,如果您定义自己的 memcpy 并使用相同的符号名称 override 库函数,我希望编译器仍然能够提供优化不调用覆盖。
  • @Clifford 如果您用相同的名称定义自己的 memcpy,则行为未定义 - 编译器无需调用您的 memcpy。
  • 可能未由语言定义定义(尽管我相信你的话),但在这种情况下(gnu arm,Newlib),行为是明确的(并由链接器的行为决定) .是的,编译器可能不会调用覆盖,原因与它可能不会像您概述的那样调用库实现的原因相同——这正是我的观点。
【解决方案2】:

这个问题只能作为一个意见问题来回答,因为您已经具体了解了目标和工具链。不可能一概而论(而且从来没有)。

GNU ARM 工具链使用 Newlib C 库。 Newlib 设计为与架构无关且可移植。因此,它是用 C 语言而不是汇编语言编写的,因此它的性能取决于编译器的代码生成,进而取决于构建库时应用的编译器选项。可以为非常特定的 ARM 架构构建,或者为更通用的 ARM 指令子集构建;这也会影响性能。

此外,Newlib 本身可以使用各种条件编译选项构建,例如 PREFER_SIZE_OVER_SPEED__OPTIMIZE_SIZE__

现在,如果您能够生成比编译器更好的 ARM 汇编代码(并且有时间),那就太好了,但这种功夫编码技能越来越少见,坦率地说,越来越没有必要了。你有足够的汇编专业知识来击败编译器吗?你有时间吗,你真的想为你可能使用的每一个架构都这样做吗?这可能是一个过早的优化,而且效率很低。

在某些情况下,在具有此功能的目标上,可能值得设置内存到内存的 DMA 传输。 GNU ARM 编译器不会生成 DMA 代码,因为这取决于芯片供应商,而不是 ARM 架构的一部分。但是memcpy 是用于任意复制大小对齐和线程安全的通用目的。对于 DMA 最佳的特定情况,最好定义一个新的不同名称的例程并在需要的地方使用它,而不是重新定义 memcpy 并冒险它对于可能占主导地位的小型副本或多线程应用程序来说是次优的。

memcpy()在Newlib中的实现例如可以看到here。这是一个合理的惯用实现,因此对典型的编译器优化器表示同情,它通常在惯用代码上工作得最好。替代实现可能在未优化的编译中表现更好,但如果它“不寻常”,优化器可能无法正常工作。如果你用汇编程序编写它,你只需要比编译器更好 - 你将是一种罕见但不一定有价值(商业)的商品。也就是说,看看这个特定的实现,在速度超过尺寸的实现中,对于大型未对齐的块来说,它的效率确实要低得多。有可能以一些小成本改进它,也许是更常见的对齐副本。

【讨论】:

    【解决方案3】:

    memcpy 之类的函数属于标准库,几乎可以肯定它们是用汇编程序实现的,而不是用 C 语言实现的。

    如果你重新定义它们,它肯定会运行得更慢。如果你想优化memcpy,你应该使用memmove,或者将指针声明为restrict,以告知它们不重叠,并像memmove一样快速处理它们。

    那些为给定架构编写标准 C 库的工程师肯定会使用现有的汇编器函数来更快地移动内存。

    编辑:

    取一些cmets的注释,每一代保持复制语义的代码(包括用mov-instructions或其他代码替换memcpy)都是允许的。

    对于复制算法(包括newlib 正在使用的算法),您可以查看this article。引用这篇文章:

    特殊情况如果您对要复制的数据了如指掌 以及 memcpy 运行的环境,您也许可以 创建一个运行速度非常快的专用版本

    【讨论】:

    • 您还应该提到,在许多情况下,对 memcpy 的调用将被忽略,特别是对于对齐大小较小的 memcpy,例如 4,8 等。然后编译器将只发出一两个mov 说明。
    • @Jabberwocky:我会怀疑这一点。但后来我在godbolt.org 上试了一下,确实有效,例如:godbolt.org/z/pwB3k5
    • @Codo 您显示的 Godbolt 上的链接是一个不好的例子,对 memcpy 的调用没有被忽略,但由于代码没有可观察到的效果,它已被完全优化。删除 -Os 编译器标志,然后您将看到对 memcpy 的调用的实际省略
    • 你的 memcpy 和 memmove 颠倒了。
    • 问题是,问题中提到的 Newlib 库不是为特定架构编写的 - 它被编写为可移植的 - 用 C.github.com/eblot/newlib/blob/master/newlib/libc/string/memcpy.c跨度>
    【解决方案4】:

    这里有几点,可能上面已经提到了:

    • 经过认证的库:通常它们未经认证可在安全受限的环境中运行。通常从不提供根据特定 ASPICE/CMM 级别开发的库,因此这些库不能在此类环境中使用。
    • 架构特定的实现:也许您自己的实现使用了一些非常针对特定目标的功能,而库无法提供这些功能,例如特定的加载/存储指令(SIMD,基于向量的指令),甚至是基于 DMA 的更大数据的实现,或者在具有不同内核架构的多处理器的情况下使用不同的实现(例如,具有 e200z4 和 e200z7 内核的 NXP S32,或 ARM M5 vs. A53),并且 lib 需要找出调用它的核心以获得最佳性能
    • 由于嵌入式开发是根据 C 标准“独立”而非“托管”的,因此该标准的很大一部分是“实现定义”甚至“未指定”,其中包括库。

    【讨论】:

      猜你喜欢
      • 2011-12-18
      • 2020-03-10
      • 1970-01-01
      • 2012-01-11
      • 2017-04-14
      • 1970-01-01
      • 2017-03-04
      • 1970-01-01
      • 2014-07-01
      相关资源
      最近更新 更多