【问题标题】:Implementing memcmp实现 memcmp
【发布时间】:2011-06-28 09:47:49
【问题描述】:

以下是memcmp的微软CRT实现:

int memcmp(const void* buf1,
           const void* buf2,
           size_t count)
{
    if(!count)
        return(0);

    while(--count && *(char*)buf1 == *(char*)buf2 ) {
        buf1 = (char*)buf1 + 1;
        buf2 = (char*)buf2 + 1;
    }

    return(*((unsigned char*)buf1) - *((unsigned char*)buf2));
}

它基本上是逐字节进行比较。

我的问题分为两部分:

  1. 是否有任何理由在 count < sizeof(int) 之前不将其更改为 int by int 比较,然后对剩余的内容进行逐字节比较?
  2. 如果我要执行 1,是否存在任何潜在/明显的问题?

注意:我根本没有使用 CRT,所以无论如何我都必须实现这个功能。我只是在寻找有关如何正确实施它的建议。

【问题讨论】:

  • 这在大多数情况下并不完全正确。假设您使用优化进行编译,它将变成编译器内在函数,而不是调用 CRT 的实现。
  • 添加在 C 标记中,因为这确实是一个 C 问题
  • 优化时,需要考虑的一个问题是需要多大的数据量才能看到任何显着的改进? 有时执行函数的开销会占用更多时间比实际比较。
  • 这真的是实现吗?我原以为他们使用的是 SIMD 指令 (SSE)。

标签: c++ c memcmp


【解决方案1】:

您找到的代码只是memcmp 的调试实现,它针对简单性和可读性进行了优化,而不是针对性能。

内在编译器实现是特定于平台的,并且足够智能,可以生成处理器指令,尽可能一次比较双字或双字(取决于目标架构)。 此外,如果两个缓冲区具有相同的地址(buf1 == buf2),则内部实现可能会立即返回。调试实现中也缺少此检查。

最后,即使您确切知道要在哪个平台上运行,完美的实现仍然不是通用的,因为它取决于一系列特定于程序其余部分的不同因素:

  • 保证缓冲区对齐的最小值是多少?
  • 能否在不触发访问冲突的情况下读取缓冲区末尾之后的任何填充字节?
  • 缓冲区参数可以相同吗?
  • 缓冲区大小可以为 0 吗?
  • 您是否只需要比较缓冲区内容是否相等?或者你还需要知道哪个更大(返回值 0)?
  • ...

如果性能是一个问题,我建议在汇编中编写比较例程。大多数编译器都为您提供了查看它们为源代码生成的程序集列表的选项。您可以使用该代码并根据您的需要对其进行调整。

【讨论】:

    【解决方案2】:

    如果您愿意,您可以将其作为 int-by-int 比较或更广泛的数据类型进行。

    您必须注意的两件事(至少)是开始和结束处的悬垂,以及这两个区域之间的对齐方式是否不同。

    如果您在不遵循对齐规则的情况下访问值,某些处理器运行速度会变慢(如果您尝试这样做,有些甚至会崩溃)。

    因此,您的代码可能会进行 char 比较,直到 int 对齐区域,然后是 int 比较,然后再次进行 char 比较,但同样,两个区域的对齐可能很重要。

    额外的代码复杂性是否值得您节省下来,这取决于您无法控制的许多因素。一种可能的方法是检测两个区域对齐相同的理想情况并快速进行,否则只需逐个字符地进行。

    【讨论】:

    • 这种优化不是也要求两个指针在开始时悬垂相同的量。
    • 这种优化是可能的,但我怀疑所有涉及的转换(从 void 到 uintptr_t 用于对齐检查,到 int* 用于比较和 char*...)它只需下拉组装并将其组合在一起会更快......
    【解决方案3】:

    不要忘记,当您在较大的块中发现不匹配时,您必须识别该块中的第一个不同 char,以便计算正确的返回值 (@987654322 @ 返回第一个不同字节的差值,视为unsigned char 值)。

    【讨论】:

      【解决方案4】:

      另一个想法是优化处理器缓存和获取。处理器喜欢随机获取大块而不是单个字节。尽管内部工作可能已经解释了这一点,但无论如何这将是一个很好的练习。始终分析以确定最有效的解决方案。

      伪代码:

      while bytes remaining > (cache size) / 2 do // Half the cache for source, other for dest.
        fetch source bytes
        fetch destination bytes
        perform comparison using fetched bytes
      end-while
      perform byte by byte comparison for remainder.
      

      有关更多信息,请在网上搜索“数据驱动设计”和“面向数据的编程”。

      某些处理器(例如 ARM 系列)允许有条件地执行指令(在 32 位、非拇指模式下)。处理器获取指令,但只有在满足条件时才会执行它们。在这种情况下,请尝试根据布尔赋值重新表述比较。这也可以减少所采用的分支数量,从而提高性能。

      另请参阅循环展开
      另请参阅汇编语言

      通过针对特定处理器定制算法可以获得很多性能,但在可移植性方面比较松散。

      【讨论】:

        【解决方案5】:

        许多处理器将其作为一条指令来实现。如果你能保证你运行的处理器可以用一行内联汇编器来实现。

        【讨论】:

        • 仅仅因为它是一个单一的组装操作并不意味着它更快! x86 字符串操作是 slow,因为它们从慢速指令解码 ROM 扩展到大量微操作
        【解决方案6】:

        这真的是他们的实现吗?除了不这样做之外,我还有其他问题:

        • 抛弃 constness。
        • return 语句有效吗?无符号字符 - 无符号字符 = 有符号整数?

        int 一次仅在指针对齐时有效,或者如果您可以从每个字节的前面读取几个字节并且它们仍然对齐,因此如果在对齐边界之前两者都是 1,您可以读取一个字符然后每个都去 int-at-a-time,但是如果它们的对齐方式不同,例如一个对齐一个不对齐,就没有办法做到这一点。

        memcmp 在实际比较(它必须走到最后)并且数据很长时,效率最低(即它花费的时间最长)。

        我不会自己写,但如果你要比较大部分数据,你可以做一些事情,比如确保对齐,甚至填充末端,然后如果你愿意,可以一次一个单词。

        【讨论】:

        • “抛弃常量”只是为了允许指针增加char 大小的块。这也是 return 语句中发生的事情——char 版本没有被取消引用和比较,它们只是在进行指针减法。还要记住 w.r.t. const 是这个 memcmp 可能是在 const 添加到语言之前编写的,并且有人刚刚将 const 添加到方法签名中以针对 C++ 更新它。编辑:另外,你真的没有回答 OP 的问题。
        • @Billy 没有必要抛弃常量,您只需在本地创建 const char* 指针,然后在整个过程中使用它们,并且也可以增加它们。是的,我确实回答了 OP 的问题,说明如果您比较他提出的一个单词,那么对齐可能存在的问题。
        • 如果代码是在const 存在之前编写的,那么当然有理由抛弃const。我看到你现在确实回答了这个问题,但它隐藏在你对一个特定实现的抨击中,以至于我没有看到它。
        • unsigned char - unsigned char = int 正是需要的,是的,它确实有效。比int 更窄的算术参数隐式提升到int
        【解决方案7】:

        如果您比较为 int,则需要检查对齐情况并检查 count 是否可被 sizeof(int) 整除(将最后一个字节比较为 char)。

        【讨论】:

          【解决方案8】:

          您提出的优化很常见。最大的担忧是,如果您尝试在不允许对单个字节以外的任何内容进行非对齐访问的处理器上运行它,或者在该模式下运行速度较慢; x86 系列没有这个问题。

          它也更复杂,因此更有可能包含错误。

          【讨论】:

          • both 指针的对齐访问是性能所必需的(尤其是在使用 128 位 sse 时)。请参阅Importance of alignment even on x86 machines(诚然,从 2004 年开始,但应该让您对潜在的恐怖有所了解)。
          • @bobbogo:与 2004 年相比,现代英特尔处理器上未对齐的 SSE 访问速度快得多;在可能的情况下仍应首选对齐访问,但我看到许多情况下,未对齐访问比对齐访问 + 软件修复更快。
          • @Stephen Canon:是的,YMMV 和往常一样。大概对于小区域来说,你的效率有多低并不重要。对齐访问 + 软件修复比没有最新 CPU 上的修复更快的内存块大小是否有限制?
          • @bobbogo:不是,不是。在某些高度特定的场景中它会更快,但总的来说,未对齐的 SSE 访问(即使它跨越缓存线或页面边界)在 i5/i7 内核上相当快。 (请注意,对齐访问仍然更快,因此您应该始终确保至少有一个访问是对齐的;只是当您从多个数组中读取时,您通常应该对齐那些您可以对齐的并在其他人现在而不是尝试做一些更聪明的事情)。
          • 只是提供一些具体数字,来自英特尔优化手册,在 i7 上用于 L1 缓存中的数据:不跨越缓存线边界的未对齐负载与对齐负载(单周期吞吐量),并且跨缓存线边界的未对齐负载大约需要 4 个周期(与早期微架构上的约 20 个周期相比)。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-10-09
          • 2012-01-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-10-02
          相关资源
          最近更新 更多