如何生成256位掩码

时间:2019-04-15 15:21:08

标签: c assembly bit-manipulation bit zig

我有一个uint64_t [4]数组,我需要生成一个掩码, 这样,如果该数组是256位整数,则等于 (1 << w)-1,其中w从1到256。

我想出的最好的东西是无分支的,但是它需要很多指令。它在Zig中是因为Clang似乎没有暴露llvm的饱和减法。 http://localhost:10240/z/g8h1rV

有更好的方法吗?

var mask: [4]u64 = undefined;
for (mask) |_, i|
    mask[i] = 0xffffffffffffffff;
mask[3] ^= ((u64(1) << @intCast(u6, (inner % 64) + 1)) - 1) << @intCast(u6, 64 - (inner % 64));
mask[2] ^= ((u64(1) << @intCast(u6, (@satSub(u32, inner, 64) % 64) + 1)) - 1) << @intCast(u6, 64 - (inner % 64));
mask[1] ^= ((u64(1) << @intCast(u6, (@satSub(u32, inner, 128) % 64) + 1)) - 1) << @intCast(u6, 64 - (inner % 64));
mask[0] ^= ((u64(1) << @intCast(u6, (@satSub(u32, inner, 192) % 64) + 1)) - 1) << @intCast(u6, 64 - (inner % 64));

2 个答案:

答案 0 :(得分:1)

您是否针对带有256位向量的AVX2定位了x86-64?我认为这是一个有趣的案例。

如果是这样,您可以按照一些说明使用饱和减法和可变计数移位来完成此操作。

x86 SIMD 移位像vpsrlvq一样使移位计数饱和,当计数> =元素宽度时,将所有位移出。与整数移位不同,移位计数是屏蔽的(因此可以环绕)。

对于最低的u64元素,从全1开始,我们需要对bitpos> = 64保持不变。或者对于较小的位位置,将其右移{{ 1}} 。正如您所观察到的那样,无符号饱和减法看起来像是要为较大的位元创建移位计数为0的方式。但是x86仅具有SIMD饱和减法,并且仅用于字节或字元素。但是,如果我们不在乎bitpos> 256,那就可以了,我们可以在每个u64的底部使用16位元素,并在64-bitpos的其余部分使用0-0

您的代码看起来过于复杂,创建了u64并进行XOR运算。 我认为直接在(1<<n) - 1元素上使用可变计数移位要容易得多。

我不知道Zig,所以要做任何让它发出这样的asm的事情。希望这很有用,因为您标记了此;应该易于转换为C或Zig(如果有)的内在函数。

0xFFFF...FF

如果输入整数从内存中开始,您当然可以有效地将其直接广播加载到ymm寄存器中。

平移偏移矢量当然可以像所有全零一样悬挂在循环之外。


在输入= 77的情况下,高2个元素通过256-77 = 179和192-77 = 115位的移位而置零。经NASM + GDB测试,EDI = 77,结果为

default rel
section .rodata
shift_offsets:  dw  64, 128, 192, 256        ; 16-bit elements, to be loaded with zero-extension to 64

section .text
pos_to_mask256:
    vpmovzxwq   ymm2, [shift_offsets]      ; _mm256_set1_epi64x(256, 192, 128, 64)
    vpcmpeqd    ymm1, ymm1,ymm1            ; ymm1 = all-ones
                                  ; set up vector constants, can be hoisted

    vmovd         xmm0, edi
    vpbroadcastq  ymm0, xmm0           ; ymm0 = _mm256_set1_epi64(bitpos)

    vpsubusw      ymm0, ymm2, ymm0     ; ymm0 = {256,192,128,64}-bitpos with unsigned saturation
    vpsrlvq       ymm0, ymm1, ymm0     ; mask[i] >>= count, where counts >= 64 create 0s.

    ret

GDB首先打印低位元素,与Intel表示法/图表相反。该向量实际上是(gdb) p /x $ymm0.v4_int64 {0xffffffffffffffff, 0x1fff, 0x0, 0x0} ,即64 + 13 = 77个1位,其余均为0。其他测试用例

  • 0, 0, 0x1fff, 0xffffffffffffffff:掩码=全零
  • edi=0:掩码= 1
  • ...:掩码= edi=1的最底一位,然后为零
  • edi:mask =除顶部元素的最高位以外的所有数字
  • edi=255:掩码=全部
  • edi=256:掩码=全部。 (无符号减法到处都饱和为0。)

您需要AVX2进行可变计数移位。 psubusb/w is SSE2,因此您可以考虑使用SIMD完成该部分操作,然后返回标量整数进行移位,或者一次只对一个元素使用SSE2移位。像psrlq xmm1, xmm0,它以edi>256的低64位作为xmm1所有元素的移位计数。

大多数ISA 没有具有饱和的标量减法。我认为某些ARM CPU可以处理标量整数,但x86却不能。 IDK您正在使用什么。

在x86(和许多其他ISA)上,您有2个问题:

  • 对低位元素保留全为一(修改移位结果或将移位计数饱和为0)
  • 为高位元素生成xmm0,使其高于包含掩码最高位的元素。 x86标量移位根本无法做到这一点,因此您可以在这种情况下将移位输入0输入。也许使用0基于cmovsub设置的标志或其他东西来创建它。
192-w

嗯,尽管如此,不能将减法饱和到0以保持全1。

如果针对x86以外的ISA进行调优,则可以考虑其他一些选择。也许x86上也有更好的东西。用 count = 192-w; shift_input = count<0 ? 0 : ~0ULL; shift_input >>= count & 63; // mask to avoid UB in C. Optimizes away on x86 where shr does this anyway. 创建全1或全0是一个有趣的选项(广播符号位),但是当sar reg,63的符号位= 0时,我们实际上需要全1。

答案 1 :(得分:0)

以下是一些可以编译并运行的Zig代码:

const std = @import("std");

noinline fn thing(x: u256) bool {
    return x > 0xffffffffffffffff;
}

pub fn main() anyerror!void {
    var num: u256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
    while (thing(num)) {
        num /= 2;
        std.debug.print(".", .{});
    }
    std.debug.print("done\n", .{});
}

Zig master从中生成相对干净的x86汇编程序。