【发布时间】:2013-11-25 11:38:16
【问题描述】:
我正在尝试通过一个非常短的双精度排序数组来优化搜索,以定位给定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]
因此检查 1 个存储桶需要 4 条指令(lea、vmovsd、vucomisd、jae)。假设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 展开循环,但关键是每个元素的指令数比具有恒定边界的循环的情况下要多,并且代码更慢。另外,我不明白在vmovsd 中使用额外的rcx 寄存器进行寻址的原因。
我可以手动修改循环的程序集,使其看起来像第一个示例中的那样,并且它确实工作得更快:
.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 之外,您有什么想法吗?甚至更好 - 您能建议更快地实现搜索吗?
【问题讨论】:
-
+1 但“我可以告诉 gcc 展开循环......在这种情况下,代码更慢”,对于 您的特定处理器乙>。我认为您的优化有点过于先进,风险是仅测试机器的最佳匹配架构(可能在其他方面表现不佳) .当然,如果您没有针对 CPU 固定且不会随时间变化的嵌入式系统进行优化。
-
@Adriano 我不准确 - 具有一般边界的循环的展开代码比为具有恒定边界的循环生成的代码慢。我不希望这非常依赖于平台 - 只是少了 2 条指令。还是我错过了什么?
-
这里我只是猜测,因为我没有看到 gcc 生成了什么,但我会检查缓存大小(一级缓存)、分支预测或管道内容
-
@angainor “现在,在一般情况下,数组当然可能大于 8 个元素”,正如您所提到的,它是一个排序数组,O(log(n)) 搜索可能(稍微)提高性能。
-
@starrify 谢谢,当然我已经为大型阵列做到了。我有一个八进制树结构——这就是为什么我需要在一个包含 8 个元素的数组中进行搜索。另外,我需要在树叶中进行一般的简短搜索。
标签: c gcc assembly compiler-optimization micro-optimization