解决方案
提出的解决方案始终将所有唯一元素放置在输出的下部,并按首次出现的顺序排序。上部分被清零。可以通过修改LUT来轻松更改放置策略:将元素放置在上部,或者颠倒它们的顺序。
static __m128i *const lookup_hash = (__m128i*) &lookup_hash_chars[0][0];
static inline __m128i deduplicate4_ssse3(__m128i abcd) {
__m128i bcda = _mm_shuffle_epi32(abcd, _MM_SHUFFLE(0, 3, 2, 1));
__m128i cdab = _mm_shuffle_epi32(abcd, _MM_SHUFFLE(1, 0, 3, 2));
uint32_t mask1 = _mm_movemask_epi8(_mm_cmpeq_epi32(abcd, bcda));
uint32_t mask2 = _mm_movemask_epi8(_mm_cmpeq_epi32(abcd, cdab));
uint32_t maskFull = (mask2 << 16U) + mask1;
uint32_t lutIndex = (maskFull * 0X0044CCCEU) >> 26U;
__m128i shuf = lookup_hash[lutIndex];
return _mm_shuffle_epi8(abcd, shuf);
}
完整代码(带测试)可在此处获得。
我还实现了一个简单的标量解决方案,通过5个比较器的排序网络,然后对连续元素进行串行比较。我在两个处理器上使用的是MSVC2013:Core 2 E4700(Allendale,2.6 GHz)和Core i7-3770(Ivy Bridge,3.4 GHz)。以下是2^29次调用的时间(以秒为单位):
SSE: time = 3.340
Scalar: time = 17.218
SSE: time = 1.203
Scalar: time = 11.673
讨论
请注意,结果必须由两种类型的元素组成:
- 来自输入向量的元素,
- 零。
然而,必要的洗牌掩码是在运行时以非常复杂的方式确定的。所有SSE指令都只能处理立即(即编译时常数)洗牌掩码,除了一个。它是SSSE3中的_mm_shuffle_epi8
内部函数。为了快速获取洗牌掩码,所有掩码都存储在查找表中,由一些位掩码或哈希索引。
为了获得给定输入向量的洗牌掩码,有必要收集关于其中相等元素的足够信息。请注意,仅知道哪些元素对相等就足够确定如何去重。如果我们还想将它们排序,那么我们还需要知道不同的元素彼此比较的方式,这增加了信息量,并随后增加了查找表。这就是为什么我在这里展示不进行排序的去重。
因此,我们在XMM寄存器中有四个32位元素。它们总共组成了六对。由于我们一次只能比较四个元素,所以我们至少需要进行两次比较。实际上,很容易进行两个XMM比较,这样每对元素都会被比较至少一次。之后,我们可以使用
_mm_movemask_epi8
提取比较的16位掩码,并将它们连接成一个单独的32位整数。请注意,每个4位块肯定包含相同的位,并且最后两个4位块不是必需的(它们对应于过多的比较)。
理想情况下,我们需要从这个掩码中提取出位于编译时已知位置的6位。可以使用BMI2指令集中的
_pext_u32
内部函数轻松实现。结果,我们得到一个范围在
[0..63]之间的整数,其中每个位表示相应的元素对是否相等。然后,我们从预先计算的64项查找表中加载一个洗牌掩码,并使用
_mm_shuffle_epi8
重新排列我们的输入向量。
很不幸,BMI指令相当新(Haswell及更高版本),而我没有它们=)为了摆脱它,我们可以尝试为所有64个有效位掩码(记住位掩码是32位)创建一个非常简单快速的完美哈希函数。对于类f(x) = (a * x) >> (32-b)
中的哈希函数,通常可以构造一个相当小的完美哈希,具有2x或3x的内存开销。由于我们的情况比较特殊,因此可以构造一个最小完美哈希函数,以便查找表具有最小的64个条目(即大小=1 KB)。
对于8个元素(例如XMM寄存器中的16位整数),同样的算法不可行,因为有28对元素,这意味着查找表必须包含至少2^28个条目。
使用这种方法处理 YMM 寄存器中的 64 位元素也存在问题。因为
_mm256_shuffle_epi8
内置函数只是执行两个独立的 128 位 shuffle(从不跨越通道进行 shuffle)。
_mm256_permutevar8x32_epi32
内置函数可以任意地对 32 位块进行 shuffle,但它不能插入零。为了使用它,您还需要在 LUT 中存储唯一元素的数量。然后您需要手动将零放入寄存器的高位。
更新:哈希/BMI 已删除
我已经意识到使用 BMI2 进行位提取或完美哈希函数并不是必要的,我们可以简单地使用
_mm_movemask_ps
提取 32 位掩码。这种方法可能会受到轻微延迟问题的影响,因为我们混合了 INT 和 FP 计算,但实际上它的工作速度更快。
static __m128i *const lookup_direct_offset = lookup_direct - 0xC0U;
static inline __m128i deduplicate4_ssse3_direct(__m128i abcd) {
__m128i bcda = _mm_shuffle_epi32(abcd, _MM_SHUFFLE(0, 3, 2, 1));
__m128i cdcd = _mm_shuffle_epi32(abcd, _MM_SHUFFLE(3, 2, 3, 2));
uint32_t mask1 = _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpeq_epi32(abcd, bcda)));
uint32_t mask2 = _mm_movemask_ps(_mm_castsi128_ps(_mm_cmpeq_epi32(abcd, cdcd)));
uint32_t maskFull = 16U * mask2 + mask1;
uint32_t lutIndex = maskFull;
__m128i shuf = lookup_direct_offset[lutIndex];
return _mm_shuffle_epi8(abcd, shuf);
}
完整代码也已更新。这会带来轻微的性能提升:
new: Time = 1.038 (782827520)
old: Time = 1.169 (782827520)
pshufd
和pcmpeqd
指令。 - Gunther Piez