我的最初目标是使用一字节长的指令,在小循环和快速计算跳转分派中实现。但现实与此完全不同-256远远不足以覆盖带符号和无符号的8、16、32和64位整数、浮点数和双精度浮点数、指针操作、不同寻址组合等。一种选择是不实现字节和短整型,但目标是要创建一个支持完整C子集以及向量操作的虚拟机,因为它们几乎无处不在,尽管实现方式不同。
所以我将指令切换为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位宽指令,但这可能会添加另一个跳转 - 跳转到处理该指令的位置,然后浪费时间跳转到特定寻址方案处理的情况或解码操作和更复杂的执行方法。而在第一种情况下,调度循环仍然增长,并添加另一个跳转。第二个选项 - 可以使用寄存器操作来解码寻址,但需要更复杂的指令以处理任何内容。我不确定这如何与较短的调度循环相比,再次不确定我的“较短和较长的调度循环”与在汇编指令方面被认为是短或长的相关性,它们需要的内存和执行速度。
我可以选择“许多指令”的解决方案 - 调度循环大几倍,但仍使用预先计算的直接跳转。复杂的寻址对于每个指令都是特定的并进行优化,并编译为本机代码,因此“仅寄存器”方法所需的额外内存操作将被编译并大多在寄存器上执行,这对性能有好处。通常,想法是增加指令集的内容,但也增加可以预先编译并在单个“指令”中完成的工作量。更长的指令集还意味着更长的调度循环,更长的跳转(虽然可以进行最小化优化),更少的缓存命中,但问题是BY HOW MUCH?考虑到每个“指令”只是几条汇编指令,大约7-8k指令的汇编片段是否被认为是正常的,或者太多?考虑到平均指令大小约为2-3b,这应该不超过20k的内存,足以完全适合大多数L1缓存。但这并非具体的计算,只是我在Google上找到的东西,所以也许我的“计算”有误?还是可能不起作用?我对缓存机制不是很有经验。
在我权衡各种论点后,采用“多指令”方法似乎有最大的性能优势,当然,前提是我的理论关于将“扩展分发循环”适配到L1缓存中是否成立。在这里,您的专业知识和经验就派上用场了。现在,由于语境已经缩小,并且提出了一些支持性想法,也许更容易给出一个更具体的答案,即一个更大的指令集是否比通过减少较慢的解释代码的数量来降低本机代码量所带来的好处。
我的指令大小数据基于这些统计数据。