【问题标题】:Fastest way to see how many bytes are equal between fixed length arrays查看固定长度数组之间有多少字节相等的最快方法
【发布时间】:2010-09-12 02:28:01
【问题描述】:

我有 2 个包含 16 个元素(字符)的数组,我需要“比较”并查看两者之间有多少元素相等。

这个例程将被使用数百万次(通常运行大约 60 或 7000 万次),所以我需要它尽可能快。我正在研究 C++(C++Builder 2007,记录在案)

现在,我有一个简单的:

matches += array1[0] == array2[0];

重复 16 次(分析它似乎比使用 for 循环快 30%)

有没有其他方法可以更快地工作?

关于环境和数据本身的一些数据:

  • 我正在使用 C++Builder,它没有考虑任何速度优化。我最终会尝试使用另一个编译器,但现在我被这个编译器卡住了。
  • 大多数时候数据会有所不同。 100% 相同的数据通常非常罕见(可能少于 1%)

【问题讨论】:

  • 抱歉,这是一个有 16 个元素的数组吗?还是一个总共 16 字节长的数组?
  • 是的,我要澄清一下。它是一个由 16 个元素组成的数组,每个元素一个字节长。基本上是 char[16]。
  • 查看接受的答案似乎表明值仅为 0 和 1。对吗?

标签: c++ arrays optimization


【解决方案1】:

更新:已修改此答案以使我的 cmets 与下面提供的源代码匹配。

如果您能够使用 SSE2 和 popcnt 指令,则可以进行优化。

16 字节恰好适合 SSE 寄存器。使用 c++ 和汇编/内在函数,将两个 16 字节数组加载到 xmm 寄存器中,然后对它们进行 cmp。这会生成一个位掩码,表示比较的真/假条件。然后,您使用 movmsk 指令将位掩码的位表示加载到 x86 寄存器中;然后这成为一个位字段,您可以在其中计算所有 1 以确定您有多少真实值。硬件 popcnt 指令可以是一种快速计算寄存器中所有 1 的方法。

这需要了解汇编/内在知识,尤其是 SSE。您应该能够找到两者的网络资源。

如果您在不支持 SSE2 或 popcnt 的机器上运行此代码,则必须遍历数组并使用展开循环方法计算差异。

祝你好运

编辑: 既然你说你不知道汇编,这里有一些示例代码来说明我的答案:

#include "stdafx.h"
#include <iostream>
#include "intrin.h"

inline unsigned cmpArray16( char (&arr1)[16], char (&arr2)[16] )
{
    __m128i first = _mm_loadu_si128( reinterpret_cast<__m128i*>( &arr1 ) );
    __m128i second = _mm_loadu_si128( reinterpret_cast<__m128i*>( &arr2 ) );

    return _mm_movemask_epi8( _mm_cmpeq_epi8( first, second ) );
}

int _tmain( int argc, _TCHAR* argv[] )
{
    unsigned count = 0;
    char    arr1[16] = { 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0 };
    char    arr2[16] = { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0 };

    count = __popcnt( cmpArray16( arr1, arr2 ) );

    std::cout << "The number of equivalent bytes = " << count << std::endl;

    return 0;
}

一些注意事项:这个函数使用 SSE2 指令和在 Phenom 处理器(我使用的机器)中引入的 popcnt 指令。我相信最新的带有 SSE4 的英特尔处理器也有 popcnt。此函数不检查 CPUID 的指令支持;如果在没有 SSE2 或 popcnt 的处理器上使用该函数是未定义的(您可能会得到一个无效的操作码指令)。该检测代码是一个单独的线程。

我没有对这段代码计时;我认为它更快的原因是因为它一次比较 16 个字节,无分支。你应该修改它以适应你的环境,并自己计时,看看它是否适合你。我在 VS2008 SP1 上编写并测试了这个。

SSE 更喜欢在自然 16 字节边界上对齐的数据;如果你能保证,那么你应该得到额外的速度提升,你可以将 _mm_loadu_si128 指令更改为 _mm_load_si128,这需要对齐。

【讨论】:

  • 我实际上需要知道两个数组之间有多少元素相等。无论如何,我都会研究这个想法,并为此搜索 ASM 代码。我对 ASM 一无所知。谢谢:-)
  • 感谢您的代码。我认为我无法让它在 BCB 上运行,但我会尽快尝试使用 VS2008 DLL。我实际上相信即使是我的实际代码在使用 VS2008 编译时也会运行得更快,但我会分析这两个版本。
  • Kent:我实现了你的解决方案,除了 __popcnt 使用(我将它替换为 std::bitset),现在它需要一半的时间!我期望加速,但没有那么多!我会做 CPUID 并在支持它的机器上尝试(我的第一个 MacPro 似乎不支持它)。非常感谢!
  • 您使用 std::bitset 替换硬件 popcnt 指令很聪明。您可以想象 bitset::count 函数被合理优化,并涵盖了所有不提供硬件功能的处理器。如果合适的话,硬件popcount 可以提供额外的好处。
  • 是的,我实际上必须创建未优化的版本,按照我以前的方式做事,以防它必须在非 sse2 cpus 上运行(我真的希望它不会,但你永远不知道),所以我将创建 3 个版本,未优化的、sse2 和 sse2+popcnt。再次感谢!
【解决方案2】:

关键是使用您的 CPU 支持的最大寄存器进行比较,然后在必要时回退到字节。

下面的代码演示了使用 4 字节整数,但如果您在 SIMD 架构(任何现代 Intel 或 AMD 芯片)上运行,您可以在回退到基于整数的循环之前在一条指令中比较两个数组。如今,大多数编译器都对 128 位类型具有内在支持,因此不需要 ASM。

(请注意,对于 SIMD 比较,您的数组必须是 16 字节对齐的,并且某些处理器(例如 MIPS)会要求数组是 4 字节对齐以进行基于 int 的比较。

例如

int* array1 = (int*)byteArray[0];
int* array2 = (int*)byteArray[1];

int same = 0;

for (int i = 0; i < 4; i++)
{
  // test as an int
  if (array1[i] == array2[i])
  {
    same += 4;
  }
  else
  {
    // test individual bytes
    char* bytes1 = (char*)(array1+i);
    char* bytes2 = (char*)(array2+i);

    for (int j = 0; j < 4; j++)
    {
      same += (bytes1[j] == bytes2[j];
    }
  }
}

我不记得 MSVC 编译器到底支持什么 SIMD,但你可以做类似的事情;

// depending on compiler you may have to insert the words via an intrinsic
__m128 qw1 = *(__m128*)byteArray[0];
__m128 qw2 = *(__m128*)byteArray[1];

// again, depending on the compiler the comparision may have to be done via an intrinsic
if (qw1 == qw2)
{
    same = 16;
}
else
{
    // do int/byte testing
}

【讨论】:

  • 我刚试过这个,它并没有让事情变得更快。带有 BCB 的 for 循环真的很糟糕,另一方面,大多数 int comps 都是错误的,所以仍然需要逐字节检查。不过谢谢你的想法。将此移动到 MSVC dll 时,我会再试一次。
  • Rodgrigo,你显然可以展开 for 循环。
【解决方案3】:

如果您有能力控制数组的位置,例如在内存中一个接一个地放置,可能会导致它们在第一次访问时被加载到 CPU 的缓存中。

这取决于 CPU 及其缓存结构,并且会因一台机器而异。

您可以在Henessy & Patterson's Computer Architecture: A Quantitative Approach阅读有关内存层次结构和缓存的信息

【讨论】:

    【解决方案4】:

    如果您需要绝对最小的占用空间,我会使用汇编代码。我已经有一段时间没有这样做了,但我敢打赌 MMX(或者更可能是 SSE2/3)的说明可以让您在极少的说明中做到这一点。

    【讨论】:

      【解决方案5】:

      如果匹配是常见情况,则尝试将值加载为 32 位整数而不是 16,这样您就可以一次比较 2(并将其计为 2 个匹配)。

      如果这两个 32 位值相同,那么您将必须分别测试它们(并取出顶部和底部 16 位值)。

      代码会更复杂,但应该更快。

      如果您的目标是 64 位系统,您可以使用 64 位整数执行相同的技巧,如果您真的想突破限制,那么请考虑进入汇编程序并使用各种基于向量的指令,这些指令可以让您工作一次 128 位。

      【讨论】:

      • 谢谢罗伯。我刚刚尝试了类似的东西,安德鲁发布的代码,它并没有加快速度。通常匹配不会很常见。
      【解决方案6】:

      神奇的编译器选项会在时间上有很大的不同。特别是让它生成 SSE 矢量化可能会给你带来巨大的加速。

      【讨论】:

        【解决方案7】:

        这是否必须独立于平台,或者这段代码是否总是在相同类型的 CPU 上运行?如果您将自己限制在现代 x86 CPU 上,您也许可以使用 MMX 指令,这应该允许您在一个时钟滴答中对一组 8 字节的数组进行操作。 AFAIK,gcc 允许您在 C 代码中嵌入汇编,并且英特尔的编译器 (icc) 支持内部函数,这些内部函数是允许您直接调用特定汇编指令的包装器。其他 SIMD 指令集,例如 SSE,也可能对此有用。

        【讨论】:

        • 它不必独立于平台,至少现在不是。我知道我使用的 C++Builder 编译器允许嵌入 asm 指令。问题是我不了解 ASM :-) 我将不得不开始研究它。
        【解决方案8】:

        数组中的值之间是否有任何联系?某些字节是否比其他字节更可能相同?价值观中可能存在某种内在顺序吗?然后您可以针对最可能的情况进行优化。

        【讨论】:

        • 谢谢马库斯。不幸的是,没有可能的价值观/立场,或者最后,没有可能的情况。唯一的一个是数组的固定长度,16,这是 95% 或更多的情况。对于大小不是 16 的其他情况,我仍然有一个 for 循环。
        【解决方案9】:

        如果您解释数据实际代表什么,那么可能有一种完全不同的方式来表示内存中的数据,这将使这种类型的蛮力比较变得不必要。想详细说明数据实际代表什么?

        【讨论】:

          【解决方案10】:

          作为一个语句是不是更快?

          matches += (array1[0] == array2[0]) + (array1[1] == array2[1]) + ...;
          

          【讨论】:

            【解决方案11】:

            如果写这 16 倍比一个简单的循环快,那么你的编译器要么很糟糕,要么你没有打开优化。

            简短回答:没有更快的方法,除非您在并行硬件上进行向量运算。

            【讨论】:

            • 是的,Borland C++ 编译器在优化方面很糟糕。我可能会将这段代码移到 MSVC 并在那里尝试,我有一些经验,在 MS 编译器中相同的代码比 Borland/CodeGear 的编译器快得多。
            【解决方案12】:

            尝试使用指针而不是数组:

            p1 = &array1[0];
            p2 = &array2[0];
            match += (*p1++ == *p2++);
            // copy 15 times.
            

            当然,您必须对照其他方法来衡量这一点,看看哪种方法最快。

            您确定此例程是您处理过程中的瓶颈吗?您是否真的通过优化来提高整个应用程序的性能?同样,只有测量才能说明问题。

            【讨论】:

            • 我确定这是瓶颈。我一直在使用 AQTime 对此进行分析,这个函数代表了进程总运行时间的大约 65%。另外 25% 是调用它的函数,它是将大数组“拆分”为 16 个元素的数组。
            • 注意:“指针而不是数组”并不总是一个好主意。一个好的优化编译器在数组+索引上比在指针访问上工作得更好。我建议两者都编码,测量两者并保留最简单的一个(恕我直言,数组)。当然是 YMMV。
            【解决方案13】:

            有什么方法可以修改数组的存储方式吗?考虑到您可能使用的是 32 位编译器,一次比较 1 个字节非常慢。相反,如果您将 16 个字节存储在 4 个整数(32 位)或 2 个长整数(64 位)中,则只需分别执行 4 或 2 次比较。

            要问自己的问题是将数据存储为 4 整数或 2 长数组的成本是多少。您需要多久访问一次数据等。

            【讨论】:

            • 这里的问题是我不需要只看 16 字节是否相等,而是它们有多相似。通常它们不会 100% 相等,因此将它们作为整数或长整数进行比较通常不会有太大帮助(我只是尝试了类似的方法,但没有帮助)无论如何谢谢。
            【解决方案14】:

            总是有很好的旧 x86 REPNE CMPS 指令。

            【讨论】:

              【解决方案15】:

              一个额外的可能优化:如果您期望大多数时候数组是相同的,那么在第一步执行 memcmp() 可能会稍微快一些,如果测试返回 true,则设置 '16' 作为答案.当然,如果您不希望数组经常相同,那只会减慢速度。

              【讨论】:

              • 谢谢。不幸的是,大多数时候数组会有所不同。
              猜你喜欢
              • 2011-02-16
              • 1970-01-01
              • 2013-02-25
              • 1970-01-01
              • 2012-12-16
              • 2023-03-24
              • 1970-01-01
              相关资源
              最近更新 更多