搜索一个短排序的双打数组

时间:2013-11-25 11:38:17

标签: c gcc assembly compiler-optimization micro-optimization

我正在尝试通过非常短的排序双打数组来优化搜索,以找到给定value所属的存储桶。假设数组的大小是8倍,我提出了以下AVX内在函数序列:

_data = _mm256_load_pd(array);
temp  = _mm256_movemask_pd(_mm256_cmp_pd(_data, _value, _CMP_LT_OQ));
pos   = _mm_popcnt_u32(temp);
_data = _mm256_load_pd(array+4);
temp  = _mm256_movemask_pd(_mm256_cmp_pd(_data, _value, _CMP_LT_OQ));
pos  += _mm_popcnt_u32(temp);

令我惊讶的是(我脑子里没有指令延迟规范......),结果发现gcc为后面的C循环生成了更快的代码:

for(i=0; i<7; ++i) if(array[i+1]>=value) break;

这个循环编译成我发现的非常有效的代码:

lea ecx, [rax+1]
vmovsd  xmm1, QWORD PTR [rdx+rcx*8]
vucomisd    xmm1, xmm0
jae .L7

lea ecx, [rax+2]
vmovsd  xmm1, QWORD PTR [rdx+rcx*8]
vucomisd    xmm1, xmm0
jae .L8

[... repeat for all elements of array]

因此需要4条指令来检查1个广告位(leavmovsdvucomisdjae)。假设value均匀分布,平均每个value必须检查~3.5个桶。显然,这足以胜过前面列出的AVX代码。

现在,在一般情况下,阵列当然可以大于8个元素。如果我像这样编写一个C循环:

for(i=0; u<n-1; i++) if(array[i+1]>=value) break;

我得到循环体的以下指令序列:

.L76:
mov eax, edx
.L67:
cmp eax, esi
jae .L77
lea edx, [rax+1]
mov ecx, edx
vmovsd  xmm1, QWORD PTR [rdi+rcx*8]
vucomisd    xmm1, xmm0
jb  .L76

我可以告诉gcc展开循环,但关键是每个元素的指令数大于具有常量边界的循环的情况,并且代码更慢。另外,我不明白在rcx中使用额外的vmovsd寄存器进行寻址的原因。

我可以手动修改循环的程序集,使其看起来与第一个示例类似,并且它的工作速度更快:

.L76:
cmp edx, esi            # eax -> edx
jae .L77
lea edx, [rdx+1]        # rax -> rdx
vmovsd  xmm1, QWORD PTR [rdi+rdx*8]
vucomisd    xmm1, xmm0
jb  .L76

但我似乎无法让gcc做到这一点。我知道它可以 - 第一个例子中生成的asm就可以了。

除了使用内联asm之外,您有什么想法吗?甚至更好 - 你能建议更快地实施搜索吗?

2 个答案:

答案 0 :(得分:1)

不是真的答案,但评论中没有空间。

我针对简单的C实现测试了AVX功能,并得到了完全不同的结果。 我测试的是Windows 7 x64而不是Linux,但生成的代码非常相似。 测试如何进行: 1)我禁用了CPU的SpeedStep。 2)在main()中,我将进程优先级和线程优先级提高到最大值(实时)。 3)我对测试功能进行了10M调用以加热CPU - 激活turbo。 4)我调用Sleep(0)以避免上下文切换 5)我调用了__rdtscp来开始测量 6)在一个循环中我调用了AVX查找索引函数或简单的C版本 - 就像你做的那样。其他实现已被注释掉而未被使用。循环大小为10M呼叫。 7)我再次调用__rdtscp来完成基准测试。 8)我打印了刻度/迭代。获得呼叫的平均滴答计数

注意:我将'find index'函数声明为inline,我在反汇编中确认它们是内联的。 您描述的AVX函数和C函数不相同,C函数返回基于零的索引,AVX函数返回基于1的索引。 在我的系统中,AVX函数每次迭代需要1.1个周期,C函数每次迭代需要4.4个周期。

我无法强制MSVC编译器使用多个ymm寄存器:(

使用的数组:

double A[8] = {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8 };

结果(平均滴答声/ iter): value = 0.3(index = 2):AVX:1.1 | C:4.4 value = 0.5(index = 3):AVX:1.1 | C:11.1 value = 0.9(index = 7):AVX:1.1 | C:18.1

如果AVX功能被修正为返回pos-1,那么它将慢50%。 您可以看到AVX函数在恒定时间内工作,而微不足道的C循环函数性能取决于您正在寻找的索引。

使用clock()和运行100M的时序产生类似的结果,AVX在第一次测试时几乎快了x4。 另请注意,运行更长时间的测试会显示不同的结果,但每次AVX都具有类似的优势。

答案 1 :(得分:0)

您可以尝试整数比较。双重比较等同于相同位的int64_t比较,NaN的例外。它可以变得更快。 CPU具有更多整数执行单元,然后是SIMD。只需发送double *并在函数参数中接收int64_t *。