let mut j = 32;
let mut m = 0xffffffffu64;
while j != 0 {
let mut k = 0;
while k < 64 {
unsafe {
let a = *x.get_unchecked(k + j);
let b = *x.get_unchecked(k);
let t = (a ^ (b >> j)) & m;
*x.get_unchecked_mut(k + j) = a ^ t;
*x.get_unchecked_mut(k) = b ^ (t << j);
}
k = (k + j + 1) & !j;
}
j >>= 1;
m ^= m << j;
}
4条答案
按热度按时间pkwftd7m1#
矩阵转置的基本递归方案是将矩阵表示为分块矩阵
字符串
通过首先转置A、B、C和D中的每一个,然后交换B和C来转置。在实践中,这意味着应用一系列越来越粗糙的混合步骤,首先使用逐位操作,然后使用置换操作。
这可以例如实现为as follows:
型
我们可以看到,性能与Lee's approach相当,大约快三倍。
型
进一步的改进可能是可能的。例如,可以将合适的置换掩码与
tbl
指令集一起使用,以一次执行多个转置步骤(特别是步骤3到5)。请注意,该算法只需要加载和写出数组两次。一次是转置每个32x32子数组(两次调用
xpose_half
),另一次是交换右上角和左下角的32x32子数组。在这两种情况下,使用最大宽度64字节的加载和存储,将内存操作量降至最低。nr9pn0ug2#
我做了一些调查和阅读其他答案,并实现了一些不同的方法(在Rust和组装),并在我的Apple M2 Max上进行基准测试。
tl;dr使用 neon 说明书可获得比非 neon 版本高2倍的优势。
结果如下:
字符串
ld4
指令等稍微优化,但涉及更多的内存移动。型
Rust/LLVM会自动向量化一点,但不是很多(Rust 1.76.0)。
在Hacker's Delight展开版本中可能还有更多的改进空间,可以切换到汇编/intrinsic以实现更有效的内存管理,并可能使用旋转指令进行更有效的交换(正如本书所述)。
Lee的代码将转置的比特输出到内存的一个单独区域,这是它稍微慢一点的原因之一。如果我们不做回拷,它就在fuz的答案的误差范围内。如果我们做就地转换,可能还有一些改进的空间。
福兹的回答似乎是目前的赢家。
我已经在Rust中实现了所有这些(在适用的情况下使用内联汇编):https://github.com/swenson/binary_matrix/tree/simd-transpose/src
myzjeezk3#
字符串
这可能是完美的新手代码:完美地最小化零延迟。
我以为福兹也会送类似的东西,但是......
尽管如此,这毕竟是一个新手版本,我会给它一个C级(befriedigend)给予。
为什么它不值得A级将在下次更新时明确:带宽限制和功耗,这是现代多核处理器的真实的问题。
Fuz's code(F级,万向轴):
型
vd2z7a6w4#
数据大小远远超过寄存器组的大小。您可以选择:
而连续存储总是更可取的。
字符串
我尽了最大努力让编译器生成优化的机器码,但他们根本不听:godbolt特别是GCC(一如既往)。
我明天会添加一个汇编版本。