【问题标题】:Average of marks with bytes带有字节的标记的平均值
【发布时间】:2019-03-03 15:38:42
【问题描述】:

我正在计算 3 分的平均值:

g0  dw  70
g1  dw  100
g2  dw  65  

xor rax, rax
xor rcx, rcx

mov ax, [g0]
inc rcx

add ax, [g1]
inc rcx 

add ax, [g2]
inc rcx

xor rdx, rdx

idiv rcx

成绩不需要是单词,因为对于平均而言,字节就足够了,但总和必须是一个单词以避免溢出(即使用此算法)。

如何将成绩转换为字节?使用db 是不够的,因为那样我必须将ax 更改为al,但这会导致最后溢出。 我不能指示mov/add 只从[g*] 中获取一个字节,因为这会导致操作数大小不匹配。

我正在使用 yasm。

【问题讨论】:

  • 如果你知道总和只是字,你可以做dx:ax / cx除以idiv cx ...在x86-64的大多数情况下,使用字寄存器不会带来性能优势,甚至可能由于现代 CPU 的更复杂的寄存器管理而招致额外的惩罚,但在 DIV/IDIV 指令的情况下,64 位变体可能仍然比 32/16/8 位变体慢得多。所以这是一件需要考虑的事情(因为您的代码只能写为 16b,尽管在 x86-64 模式下,出于性能原因(更短的指令操作码)我可能会直接使用 32b)。
  • 如果您坚持使用idiv(签名除法),您还应该在签名变体中包含其他部分(然后使用movsx而不是建议的movzx,并使用cqo签名-将rax 扩展到rdx:rax 而不是xor rdx,rdx .. 或使用div 与您的zx485 代码进行无符号除法。
  • @Ped7g 我问这个是因为我认为我可以从使用更少的字节中受益。只有在内存不足的情况下,我才应该记住这一点?
  • 特殊情况可能需要特殊的解决方案,所以继续练习你能想到的任何东西。 x86-64 的性能问题从来都不是简单的,机器非常复杂。您将标记更改为 8 位类型的方式将节省内存,因此如果您在输入端有数十亿个标记,这绝对是有道理的(但总和需要超过 64 位,两个 64b 寄存器对将提供总计 128b 的总和,这在这种数十亿马克的特殊情况下就足够了)。更短的代码确实使 L1 指令缓存等更容易... ...
  • 然后对一些不明显的事情有更复杂(轻微!)的惩罚,比如以不可预测的方式使用寄存器的一部分,迫使 CPU 将两个独立代码行的结果合并回一个某些指令的价值,限制乱序执行以不同的顺序做更多的事情。无论如何,首先要专注于编写易于维护的 100% 正确代码(即,与细微优化相比,有符号 div 与无符号值是主要问题)。一旦你有了工作代码,你就可以测量它的性能,看看瓶颈在哪里,然后解决那个部分。

标签: assembly x86 64-bit x86-64 yasm


【解决方案1】:

如果您使用另一个寄存器进行添加,您可以将变量更改为字节。所以以下是可能的:

g0  db  70
g1  db  100
g2  db  65  

使用MOVZX instruction 并注明memory reference size BYTE

xor ecx, ecx              ; clear counter register and break dependencies

movzx eax, BYTE [g0]      ; movzx loads g0 and fills the upper bytes with zeroes
inc ecx

movzx edx, BYTE [g1]      ; move byte from g1 to dl and zero-extend
add eax, edx              ; add the widened integers
inc ecx 

movzx edx, BYTE [g2]      ; the upper half of RDX is zeroed automatically by this instruction, but 32-bit is fine.
add eax, edx
inc ecx

xor edx, edx
div ecx                   ; unsigned division of EAX / 3
                          ; quotient in EAX, remainder in EDX
;mov [average], al        ; or do whatever you want with it.

也不需要使用 64 位操作数大小。对于大多数指令来说,32 位是 x86-64 的“标准”操作数大小。

当然,您可以将eaxedx 寄存器引用分别更改为raxrdx,因为这些值已被零扩展为整个寄存器宽度。如果您要添加的分数超过2^32 / 100,则可以使用它来避免溢出。

如果您要重复这个固定次数,mov ecx, count 而不是使用那么多 inc 指令。 inc 如果这是在循环体中并且您正在递增指向成绩数组的指针,那么inc 会有意义,但是完全展开的部分好处是不必inc 任何东西来计算交互。

【讨论】:

  • yasm 是否默认使用byte 大小而不明确指定它(在movzx 中)?还是它像 MASM 一样从db 跟踪它? (我很好奇)
  • 在这种特殊情况下,您可以删除xor rax,rax,因为第一个movzx 将完全设置它。另外要学习 OP 一些新的东西,您可以证明 movzx eax,.. 就足够了,因为 32b 寄存器写入将自动清除高 32 位(原始 OP xor rax,rax 也是如此,其中 xor eax,eax 足以获得相同的结果,但是使用短 1 字节的操作码进行编码)。
  • 感谢您的补充。我为 NASM/YASM 添加了缺少的 BYTE 指令。
  • @Ped7g 在zx485的代码中我应该使用xor ecx, ecxxor edx, edx;我应该总是更喜欢他们而不是他们的r64 同行吗?
  • @J.Poe 基本上是的,它们的编码短了 1 个字节,并且执行完全相同的操作。但是不要过分强调自己,如果您现在更喜欢源代码清晰度rdx,请使用它。但是,一旦您在 64b 模式下使用寄存器的 32 位部分,无论如何您都会遇到 clear-upper-32,因此您应该了解该功能是设计的(由 AMD 公司提供)。 (正如谚语所说,“过早的优化是万恶之源”:) ...这不是真的,臃肿粗心的程序员是根源,但过早的优化至少排在前 5 位:))
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-01
  • 1970-01-01
相关资源
最近更新 更多