这是计算缓冲区中不同值数量的基本算法:
unsigned getCount(const uint8_t data[16])
{
uint8_t pop[256] = { 0 };
unsigned count = 0;
for (int i = 0; i < 16; ++i)
{
uint8_t b = data[i];
if (0 == pop[b])
count++;
pop[b]++;
}
return count;
}
这可以通过加载到q-reg并做一些魔术来有效地在霓虹灯中完成吗?或者,我是否可以有效地说data
所有元素都相同,或者只包含两个不同的值或多于两个?
例如,使用vminv_u8
和vmaxv_u8
我可以找到min和max元素,如果它们相等,我知道data
具有相同的元素。如果没有,那么我可以vceq_u8
使用最小值,vceq_u8
使用最大值,然后vorr_u8
这些结果,并比较我在结果中拥有所有1-s。基本上,在霓虹灯中,它可以通过这种方式完成。任何想法如何让它更好?
unsigned getCountNeon(const uint8_t data[16])
{
uint8x16_t s = vld1q_u8(data);
uint8x16_t smin = vdupq_n_u8(vminvq_u8(s));
uint8x16_t smax = vdupq_n_u8(vmaxvq_u8(s));
uint8x16_t res = vdupq_n_u8(1);
uint8x16_t one = vdupq_n_u8(1);
for (int i = 0; i < 14; ++i) // this obviously needs to be unrolled
{
s = vbslq_u8(vceqq_u8(s, smax), smin, s); // replace max with min
uint8x16_t smax1 = vdupq_n_u8(vmaxvq_u8(s));
res = vaddq_u8(res, vaddq_u8(vceqq_u8(smax1, smax), one));
smax = smax1;
}
res = vaddq_u8(res, vaddq_u8(vceqq_u8(smax, smin), one));
return vgetq_lane_u8(res, 0);
}
通过一些优化和改进,可以在32-48个氖指令中处理一个16字节的块。这可以做得更好吗?不大可能
我问这个问题的一些背景。当我正在研究算法时,我尝试了不同的处理数据的方法,而且我还不确定最终我会使用什么。可能有用的信息:
所以,我尝试了一些东西,在使用任何方法之前,我想看看是否可以很好地优化这种方法。例如,每个块的平均值基本上是arm64上的memcpy速度。
答案 0 :(得分:1)
如果您预计会有大量重复,并且可以 获得vminv_u8
的水平分钟,这可能比标量更好。或者不是,也许NEON-&gt;循环条件的ARM停顿会使其失效。 &GT;。&LT;但是应该可以通过展开来缓解这种情况(并在寄存器中保存一些信息以确定你超出的程度)。
// pseudo-code because I'm too lazy to look up ARM SIMD intrinsics, edit welcome
// But I *think* ARM can do these things efficiently,
// except perhaps the loop condition. High latency could be ok, but stalling isn't
int count_dups(uint8x16_t v)
{
int dups = (0xFF == vmax_u8(v)); // count=1 if any elements are 0xFF to start
auto hmin = vmin_u8(v);
while (hmin != 0xff) {
auto min_bcast = vdup(hmin); // broadcast the minimum
auto matches = cmpeq(v, min_bcast);
v |= matches; // min and its dups become 0xFF
hmin = vmin_u8(v);
dups++;
}
return dups;
}
这会将唯一值转换为0xFF,一次重复一组。
通过v / hmin的循环传输dep链停留在向量寄存器中;它只是需要NEON->整数的循环分支。
在hmin
上展开8而没有分支,结果为8个NEON寄存器。然后转移这8个值; back-to-back transfers of multiple NEON registers to ARM only incurs one total stall (无论Jake测试了什么,都是14个周期。)无序执行也可以隐藏这个失速的一些惩罚。然后使用完全展开的整数循环检查这8个整数寄存器。
将展开因子调整得足够大,以至于您通常不需要对大多数输入向量进行另一轮SIMD操作。如果几乎所有向量都有最多5个唯一值,则展开5而不是8。
不是将多个hmin
结果转换为整数,而是将它们计入NEON 。如果你可以使用ARM32 NEON部分寄存器技巧将多个hmin
值放在同一个向量中,那么将它们中的8个混合到一个向量中并进行比较是不相等的到0xFF
。然后水平添加比较结果以获得-count
。
或者,如果您在单个矢量的不同元素中具有来自不同输入矢量的值,则可以使用垂直操作一次添加多个输入矢量的结果,而无需水平操作。
几乎可以肯定有这个优化空间,但我不太了解ARM,或者ARM性能细节。由于NEON->整数的性能损失很大,NEON很难用于任何条件,完全不同于x86。循环中带有NEON->整数的Glibc has a NEON memchr
,但我不知道它是否使用它,或者它是否比标量更快。
每次将256字节缓冲区归零会很昂贵,但我们并不需要这样做。 使用序列号以避免需要重置:
++seq
; 对于集合中的每个元素:
sum += (histogram[i] == seq);
histogram[i] = seq; // no data dependency on the load result, unlike ++
您可以将直方图设为uint16_t
或uint32_t
的数组,以避免在uint8_t seq
换行时需要重新归零。但是它需要更多的缓存占用空间,因此可能只需重新调零每254个序列号最有意义。