【问题标题】:How to quickly replicate a 6-byte unsigned integer into a memory region?如何快速将 6 字节无符号整数复制到内存区域?
【发布时间】:2015-02-16 22:03:52
【问题描述】:

我需要将一个 6 字节的整数值复制到一个内存区域中,从它的开头开始并尽快。如果硬件支持这样的操作,我想使用它(我现在在 x64 处理器上,编译器是 GCC 4.6.3)。

memset 不适合这项工作,因为它只能复制字节。 std::fill 也不好,因为我什至无法定义迭代器,在内存区域的 6 个字节宽度位置之间跳转。

所以,我想要一个函数:

void myMemset(void* ptr, uint64_t value, uint8_t width, size_t num)

这看起来像memset,但还有一个额外的参数width 来定义要从value 复制多少字节。如果这样的东西可以用 C++ 来表达,那就更好了。

我已经知道明显的myMemset 实现,它将调用memcpy in 循环,最后一个参数(要复制的字节)等于width。我也知道,我可以定义一个大小为 6 * 8 = 48 字节的临时内存区域,用 6 字节整数填充它,然后将 memcpy 填充到目标区域。

我们可以做得更好吗?

【问题讨论】:

  • 我认为你的最后一种方法是我们能得到的最好的方法。手动填充一个 n 字节的小区域,然后 memcpy 它一次加倍,然后memcpy 2 n 字节的加倍区域得到 4 n 个字节等等。最后,memcpy 完成任务的最后一部分(通常不是 2 的幂)。你只会有一个对数的函数调用。
  • 我不明白memset()std:fill 能为您做什么,因为您说要复制一个数字,而不是清除它。 memcpy(&destination,&source,6)是最好的,你觉得有什么可以改进的?
  • 您可以扩展您的最后一个想法,甚至超过 48 个字节。就此而言,您可以使其递归;手动填充前 6 个字节,然后使用 memcpy 将其扩展为 12 个字节,扩展为 24 个字节、48 个字节等。
  • 我很好奇您为什么使用 6 字节整数。如果是为了节省空间,我建议你重新考虑。即使您不对数据使用 SSE 指令,使用自然对齐的 8 字节整数所带来的加速也可能超过节省的空间。但你还没有真正告诉我们你在做什么。
  • 最 C++ 的方式是迭代器和 std::copy: coliru.stacked-crooked.com/a/7a521244b39e3567 为什么需要更快的东西?

标签: c++ c memory


【解决方案1】:

@Mark Ransom 评论:

复制 6 个字节,然后复制 6、12、24、48、96 等。

void memcpy6(void *dest, const void *src, size_t n /* number of 6 byte blocks */) {
  if (n-- == 0) {
    return;
  }
  memcpy(dest, src, 6);
  size_t width = 1;
  while (n >= width) {
    memcpy(&((char *) dest)[width * 6], dest, width * 6);
    n -= width;
    width <<= 1; // double w
  }
  if (n > 0) {
    memcpy(&((char *) dest)[width * 6], dest, n * 6);
  }
}

优化:将 nwidth 缩放 6。

[编辑]
更正目的地@SchighSchagh
添加演员(char *)

【讨论】:

  • 这让我想到:将要填充的内存量除以 6 会给你一个指示“做什么”的位设置。然而,感冒严重破坏了我的数学技能,所以我无法推断出“什么”应该是什么。 (复制大小加倍?增加复制指针?两者都有?)
  • @Jongware 是的,每次迭代将副本的宽度加倍,将目标的初始部分作为源复制到目标中下一个不断增加的偏移量。
  • 在所谓的紧密内部循环中需要一些额外的处理,但另一方面,如果必须填充 127 个六元组,您当前的方式会在 64 处停止,并在之后恢复为“哑”复制那个。
  • @Jongware 对于 127 个块,首先是 1 个块的 memcpy(),然后是循环副本,1,2,4,8,16,32,64 个块。最后memcpy()复制了31个区块。
  • memcpy() 的真正函数调用实际上非常昂贵。 memcpy() 中的大部分代码都处理内存对齐问题。更不用说真正调用memcpy() 总是为了写而读;如果你可以避免读取而只写入寄存器的内容,那么你就有了明显的赢家。
【解决方案2】:

确定 CPU 支持的最有效的写入大小;然后找到可以被 6 和写入大小均分的最小数字,并将其称为“块大小”。

现在将内存区域分成该大小的块。每个块都是相同的,所有写入都将正确对齐(假设内存区域本身正确对齐)。

例如,如果 CPU 支持的最有效写入大小是 4 字节(例如古代 80486),那么“块大小”将为 12 字节。您将设置 3 个通用寄存器并每个块执行 3 次存储。

再举一个例子,如果 CPU 支持的最有效的写入大小是 16 字节(例如 SSE),那么“块大小”将是 48 字节。您将设置 3 个 SSE 寄存器并每个块执行 3 个存储。

另外,我建议将内存区域的大小向上取整,以确保它是块大小的倍数(带有一些“非严格必要”的填充)。一些不必要的写入比填充“部分块”的代码成本更低。

第二种最有效的方法可能是使用内存副本(但不是memcpy()memmove())。在这种情况下,您将写入最初的 6 个字节(或 12 个字节或 48 个字节或其他任何内容),然后从(例如)&amp;area[0] 复制到&amp;area[6](从最低到最高工作)直到到达末尾。对于这个memmove() 将不起作用,因为它会注意到该区域是重叠的,而是从最高到最低工作;而memcpy() 将不起作用,因为它假定源和目标不重叠;所以你必须创建自己的内存副本以适应。这样做的主要问题是内存访问次数增加了一倍——“读写”比“单独写”要慢。

【讨论】:

    【解决方案3】:

    如果您的 Num 足够大,您可以尝试使用 AVX 向量指令,该指令一次可处理 32 个字节 (_mm256_load_si256/_mm256_store_si256 或其未对齐的变体。

    由于 32 不是 6 的倍数,您必须首先使用短 memcpy 或 32/64 位移动复制 6 字节模式 16 次。

    ABCDEF
    ABCDEF|ABCDEF
    ABCD EFAB CDEF|ABCD EFAB CDEF
    ABCDEFAB CDEFABCD EFABCDEF|ABCDEFAB CDEFABCD EFABCDE
    ABCDEFABCDEFABCD EFABCDEFABCDEFAB CDEFABCDEFABCDEF|ABCDEFABCDEFABCD EFABCDEFABCDEFAB CDEFABCDEFABCDEF
    

    您还将完成一个简短的 memcpy。

    【讨论】:

      【解决方案4】:

      尝试使用 __movsq 内在函数(仅限 x64;在汇编中为 rep movsq),它将一次移动 8 个字节,具有适当的重复因子,并将目标地址设置在源之后 6 个字节。检查重叠地址是否得到巧妙处理。

      【讨论】:

        【解决方案5】:

        一次写入 8 个字节。

        在 64 位机器上,生成的代码当然可以在 8 字节写入时运行良好。在处理了一些设置问题之后,在一个紧密的循环中,每次写入大约 num 次写入 8 字节。假设适用 - 请参阅代码。

        // assume little endian
        void myMemset(void* ptr, uint64_t value, uint8_t width, size_t num) {
          assert(width > 0 && width <= 8);
        
          uint64_t *ptr64 = (uint64_t *) ptr;
          // # to stop early to prevent writing past array end
          static const unsigned stop_early[8 + 1] = { 0, 8, 3, 2, 1, 1, 1, 1, 0 };
          size_t se = stop_early[width];
          if (num > se) {
            num -= se;
        
            // assume no bus-fault with 64-bit write @ `ptr64, ptr64+1, ... ptr64+7`
            while (num > 0) { // tight loop
              num--;
              *ptr64 = value;
              ptr64 = (uint64_t *) ((char *) ptr64 + width);
            }
        
            ptr = ptr64;
            num = se;
          }
          // Cope with last few writes
          while (num-- > 0) {
            memcpy(ptr, &value, width);
            ptr = (char *) ptr + width;
          }
        }
        

        进一步优化包括一次写入2个块width == 3 or 4,一次写入4个块width == 2,一次写入8个块width == 1

        【讨论】:

        • 你是说这段代码比你之前回答中的代码更快(多字节复制)?
        • 另外我担心它会覆盖以前写入的字节
        • @HEKTO 更快取决于编译器、允许的编译器设置、平台和数据集。 (您提供了这 4 个中的 2 个)这只是赛马的另一个候选人。是的会覆盖以前写入的字节,这不是问题,因为覆盖的字节需要被覆盖。问题是它是否能最快地完成正确的工作?为什么要关心 ti 是否覆盖了某些字节?这里的好处是紧密循环不需要重复读取源数据。一旦源代码在value 中,可能会保存在寄存器中以优化代码。
        • 可能你的想法(对于width = 6)是将value 的两个最低字节复制到它的最高字节 - 然后覆盖不会具有破坏性,但我没有看到你的代码
        • 知道了 - 它会覆盖未使用的最高字节
        猜你喜欢
        • 2023-03-05
        • 1970-01-01
        • 1970-01-01
        • 2014-03-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-06
        • 2015-04-23
        相关资源
        最近更新 更多