查看固定长度数组之间有多少字节相等的最快方法

时间:2008-09-22 18:09:23

标签: c++ arrays optimization

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

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

现在,我有一个简单的说法:

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

重复16次(因为分析它看起来比使用for循环快30%)

还有其他方法可以更快地运作吗?

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

  • 我正在使用C ++ Builder,它没有任何速度优化需要考虑。我将最终尝试使用另一个编译器,但是现在我已经坚持使用这个了。
  • 大部分时间数据都不同。 100%相等的数据通常非常罕见(可能低于1%)

15 个答案:

答案 0 :(得分:16)

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

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

16个字节恰好适合SSE寄存器。使用c ++和assembly / intrinsics,将两个16字节数组加载到xmm寄存器中,然后cmp它们。这会生成一个位掩码,表示比较的真/假条件。然后使用movmsk指令将位掩码的位表示加载到x86寄存器中;这就变成了一个小字段,你可以计算所有的1来确定你有多少真值。硬件popcnt指令可以快速计算寄存器中的所有1。

这需要了解装配/内在和SSE的知识。您应该能够找到两者的Web资源。

如果在不支持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的英特尔处理器也有其优势。该函数不检查CPUID的指令支持;如果在没有SSE2或popcnt的处理器上使用该函数,则该函数是未定义的(您可能会得到无效的操作码指令)。该检测代码是一个单独的线程。

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

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

答案 1 :(得分:7)

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

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

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

E.g。

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
}

答案 2 :(得分:2)

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

这取决于CPU及其缓存结构,因机器而异。

您可以在Henessy & Patterson's Computer Architecture: A Quantitative Approach

中了解内存层次结构和缓存

答案 3 :(得分:2)

如果你需要绝对最低的占地面积,我会选择汇编代码。我有一段时间没有这样做,但我敢打赌MMX(或更可能是SSE2 / 3)的指令可以让你在很少的指令中做到这一点。

答案 4 :(得分:2)

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

如果两个32位值相同,则必须单独测试它们(并输出顶部和底部16位值)。

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

如果您的目标是64位系统,您可以使用64位整数执行相同的技巧,如果您真的想要超出限制,那么请查看删除汇编程序并使用各种基于向量的指令,这样可以让您工作一次128位。

答案 5 :(得分:1)

神奇的编译器选项会大大改变时间。特别是让它产生SSE矢量化可能会让你获得巨大的加速。

答案 6 :(得分:1)

这是否必须与平台无关,或者此代码是否始终在相同类型的CPU上运行?如果您将自己局限于现代x86 CPU,则可以使用MMX指令,这样可以允许您在一个时钟周期内操作8个字节的数组。 AFAIK,gcc允许您在C代码中嵌入汇编,而英特尔的编译器(icc)支持内部函数,这些封装允许您直接调用特定的汇编指令。其他SIMD指令集(例如SSE)也可能对此有用。

答案 7 :(得分:1)

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

答案 8 :(得分:1)

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

答案 9 :(得分:0)

一个声明是否更快?

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

答案 10 :(得分:0)

如果写16次比简单循环快,那么你的编译器很糟糕,或者你没有打开优化。

简短回答:没有更快的方法,除非你在并行硬件上进行矢量操作。

答案 11 :(得分:0)

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

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

当然,您必须根据其他方法来衡量这一点,以确定哪种方法最快。

您确定此例程是您处理的瓶颈吗?您是否真的通过优化它来加快整个应用程序的性能?同样,只有测量才能证明。

答案 12 :(得分:0)

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

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

答案 13 :(得分:0)

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

答案 14 :(得分:0)

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