我正在开发一个简单的虚拟机,我正处于十字路口。
我最初的目标是使用字节长指令,因此是一个小循环和一个快速计算的 goto 调度。
然而,事实证明离它更远了——256 远不足以涵盖有符号和无符号的 8、16、32 和 64 位整数、浮点数和双精度数、指针操作以及不同的寻址组合。一种选择是不实现 byte 和 shorts,但目标是制作一个支持完整 C 子集以及向量操作的 VM,因为它们几乎无处不在,尽管在不同的实现中。
所以我切换到 16 位指令,所以现在我还能够添加便携式 SIMD 内在函数和更多编译的常用例程,这些例程通过不被解释真正节省了性能。还有全局地址的缓存,最初编译为基指针偏移量,第一次编译地址时,它只是覆盖偏移量和指令,以便下次直接跳转,代价是集合中的额外指令指令对全局的每次使用。
由于我没有处于分析阶段,我处于两难境地,额外的指令是否值得更多的灵活性,更多指令的存在以及因此没有来回复制指令是否会弥补增加的调度循环大小?请记住,这些说明只是一些组装说明,例如:
.globl __Z20assign_i8u_reg8_imm8v
.def __Z20assign_i8u_reg8_imm8v; .scl 2; .type 32; .endef
__Z20assign_i8u_reg8_imm8v:
LFB13:
.cfi_startproc
movl _ip, %eax
movb 3(%eax), %cl
movzbl 2(%eax), %eax
movl _sp, %edx
movb %cl, (%edx,%eax)
addl $4, _ip
ret
.cfi_endproc
LFE13:
.p2align 2,,3
.globl __Z18assign_i8u_reg_regv
.def __Z18assign_i8u_reg_regv; .scl 2; .type 32; .endef
__Z18assign_i8u_reg_regv:
LFB14:
.cfi_startproc
movl _ip, %edx
movl _sp, %eax
movzbl 3(%edx), %ecx
movb (%ecx,%eax), %cl
movzbl 2(%edx), %edx
movb %cl, (%eax,%edx)
addl $4, _ip
ret
.cfi_endproc
LFE14:
.p2align 2,,3
.globl __Z24assign_i8u_reg_globCachev
.def __Z24assign_i8u_reg_globCachev; .scl 2; .type 32; .endef
__Z24assign_i8u_reg_globCachev:
LFB15:
.cfi_startproc
movl _ip, %eax
movl _sp, %edx
movl 4(%eax), %ecx
addl %edx, %ecx
movl %ecx, 4(%eax)
movb (%ecx), %cl
movzwl 2(%eax), %eax
movb %cl, (%eax,%edx)
addl $8, _ip
ret
.cfi_endproc
LFE15:
.p2align 2,,3
.globl __Z19assign_i8u_reg_globv
.def __Z19assign_i8u_reg_globv; .scl 2; .type 32; .endef
__Z19assign_i8u_reg_globv:
LFB16:
.cfi_startproc
movl _ip, %eax
movl 4(%eax), %edx
movb (%edx), %cl
movzwl 2(%eax), %eax
movl _sp, %edx
movb %cl, (%edx,%eax)
addl $8, _ip
ret
.cfi_endproc
此示例包含以下说明:
- 将无符号字节从立即值分配给寄存器
- 将无符号字节从寄存器分配到寄存器
- 将全局偏移量中的无符号字节分配给寄存器,缓存并更改为直接指令
- 将全局偏移量中的无符号字节分配给寄存器(现在缓存的先前版本)
- ... 等等...
自然,当我为它生成编译器时,我将能够测试生产代码中的指令流,并优化内存中指令的排列,将常用的指令打包在一起,获得更多的缓存命中。
我只是很难确定这样的策略是否是一个好主意,膨胀会弥补灵活性,但性能呢?更多的编译例程会弥补更大的调度循环吗?是否值得缓存全局地址?
我还希望有人,在汇编中体面地表达对 GCC 生成的代码质量的意见 - 是否有任何明显的低效率和优化空间?把情况说清楚,有一个sp
指针,指向实现寄存器的栈(没有其他栈),ip
逻辑上是当前指令指针,gp
是全局指针(不被引用,作为偏移量访问)。
编辑:另外,这是我执行指令的基本格式:
INSTRUCTION assign_i8u_reg16_glob() { // assign unsigned byte to reg from global offset
FETCH(globallAddressCache);
REG(quint8, i.d16_1) = GLOB(quint8);
INC(globallAddressCache);
}
FETCH 返回对结构的引用,指令根据操作码使用该结构
REG 从偏移量返回对寄存器值 T 的引用
GLOB 从缓存的全局偏移量(实际上是绝对地址)返回对全局值的引用
INC 只是将指令指针增加指令的大小。
有些人可能会建议不要使用宏,但是使用模板时它的可读性要低得多。这样代码就很明显了。
编辑:我想对这个问题补充几点:
我可以选择“仅寄存器操作”解决方案,它只能在寄存器和“内存”之间移动数据——无论是全局还是堆。在这种情况下,每个“全局”和堆访问都必须复制值、修改或使用它,然后将其移回更新。这样,我有一个更短的调度循环,但每条处理非寄存器数据的指令都有一些额外的指令。因此,困境是多几倍的本地代码和更长的直接跳转,或者多几倍的解释指令和更短的调度循环。一个短的调度循环会给我足够的性能来弥补额外和昂贵的内存操作吗?也许较短和较长的调度循环之间的增量不足以产生真正的影响?就缓存命中而言,就装配跳跃的成本而言。
我可以进行额外的解码并且只有 8 位宽的指令,但是,这可能会增加另一个跳转 - 跳转到处理该指令的任何位置,然后在跳转到处理特定寻址方案的情况或解码操作以及更复杂的情况上浪费时间执行方法。在第一种情况下,dispatch 循环仍然在增长,并且增加了另一个跳转。第二个选项 - 寄存器操作可用于解码寻址,但需要更复杂的指令和更多未知的编译时间才能寻址任何内容。我不确定这将如何与更短的调度循环叠加,再次不确定我的“更短和更长的调度循环”如何与汇编指令、它们需要的内存和速度方面被认为是短或长的内容相关联他们的执行。
我可以选择“多指令”解决方案——调度循环要大几倍,但它仍然使用预先计算的直接跳转。复杂寻址是针对每条指令的特定和优化的,并编译为本机,因此“仅寄存器”方法所需的额外内存操作将被编译并主要在寄存器上执行,这有利于性能。通常,这个想法是向指令集添加更多内容,但也增加了可以预先编译并在单个“指令”中完成的工作量。单独的指令集也意味着更长的调度循环、更长的跳转(尽管可以优化以最小化)、更少的缓存命中,但问题是多少?考虑到每个“指令”只是一些汇编指令,大约 7-8k 指令的汇编片段被认为是正常的还是太多了?考虑到平均指令大小在 2-3b 左右变化,这不应该超过 20k 的内存,足以完全适应大多数 L1 缓存。但这不是具体的数学,只是我在谷歌上搜索的东西,所以也许我的“计算”有问题?或者也许它不是那样工作的?我在缓存机制方面没有那么丰富的经验。
对我来说,当我目前权衡这些论点时,“多指令”方法似乎最有可能获得最佳性能,当然,前提是我关于在 L1 缓存中拟合“扩展调度循环”的理论成立。因此,这就是您的专业知识和经验发挥作用的地方。既然上下文已经缩小并且提出了一些支持性想法,那么通过减少较慢的解释代码的数量,也许更容易给出一个更具体的答案.
我的指令大小数据基于这些统计数据。