assembly 使用SIMD/AVX/SSE进行树遍历

ojsjcaue  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(170)

我目前正在研究是否有可能加快货车Emde Boas(或任何树)树遍历的速度。给定一个搜索查询作为输入,该高速缓存行中已经有多个树节点(van Emde Boas布局),树遍历似乎是指令瓶颈。
作为SIMD/AVX/SSE指令的新手,我想从该领域的Maven那里了解一下,是否有可能同时将多个节点与一个值进行比较,然后找出下一步要遵循的树路径。我的研究引出了以下问题:
多少CPU周期/指令被浪费在SIMD/AVX/SSE寄存器等的构造上。如果构造比手动遍历整个子树花费更多的时间(大小为64字节的1个高速缓存行中的2+4+8个节点),则这将使其成为一种韦恩。
有多少CPU周期/指令被浪费在寻找正确的SIMD/AVX/SSE寄存器上,寄存器保存着要遵循哪条路径的答案?有没有人能想出一个聪明的方法,让那些 findMinimumInteger AVX指令可以在1(??)个CPU周期内决定这一点?
你猜呢?
另一种更复杂的加速树遍历的方法是一次运行多个搜索查询,当有很高的概率落在最后一个树层中紧密相连的节点上时。对此有什么猜测吗?如果它必须把那些不再属于同一个子树的查询放在一边,然后在完成树的第一次“并行遍历”后递归地找到它们。树查询具有连续的访问模式,但不是恒定的访问模式(query[i]总是〈query[i+1])。
重要:这是关于整数树的,这就是为什么使用货车Emde Boas树的原因(也许稍后会尝试x-快/y-快)
我很好奇你在这个问题上的50美分是多少,因为你可能会对大规模树的最高可实现性能感兴趣。提前感谢你花时间在这个问题上:-)

lp0sw83n

lp0sw83n1#

我已经使用SSE 2/AVX 2来帮助执行B+树搜索。下面的代码在AVX 2中对16个DWORD的完整缓存行执行“二进制搜索”:

// perf-critical: ensure this is 64-byte aligned. (a full cache line)
union bnode
{
    int32_t i32[16];
    __m256i m256[2];
};

// returns from 0 (if value < i32[0]) to 16 (if value >= i32[15]) 
unsigned bsearch_avx2(bnode const* const node, __m256i const value)
{
    __m256i const perm_mask = _mm256_set_epi32(7, 6, 3, 2, 5, 4, 1, 0);

    // compare the two halves of the cache line.

    __m256i cmp1 = _mm256_load_si256(&node->m256[0]);
    __m256i cmp2 = _mm256_load_si256(&node->m256[1]);

    cmp1 = _mm256_cmpgt_epi32(cmp1, value); // PCMPGTD
    cmp2 = _mm256_cmpgt_epi32(cmp2, value); // PCMPGTD

    // merge the comparisons back together.
    //
    // a permute is required to get the pack results back into order
    // because AVX-256 introduced that unfortunate two-lane interleave.
    //
    // alternately, you could pre-process your data to remove the need
    // for the permute.

    __m256i cmp = _mm256_packs_epi32(cmp1, cmp2); // PACKSSDW
    cmp = _mm256_permutevar8x32_epi32(cmp, perm_mask); // PERMD

    // finally create a move mask and count trailing
    // zeroes to get an index to the next node.

    unsigned mask = _mm256_movemask_epi8(cmp); // PMOVMSKB
    return _tzcnt_u32(mask) / 2; // TZCNT
}

每个bnode最终会有一个高度可预测的分支,以测试是否已经到达树的末尾。
这应该可以轻松扩展到AVX-512。
要预处理并去除慢的PERMD指令,可以使用以下语句:

void preprocess_avx2(bnode* const node)
{
    __m256i const perm_mask = _mm256_set_epi32(3, 2, 1, 0, 7, 6, 5, 4);
    __m256i *const middle = (__m256i*)&node->i32[4];

    __m256i x = _mm256_loadu_si256(middle);
    x = _mm256_permutevar8x32_epi32(x, perm_mask);
    _mm256_storeu_si256(middle, x);
}
tuwxkamq

tuwxkamq2#

根据您的代码,我已经对3个选项进行了基准测试:AVX 2支持的嵌套分支(4次跳转)和无分支变体。结果如下:

// Performance Table...
// All using cache-line size 64byteAligned chunks (van Emde-Boas Layout); loop unrolled per cacheline;
// all optimizations turned on. Each Element being 4 byte's. Intel i7 4770k Haswell @3.50GHz

Type        ElementAmount       LoopCount       Avg. Cycles / Query
===================================================================
AVX2        210485750           100000000       610 cycles    
AVX2        21048575            100000000       427 cycles           
AVX2        2104857             100000000       288 cycles 
AVX2        210485              100000000       157 cycles   
AVX2        21048               100000000       95 cycles  
AVX2        2104                100000000       49 cycles    
AVX2        210                 100000000       17 cycles 
AVX2        100                 100000000       16 cycles   

Type        ElementAmount       LoopCount       Avg. Cycles / Query
===================================================================  
Branching   210485750           100000000       819 cycles 
Branching   21048575            100000000       594 cycles 
Branching   2104857             100000000       358 cycles 
Branching   210485              100000000       165 cycles 
Branching   21048               100000000       82 cycles
Branching   2104                100000000       49 cycles 
Branching   210                 100000000       21 cycles 
Branching   100                 100000000       16 cycles   

Type        ElementAmount       LoopCount       Avg. Cycles / Query
=================================================================== 
BranchLESS  210485750           100000000       675 cycles 
BranchLESS  21048575            100000000       602 cycles 
BranchLESS  2104857             100000000       417 cycles
BranchLESS  210485              100000000       273 cycles 
BranchLESS  21048               100000000       130 cycles 
BranchLESS  2104                100000000       72 cycles 
BranchLESS  210                 100000000       27 cycles 
BranchLESS  100                 100000000       18 cycles

所以我的结论看起来像:当内存访问是最佳的,AVX可以帮助树的大于200 k的元素。低于这几乎没有任何惩罚支付(如果你不使用AVX的任何其他事情)。这是值得的晚上基准测试。感谢大家参与:-)

相关问题