成绩单

成绩单

学习练习

简介

SIMD:掩码与条件

标量代码依赖各种指令设置的标志位,以便根据某个条件进行分支。 不过,打包值并行表示的并非一个值,而是许多值。 同一个条件可能对某个通道不成立,而对另一个通道成立。

这就是 SIMD 代码默认采用_无分支_方式的原因。

打包比较通常不依赖标志位,而是直接在目标操作数中生成一个掩码。 对于每个通道,比较结果为真时,会用 1 填满整个通道;为假时,则用 0 填充。 若按有符号整数解读,为真的通道是-1,为假的通道是0。

然后可以用位运算组合这个掩码,以筛选特定的通道。

打包比较

标量cmp是通用的,因为它可以同时设置多个标志位。 然后,另一条指令可以读取这些标志位,进行分支或执行计算。 然而,打包比较既要检查条件,又要计算掩码,因此并不通用。 必须向比较指令提供正在测试的确切条件。

有两种做法:

  • 整数比较通过后缀来指定条件:eq表示相等,gt表示大于。 其他变体则通过组合其中一种比较的结果来构造。
  • 浮点比较则通过立即数编码来指定条件。 这个立即数的不同取值对应正在测试的不同条件。

除了整数比较使用特定的条件后缀之外,语法遵循我们已见过的相同结构:

  • 对于整数,p + cmp + 条件 + 大小(b、w、d或q)。
  • 对于浮点数,cmp + p + 大小(s或d)。 条件作为额外操作数放在立即数中传递。
整数比较

如前所述,整数比较只有相等和大于两种:

指令 描述
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq 逐通道相等
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq 逐通道有符号大于
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

要创建小于比较,请交换操作数并使用gt:a < b == b > a。 注意,比较是有符号的。 要执行无符号比较,请翻转两个操作数的最高位。 可以通过与一个只在最高位上置位的掩码做异或来实现。

Note

两个常用的惯用法是:

  1. 将寄存器与自身异或,得到全 0。
  2. 将寄存器与自身比较,得到全 1。

例如:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

全 0 和全 1 是常见的掩码,分别用于编码“处处为假”和“处处为真”。 它们也可以用来表示打包的0或打包的-1,这是常见的哨兵值。 例如,标记字符串末尾的 NUL 就是0。

浮点比较

浮点通道使用不同的形式:一条指令cmpps(64 位通道则用cmppd),条件通过立即数给出:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

NASM 还提供了伪指令,它们会映射到正确的立即数,也更容易记住。 在以下所有内容中,px中的x可以是s(32 位浮点数)或d(64 位浮点数):

伪指令 立即数 比较
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a 是 NaN 或 b 是 NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 a 和 b 都不是 NaN

使用掩码选择

掩码编码了条件的结果。 然后,可以根据该条件,逐通道地在两组值之间进行选择。 掩码为真时,我们从一组值中取该通道;为假时,从另一组值中取:

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

注意,pandn的不对称性在这里很有用:掩码位于目标位置,会被取反,并在一条指令中从b里选择。 这种模式是无分支选择的打包形式。 每个通道都会被计算,仅由掩码决定哪个值保留下来,完全不需要jcc。

专用混合指令

有些指令可以直接执行相同的选择,从掩码寄存器中按元素读取一位。 它们被称为混合指令:

指令 元素 掩码来源
pblendvb 字节 隐式xmm0
blendvps 32 位通道 隐式xmm0
blendvpd 64 位通道 隐式xmm0

注意,第一条指令遵循整数语法,而另外两条遵循浮点语法。 不过,由于这些指令只是选择原始字节,因此它们中的任何一条都可以用于整数和浮点数。 对于每个元素,当对应掩码元素的最高位为 0 时,混合指令保留目标;为 1 时,则取源。 只查看那个最高位,比较掩码正好满足这一点,因为它的通道要么全为 1,要么全为 0。 掩码寄存器始终是xmm0,它是隐式的:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

也可以使用pblendvb从比较掩码中选择任意其他大小的通道。 由于为真的通道中所有字节都是全 1,pblendvb会把它们全部选中。

Note

这些指令都在所执行的操作(blend)之后加上v。 这个v代表_变量_,因为这种选择不是静态的:它取决于一个寄存器。

也有不带v的变体,它们根据立即数进行选择。 它们遵循相同的模式:如果立即数的第i位被置位,就选择通道i。

从掩码回到标量

尽管 SIMD 代码很强大,但它缺少标量代码的许多灵活性。 在许多情况下,需要从打包寄存器回到标量指令的世界。 movmsk系列指令充当这两个世界之间的桥梁。 这些指令将每个通道的最高位提取到一个通用寄存器中:

指令 收集 结果宽度
pmovmskb 16 个字节各自的最高位 16 位
movmskps 4 个双字各自的最高位 4 位
movmskpd 2 个四字各自的最高位 2 位

如果在比较之后使用,每个被置位的位表示一个“true”通道,每个被清零的位表示一个“false”通道。 然后,可以像往常一样用标量指令操作这个结果。 例如,popcnt统计匹配的数量,tzcnt查找第一个匹配。 通用寄存器可以是 32 位或 64 位宽。

测试整个向量

test标量指令还有一个打包变体:ptest。 它与标量对应指令类似,会对两个操作数执行 AND 运算,但不修改它们。 与test不同,ptest还会执行 ANDN 运算,对第一个操作数取反。 因此,可以把ptest看作pand和pandn的非破坏性版本,它会根据结果设置标志位。 与这两条指令大致相同,ptest把整个 SIMD 寄存器当作单个通道,因此不接受大小前缀。 如果 AND 运算的结果为0,则设置ZF;如果 ANDN 运算的结果为0,则设置CF。 这意味着ptest可以用来检查全 1 掩码和全 0 掩码:

  1. 将ptest用于寄存器自身时,只有当寄存器全为 0 才设置ZF。 这模仿了常见的标量惯用法:将test用于寄存器自身来检查是否为0。
  2. 将ptest用于寄存器与全 1 掩码时,只有当寄存器全为 1 才设置CF。 此外,只有当寄存器全为 0 才设置ZF,因此可以同时检查两种掩码。
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

ptest的结果可以像往常一样用于分支,或用于setcc、cmovcc之类的无分支指令。

说明

你为一所学校负责评分站,每次为一个块给班级成绩打分。

每个块包含 4 个成绩,评分站对块中的每个成绩执行同样的操作。 分数是一个 32 位浮点数。 若干步骤会用到掩码:一个由 4 个通道组成的块,每个通道要么全为 1(表示该成绩的_是_),要么全为 0(表示_否_)。

你有 5 个任务。 操作数通过内存地址传入。 有些任务把答案写到结果地址,有些则直接返回。

本练习中的所有内存地址都是 16 字节对齐的。

Note

本练习中的计算应使用 SIMD 指令完成。

1. 标记高于阈值的成绩

第一步根据阈值对每个成绩进行判定。 当某个成绩的分数严格大于阈值时,该成绩才达标。 分数小于或等于阈值的成绩则不达标。

实现 flag_above_threshold 函数,它为每个高于其阈值的分数构建一个通道全为 1 的掩码,否则该通道全为 0。

这个函数按以下顺序接收实参:

  • result:缓冲区的内存地址,4 个掩码通道写入其中。
  • scores:分数的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
  • thresholds:每个通道的阈值的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

这个函数没有返回值。

2. 标记满分成绩

另一份报告会突出显示满分的成绩,也就是达到最高可能分数的成绩。

实现 flag_perfect 函数,它为每个等于其最高分的分数构建一个通道全为 1 的掩码,否则该通道全为 0。

这个函数按以下顺序接收实参:

  • result:缓冲区的内存地址,4 个掩码通道写入其中。
  • scores:分数的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
  • maxima:每个通道的最高分的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

这个函数没有返回值。

3. 分配等级

每个分数获得 1 到 3 的等级:

  • 分数小于或等于及格阈值50.0时,等级为 1。
  • 分数高于该阈值但未达到最高分时,等级为 2。
  • 分数为满分,也就是等于最高分时,等级为 3。

实现 assign_ranks 函数,它写入每个分数的等级。

你应该把及格阈值和各等级的取值定义为内存中的打包常量。 前 2 个任务中的函数可以复用:分数高于阈值时至少为等级 2,等于最高分时为等级 3。

这个函数按以下顺序接收实参:

  • result:缓冲区的内存地址,4 个等级写入其中,每个等级是一个 32 位无符号整数。
  • scores:分数的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
  • maxima:每个通道的最高分的内存地址,包含 4 个 32 位规格化浮点数(从不为NaN)。
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

这个函数没有返回值。

4. 统计不及格数

一年下来,每个学生会不断累积一个总等级。 评分站会统计全体学生中有多少个等级低于及格阈值,以便规划要额外开多少节课。

实现 count_failures 函数,它返回所有块中严格低于及格阈值的等级数量。 阈值以由 4 个相同通道组成的块给出,因此你只需加载一次,就能在每个块中重复使用。

这个函数按以下顺序接收实参:

  • ranks:等级的内存地址,由整数个 4 通道块组成,每个等级是一个 32 位无符号整数。
  • block_count:4 通道块的数量,始终大于0。
  • pass_threshold:及格阈值的内存地址,包含 4 个相同的 32 位整数。
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

这个函数以有符号 32 位整数返回计数。

5. 所有人都及格了吗?

在记录归档之前,评分站会检查全体学生是否全部通过:只要没有任何一个成绩在任何一个块中不及格,就算通过。

实现 all_passed 函数,如果所有学生都及格则返回1,否则返回0。 当某个学生在 failing 数组中对应的通道全为 0 时,该学生及格。

这个函数按以下顺序接收实参:

  • failing:不及格掩码的内存地址,由整数个 4 通道块组成,每个通道要么全为 1,要么全为 0。
  • block_count:4 通道块的数量,始终大于0。
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

这个函数以有符号 32 位整数返回答案,即1或0。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 成绩单 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。