![](/img/trans.png)
[英]How to fast calculate the normalized l1 and l2 norm of a vector in C++?
[英]Fast merge of sorted subsets of 4K floating-point numbers in L1/L2
在现代(SSE2 +)x86处理器上合并多达4096个32位浮点数的数组的排序子集的快速方法是什么?
请假设以下内容:
可行性的主要标准:比L1-LSD基数排序更快。
考虑到上述参数,我很想知道是否有人知道这样做的合理方法! :)
这是一种非常天真的方式。 (请原谅任何凌晨4点谵妄引起的伪代码错误;)
//4x sorted subsets
data[4][4] = {
{3, 4, 5, INF},
{2, 7, 8, INF},
{1, 4, 4, INF},
{5, 8, 9, INF}
}
data_offset[4] = {0, 0, 0, 0}
n = 4*3
for(i=0, i<n, i++):
sub = 0
sub = 1 * (data[sub][data_offset[sub]] > data[1][data_offset[1]])
sub = 2 * (data[sub][data_offset[sub]] > data[2][data_offset[2]])
sub = 3 * (data[sub][data_offset[sub]] > data[3][data_offset[3]])
out[i] = data[sub][data_offset[sub]]
data_offset[sub]++
编辑:
使用AVX2及其聚集支持,我们可以同时比较多达8个子集。
编辑2:
根据类型转换,有可能在Nehalem上每次迭代削减3个额外的时钟周期(mul:5,shift + sub:4)
//Assuming 'sub' is uint32_t
sub = ... << ((data[sub][data_offset[sub]] > data[...][data_offset[...]]) - 1)
编辑3:
通过使用两个或更多个max
,可能会在某种程度上利用无序执行,尤其是在K变大时:
max1 = 0
max2 = 1
max1 = 2 * (data[max1][data_offset[max1]] > data[2][data_offset[2]])
max2 = 3 * (data[max2][data_offset[max2]] > data[3][data_offset[3]])
...
max1 = 6 * (data[max1][data_offset[max1]] > data[6][data_offset[6]])
max2 = 7 * (data[max2][data_offset[max2]] > data[7][data_offset[7]])
q = data[max1][data_offset[max1]] < data[max2][data_offset[max2]]
sub = max1*q + ((~max2)&1)*q
编辑4:
根据编译器的智能,我们可以使用三元运算符完全删除乘法:
sub = (data[sub][data_offset[sub]] > data[x][data_offset[x]]) ? x : sub
编辑5:
为了避免代价高昂的浮点比较,我们可以简单地reinterpret_cast<uint32_t*>()
数据,因为这会导致整数比较。
另一种可能性是利用SSE寄存器,因为它们不是键入的,并且明确地使用整数比较指令。
这是因为运算符< > ==
在解释二进制级别的浮点时产生相同的结果。
编辑6:
如果我们充分展开循环以使值的数量与SSE寄存器的数量相匹配,我们就可以对正在进行比较的数据进行分级。
在迭代结束时,我们将重新传输包含所选最大/最小值的寄存器,并将其移位。
虽然这需要稍微重新编写索引,但它可能比使用LEA
乱丢循环更有效。
这更像是一个研究课题,但我确实发现本文讨论了使用d-way合并排序最小化分支错误预测。
想到的最明显的答案是使用堆的标准N路合并。 那将是O(N log k)。 子集的数量在16到256之间,因此最坏情况的行为(每个16个项目的256个子集)将是8N。
缓存行为应该......合理,尽管不完美。 大多数操作所在的堆可能始终保留在缓存中。 写入的输出数组部分也很可能位于缓存中。
你拥有的是16K数据(带有排序子序列的数组),堆(1K,最坏情况)和排序输出数组(再次16K),并且你希望它适合32K缓存。 听起来像是一个问题,但也许不是。 最可能被换出的数据是插入点移动后输出数组的前面。 假设已排序的子序列是相当均匀分布的,应该经常访问它们以使它们保持在缓存中。
您可以合并int数组(昂贵)分支免费。
typedef unsigned uint;
typedef uint* uint_ptr;
void merge(uint*in1_begin, uint*in1_end, uint*in2_begin, uint*in2_end, uint*out){
int_ptr in [] = {in1_begin, in2_begin};
int_ptr in_end [] = {in1_end, in2_end};
// the loop branch is cheap because it is easy predictable
while(in[0] != in_end[0] && in[1] != in_end[1]){
int i = (*in[0] - *in[1]) >> 31;
*out = *in[i];
++out;
++in[i];
}
// copy the remaining stuff ...
}
注意(* [in] [*] in [1])>> 31等于* in [0] - * in [1] <0,相当于[in] [0] <* in [1]。 我之所以用bithift技巧而不是写下来的原因
int i = *in[0] < *in[1];
并非所有编译器都为<version生成分支免费代码。
不幸的是你使用的是浮点数而不是整数,它们最初看起来像是一个showstopper,因为我没有看到如何在[1]分支中自由地实现* [0] <*。 但是,在大多数现代体系结构中,您将正向浮点(也没有NAN,INF或类似的东西)的位模式解释为整数并使用<进行比较,您仍然可以获得正确的结果。 也许你将这个观察延伸到任意浮标。
已经详细研究了SIMD排序算法。 论文在多核SIMD CPU架构上进行排序的高效实现描述了一种有效的算法,用于执行您所描述的内容(以及更多内容)。
核心思想是你可以减少合并两个任意长的列表来合并k个连续值的块(其中k的范围从4到16):第一个块是z[0] = merge(x[0], y[0]).lo
。 为了获得第二个块,我们知道剩余的merge(x[0], y[0]).hi
包含来自x
nx
元素和来自y
ny
元素,其中nx+ny == k
。 但是z[1]
不能包含x[1]
和y[1]
元素,因为这需要z[1]
包含多于nx+ny
元素:所以我们只需要找出x[1]
哪一个并且y[1]
需要添加。 具有较低第一元素的那个必然首先出现在z
,所以这只是通过比较它们的第一个元素来完成的。 我们只是重复一遍,直到没有更多的数据要合并。
伪代码,假设数组以+inf
值结束:
a := *x++
b := *y++
while not finished:
lo,hi := merge(a,b)
*z++ := lo
a := hi
if *x[0] <= *y[0]:
b := *x++
else:
b := *y++
(注意这与通常的合并标量实现有多相似)
在实际实现中,条件跳转当然不是必需的:例如,您可以通过xor
技巧有条件地交换x
和y
,然后无条件地读取*x++
。
merge
本身可以用bitonic排序实现。 但是如果k很低,则会有很多指令间依赖性导致高延迟。 根据您必须合并的阵列数量,您可以选择k足够高,以便屏蔽merge
的延迟,或者如果可以交错几个双向合并。 有关详细信息,请参阅该文章。
编辑 :下面是k = 4时的图。所有渐近线都假定k是固定的。
大灰色框合并两个大小为n = m * k的数组(在图中, m = 3)。
最后,为了扩展我们的双向合并以合并多个阵列,我们以经典的分而治之的方式安排了大灰盒子。 每个级别的元素数量都具有线性复杂度,因此总复杂度为O( n log( n / n0 )),其中n0是排序数组的初始大小, n是最终数组的大小。
你可以做一个简单的合并内核来合并K列表:
float *input[K];
float *output;
while (true) {
float min = *input[0];
int min_idx = 0;
for (int i = 1; i < K; i++) {
float v = *input[i];
if (v < min) {
min = v; // do with cmov
min_idx = i; // do with cmov
}
}
if (min == SENTINEL) break;
*output++ = min;
input[min_idx]++;
}
没有堆,所以很简单。 坏的部分是它是O(NK),如果K很大则可能是坏的(不像堆的实现是O(N log K))。 那么你只需选择一个最大K(4或8可能是好的,然后你可以展开内循环),并通过级联合并做更大的K(通过对列表组进行8向合并来处理K = 64,然后是8路合并结果)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.