【问题标题】:how to test condition properly: je or jge如何正确测试条件:je 或 jge
【发布时间】:2017-01-26 00:25:28
【问题描述】:

我有时会使用这种模式来迭代一些东西的数组:

    mov [rsp+.r12], r12 ; Choose a register that calls inside the loop won't modify
    mov r12, -1
.i:
    inc r12
    cmp r12, [rbp-.array_size]
    je .end_i
    ; ... program logic ...
    jmp .i
.end_i:
    mov r12, [rsp+.r12]

我知道测试相等就足够了,但不应该“安全地”测试“大于或等于”(防止不会发生的情况)。

在这种情况下应该使用 je 还是 jge?

我在询问可以减少引入错误的可能性的具体技巧。

【问题讨论】:

  • 我一直有点喜欢测试一个范围而不是仅仅测试相等性的想法,以防万一发生意外翻转或其他情况。但在 x86 asm 中,请记住 cmp/jge 不能在 Core2 上进行宏融合(在 32 位模式下),但 cmp/je 可以。我认为这会更相关,直到我检查并发现只有 Core2 而不是 Nehalem 无法融合它,因为宏融合在 Core2 上的 64 位模式下根本不起作用。 (后来的微架构没有这个限制,可以宏融合越来越多的组合。)
  • 为什么你要显示一些奇怪的 r12 溢出/重新加载以释放它用作临时计数器?这完全无关紧要(而且看起来不像有效的代码)。肯定有一些寄存器已经死了,你可以不用保存就可以使用。
  • @Peter,应该怎么写?我认为 r12 是一个不错的选择,因为像 printf 这样的循环内的函数调用不会修改调用保存的 r12 寄存器,而且我们不需要手动保存和恢复调用周围的计数器。如有错误请指正。
  • 哦,是什么意思?当然,r12 是一个不错的选择,但如果您不需要 64 位计数器(无 REX 前缀),ebx 或 ebp 可能是更好的选择。最主要的是,在堆栈框架中有一个名为.r12 的命名点很奇怪。通常,如果您要命名某物,则使用具有语义含义的标签。因此,如果r12 之前持有some_value,您将存储它。在循环之后,也许将 r12 用于其他用途。
  • 不,这与我所说的相反。更新了我的答案,感谢您让我知道您迷失了短的 cmets,因此我可以扩展它,而不是浪费我们的时间 :)

标签: assembly x86 idioms loop-counter


【解决方案1】:

我一直有点喜欢测试一个范围而不是仅仅为了相等的想法,以防万一意外翻转或其他什么。但在 x86 asm 中,请记住 cmp/jge 不能在 Core2 上使用 macro-fuse(在 32 位模式下),但 cmp/je 可以。我认为这会更相关,直到我检查 Agner Fog's microarch pdf 并发现只有 Core2 而不是 Nehalem 无法融合它,因为宏融合在 Core2 上的 64 位模式下根本不起作用. (后来的微架构没有这个限制,可以宏融合越来越多的组合。)

根据计数器的不同,您通常可以在没有 CMP 的情况下倒计时 (dec/jnz)。通常你知道它不需要是 64 位的,所以你可以使用 dec esi / jnz 或其他什么。 dec esi / jge 确实适用于已签名的计数器,但 dec 没有设置 CF,因此您不能(有用地)使用 JA。

你的循环结构,中间有一个if() break,最后有一个jmp,对于asm来说不是惯用的。正常是:

mov ecx, 100

.loop:             ; do{
    ;; stuff
    dec ecx
    jge .loop      ; }while(--ecx >= 0)

您可以使用 jg 仅重新启动具有正 ecx 的循环,即从 100..1 而不是 100..0 循环。

在循环中有一个未采用的条件分支一个采用的无条件分支效率较低。


扩展讨论cmets关于保存/恢复r12:通常你会做这样的事情:

my_func:
    ; push rbp
    ; mov  rbp, rsp      ; optional: make a stack frame

    push   rbx           ; save the caller's value so we can use it
    sub    rsp, 32       ; reserve some space

    imul   edi, esi, 11   ; calculate something that we want to pass as an arg to foo
    mov    ebx, edi       ; and save it in ebx
    call   foo
    add    eax, ebx       ; and use value.  If we don't need the value in rbx anymore, we can use the register for something else later.

    ...  ;; calculate an array size in ecx

    test   ecx, ecx                ; test for the special case of zero iterations *outside* the loop, instead of adding stuff inside.  We can skip some of the loop setup/cleanup as well.
    jz    .skip_the_loop

    ; now use rbx as a loop counter
    mov    ebx, ecx
.loop:
    lea    edi, [rbx + rbx*4 + 10]
    call   bar                     ; bar(5*ebx+10);
    ; do something with the return value?  In real code, you would usually want at least one more call-preserved register, but let's keep the example simple
    dec    ebx
    jnz    .loop
.skip_the_loop:

    add   rsp, 32         ; epilogue
    pop   rbx

    ;pop  rbp             ; pointless to use LEAVE; rsp had to already be pointing to the right place for POP RBX
    ret

请注意我们如何在函数内部使用 rbx 处理一些事情,但只保存/恢复一次。

【讨论】:

  • 几乎是我的想法。测试通常在循环结束时进行以保存额外的 JMP,并且出于宏融合和安全考虑,我避免了带符号的比较。一个常见的错误是忘记测试负输入。无符号比较没有这个问题。
  • 我还要补充一点,我会根据最方便的方式以各种不同的样式编写循环;从 0 到 N-1(INC/CMP/JB 或 INC/CMP/JBE);从 N 到 1 (DEC/JNZ);从 N-1 到 -1 (INC/JNC);以及上面的变体,使用指针而不是索引。
  • @icecreamsword:有趣的是,在 asm 中编程通常会比在 C 中更仔细地选择函数参数的符号和宽度。在 C 中,有些人只是让所有与数组相关的东西 @987654331 @.
  • @BulatM.: 使用-O3 -fno-omit-frame-pointer 查看一些编译器输出。编写一个调用外部函数的函数,以便编译器必须保存/恢复某些内容。也许让它传递一个指向局部变量的指针,所以编译器必须保留堆栈空间。
  • @BulatM.: 不,不定义extern函数,只写一个原型。如果编译器没有定义,它就不能内联......添加与弹出:这是一个权衡,但 gcc 不久前做出了这个决定,当时权衡不同(在 CPU 有堆栈引擎来进行推送/弹出之前)便宜的)。 clang 确实倾向于只弹出一个临时注册以调整 8。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-05-22
  • 2017-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-05
相关资源
最近更新 更多