查看固定长度数组之间有多少字节相等的最快方法
Posted
技术标签:
【中文标题】查看固定长度数组之间有多少字节相等的最快方法【英文标题】: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。对吗? 【参考方案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' 作为答案.当然,如果您不希望数组经常相同,那只会减慢速度。
【讨论】:
谢谢。不幸的是,大多数时候数组会有所不同。以上是关于查看固定长度数组之间有多少字节相等的最快方法的主要内容,如果未能解决你的问题,请参考以下文章
数据库中的一个字段的数据大小不定如何设置字段的长度查询最快又节省空间?
solidity二固定长度数组动态长度数组字符串之间的转换solidity字典hash表的使用solidity——memorystorage使用注意项solidity状态变量局部变量