在这个问题中,一些答案显示了如何在不使用"if"语句的情况下做出决策,但是我怀疑这是可能的,因为"if"不是生成jump指令的唯一语句.
给定一个固定的编译语言(例如C++),生成的程序集可以在不使用jump和goto指令的情况下做某种决策吗?
请举例说明在肯定答案的情况下不使用此类指令的简单if/else语句.
ARM体系结构具有一个有趣的条件执行特性.在完全ARM模式下运行,几乎每条指令都可以附加条件.这些与B牧场说明中使用的条件相同.诸如add r0, r0, #15将始终执行的指令,但是addeq r0, r0, #15只有在设置了零标志时才会执行的指令.使用分支的等价物将是:
beq afteradd ; skip add if equal
add r0, r0, #15 ; add 15 to R0
afteradd:
Run Code Online (Sandbox Code Playgroud)
在使用Thumb-2的核心上运行时,条件执行受到更多限制,但仍可以使用该IT指令.该指令将在不使用分支的情况下创建"if-then"构造.该IT构造实际上是ARM的统一汇编语言的一部分,无论您是为ARM还是Thumb-2编写,都应该使用它.这是上面条件添加的UAL版本.
it eq ; if equal
addeq r0, r0, #15 ; then add 15 to r0
Run Code Online (Sandbox Code Playgroud)
该IT结构可以不仅仅包括单指令多.使用一系列T和E在指令中,您可以添加更多条件指令.在下面的例子中,R0如果设置了零标志,我们将添加15 ,否则我们将从中减去15 R0.该ITE手段,相当字面上看,IF-THEN-ELSE.第一条指令应该具有与您的ITE条件匹配的条件,然后第二条指令将成为"else",并且应该具有与条件相反的ITE条件.
ite eq ; if equal
addeq r0, r0, #15 ; then add 15 to r0
subne r0, r0, #15 ; else subtract 15 from r0
Run Code Online (Sandbox Code Playgroud)
我想这种情况确实会使用,这可能违背了你的要求.在执行方面,这实现了if而不使用跳转.
请参阅@Marco van de Voort 的回答,了解编写无分支代码的简单“正常”方法列表(cmov/其他谓词执行、cmp/setcc、将标志复制到 GP 寄存器等)。这形成了一个构建块,用于制作无分支版本的东西,使用分支会更好地完成。
当你说“做出决定”时,听起来你真的在问这样的机器是否完全可编程。
计算机科学中计算理论的一部分是弄清楚不同的计算模型有多强大。这与模型的效率无关,而是关于可以用它计算什么。我怀疑没有条件分支指令的寄存器机在功能上不等同于图灵机,又名通用计算机。(即使您允许非间接无条件分支)。
更新:即使没有自修改代码,x86 的mov指令也是图灵完备的,只需使用正常的索引寻址模式。您还需要一个围绕加载和存储序列的循环。 TODO:重写此答案无效的部分。这增加了没有自修改代码的无条件分支的价值(例如,让IP在实模式下回绕,或其他奇怪的技巧来做到这一点,没有 a jmp,见下文)。
我认为mov唯一的图灵机有点像在 x86 的寄存器和内存中模拟另一台机器,所以不是 x86 [R/E]IP 分支,通用寄存器中的值的行为就像一个程序计数器,它是分支。(或穿越磁带的图灵机)
你可以制作非常精简的架构,这些架构仍然是通用计算机,但我从目前所见的情况中收集到的是,任何类似于寄存器机(如 x86、ARM 等)的计算模型都需要某个版本的条件分支成为通用计算机。
另请参阅此 SO 问题:解决计算机程序任何问题的最小指令集,尤其是维基百科One Instruction Set Computer文章中的 Bit-manipulating machine 部分;它最不像条件分支,但仍然做同样的工作。
我不知道计算把准确的界限或形式化描述的理论已经足够,你有什么可以没有条件分支计算(寄存器机器像x86或ARM),但我可以说的东西:
我可以说,如果有足够的指令空间,您可以对数组进行排序,但也许只有当您为该数组长度生成正确的指令序列时。使用 cmov 等无分支技术,您可以根据比较有条件地交换两个内存位置。一个很好的算法选择是排序网络。
您可以完全展开任何已知长度的循环,也可以通过展开您将需要的最大迭代次数来处理可变长度循环。当循环计数器超过数组末尾时,您可以使用无分支技术从虚拟地址加载/存储。(例如,使用 cmov 将您的指针设置为指向一个小的静态数组,该数组只存在于供您存储垃圾而不影响其他任何东西。您的代码将始终执行相同的固定数量的存储,但这些存储中的不同输入具有不同的输入可以进入垃圾场。)任何指令的ARM 谓词执行意味着您可以进行条件存储,而无需静态垃圾场。
完全展开合并排序应该可以很好地工作,因为对于给定的数组大小,它总是做相同数量的工作。另一方面,快速排序会非常不方便,因为工作量随数据和分区选择而变化。此外,它有一个 O(n^2) 最坏的情况,所以你至少需要那么多代码。
由于您必须完全展开您想要运行的任何东西,这意味着您需要知道它将运行多长时间。这意味着您无法实现完整的图灵机,因为并非所有图灵机都会停止。(更不用说不可能存在决定任何给定图灵机是否停止的算法)。
自修改代码可能会提高效率,但如果您仍然不允许重写自己的机器代码以包含分支指令,我认为它不会增加计算能力。(我们必须排除 SMC 生成任何类型的分支,而不仅仅是条件分支指令,因为我们可以有条件地生成无条件分支指令。)
我认为固定循环中的 SMC 可能比循环中的非自修改代码更强大,即使不允许 SMC 生成跳转指令。有关无分支指令(例如定时器中断和IP环绕)的循环的一些疯狂想法,请参见下文。
仅使用无条件跳转(无条件跳转),您可以创建循环来处理与不使用跳转相同的问题的任意大小版本。但是你的程序不能终止。如果有某种“终止程序”指令,将其放入循环内将使其成为非循环。
所以你最终可能得到的是一个程序,它最终对一个数组进行了排序(或者它的工作是什么),也许它通过在某处存储一个“真”来表示这个事实,并在不影响数据的情况下保持循环(使用无分支条件代码) )。您可以将其视为具有“传输触发”退出函数,因此可以有条件地使用它。
更多模拟无条件跳转的方法,以便您可以创建循环,我认为增加了自我修改代码的能力。(即使您避免使用该权力有条件地分支)。
函数调用指令很容易被滥用来实现简单的跳转,所以我们显然想排除它们。
在 x86 上,您可以使用call跳转到仅弹出堆栈的返回地址而不是返回的代码。(所以你可以实现一个循环)。要从 x86 的间接call指令中构建条件分支,您可以使用无分支技术(如 cmov)将调用后的指令或不同的目标获取到寄存器中,然后运行call rax. 或者更简单地说,将 eax 设置为 0 或 1(例如来自 setcc),您可以运行call [target_table + rax*8]以在分支的两个目的地之间进行选择。
同样,ret很容易被滥用为间接跳转:push一个地址和运行ret。(在现实生活中不要这样做:CPU 已针对正确的调用 / ret 嵌套进行了优化,如果它们不匹配会导致返回地址预测器堆栈对未来的 RET 进行错误预测。)
中断导致 CPU 跳转到中断处理程序。软件中断(如 x86 的int 0x80指令,或 ARM svc/ swi)同步跳转到中断处理程序,因此它们实际上只是一个函数(并且模式切换到内核模式)。
所以这意味着int我们需要排除另一个跳转指令。但是我们也可以使用定时器中断来循环。 中断处理程序是循环的顶部。做任何你想做的工作,然后重新启用中断。之后执行 NOP;下一个定时器中断最终会触发。或者在一台在服务中断时启用中断的机器上:将您的循环体限制为始终适合定时器中断之间的工作量,即使是最坏情况的缓存行为。
软件产生中断的另一种方式是运行非法指令。 自修改代码可以通过有条件地生成非法指令来有条件地分支到非法指令中断处理程序。或者通过有条件地除以 0 有条件地分支到除法错误处理程序。由于您可以设置该中断处理程序的地址(例如,通过存储到x86 上的中断向量/描述符表中),您可以使用存储有条件地分支到任意地址和除以 0。(不过,您的所有分支目标都需要做任何必要的事情来重新启用该中断为可触发的)。
让程序计数器环绕:在平面内存冯诺依曼架构机器上,这需要自修改代码来跟踪程序状态,因为没有不作为代码执行的内存。
在分段 x86(例如 16 位实模式)中IP,将从 0xFFFF 回绕到 0x0000。所以你的循环是整个代码段,你在其他段有单独的数据存储。你只能CS用一个far jmp或far call/ far ret(或interrupt/interrupt-return)修改,所以你不能做任何类似pop cs跳转的事情 (因为pop cs不存在,不像其他pop-segment-register指令)。
有趣的事实:代码段末尾的最后一条指令不必以 0xFFFF 结束。例如,多字节指令可以从 0xFFFF 开始并在 0x04 结束。如果从不同的起点解码,则可以编写以不同方式执行的 x86 机器代码,但这在这里没有用。(这对您可以做什么施加了巨大的限制)。
ARM 使程序计数器可作为普通的通用寄存器访问,因此您可以使用sub r15, #1024或其他方式进行控制流,而不是技术上使用b(分支)或任何类型的调用(bl)或返回指令。但是,您仍然直接修改程序计数器,因此它实际上只是一条分支指令。(pc是 的另一个名称r15)。
假设你的意思是x86,
所有这些都允许在寄存器中获取决策结果(cmp*),可以通过移位和结果进一步操作它们。
这主要用于创建没有分支的函数返回值或执行某些形式的流处理(例如基于 SSE2 的图像二值化)。它并不是真正的分支的通用替代品。