为什么C到Z80编译器产生的代码很差?

2020-05-31 03:24:54

我的印象是,很难将C编译成Z80并最终得到优化良好的代码。是这样吗?为什么?

我对6502的了解更多。下面的一些例子说明了为什么C不适合6502:

C中的数组由整数类型索引。6502在索引数组方面相当快,但不幸的是,只有在索引是一个字节宽的情况下。因此,像strcmp或strlen这样的代码可能需要对每个字符执行16位加法。

堆栈是传递函数参数的理想数据结构。堆栈限制为256字节,与PDP-11相比,6502的堆栈寻址模式相当有限,因此CC65使用以软件实现的第二堆栈来传递参数IIRC。

据我对Z80的理解,这两个例子并不适用。Z80有这些索引寄存器和更大的堆栈。那么C不适合的原因是什么呢?

数组索引效率很低,因为没有可以使用可变偏移量的合理基址+偏移量寻址模式,这意味着编译器需要生成代码来进行地址计算。

同样,在堆栈上保存和恢复函数参数也很麻烦,因为没有堆栈指针+偏移量寻址模式。非叶函数的局部变量也是如此。

缺乏真正的通用寄存器。大多数寄存器都有特定的用途,这使得寄存器分配变得复杂。

Z80的大问题是没有索引寄存器。您必须先计算地址,然后执行xchg。任何与指针、取消引用或数组有关的操作都会出现此问题。-咖啡杯。

您的语句(C中的数组)按宽度与指针相同的类型进行索引。是错误的。它是大于零的整数常量,类型可以是任何整数类型-包括字节。-Gettofro。

A堆栈是传递函数参数的理想数据结构,语句具有误导性-堆栈是传递函数参数的理想数据结构,是的。但是C并不要求它的堆栈与CPU堆栈相同。巧合的是,大多数平台选择它,因为它很方便。你称之为软件堆栈(不管它是什么)--如果你认为软件堆栈是你必须在软件中推送的东西,那么CPU堆栈也是一样的。-Gettofro。

CppCon 2016:Jason Turner“微型计算机的丰富代码:用C++17编写的简单准将64游戏”演讲在这里可能会很有趣。-马利奥里。

还必须注意的是,您不能将现代编译器与较旧的编译器进行比较。较旧的编译器被设计为在Z80上运行,没有内存,CPU停滞不前,甚至更糟的是软盘驱动器。这就是他们所能做的就是产生糟糕的代码,更不用说好的代码了。相比之下,现代编译器有无限的RAM、无限的CPU和即时持久存储。我在雅达利800上使用了C编译器--有一次。多么悲惨的经历啊。-约翰·威尔·哈东(Will Hartung)。

如果您尝试将C语言转换为Z80,您会发现Z80索引寄存器和堆栈的行为与您预期的不太一样。那么,让我们先从。

您的编译器几乎需要对i使用16位值。因此,您在某个地方有&;c,甚至可能在您的索引寄存器中!,所以让我们使用ix=&;c。但是,索引寄存器的操作只允许常量偏移量,这也是单有符号字节。因此,您没有命令可以读取(寄存器中的IX+16位值)。因此,您最终会使用如下内容。

ld ix,c_addr;数组地址ld de,(I_Addr);计数器值add ix,deld a,0ld(ix+0),a;14+20+15+7+19=75T(每字节)

大多数编译器都会输出与我编写的代码非常接近的代码。实际上,有经验的Z80程序员知道-IX和IY对于大多数使用内存的操作是没有希望的-它们太慢了,太笨拙了。一个优秀的编译器编写者可能会让他/她的编译器执行如下操作。

ld hl,c_addr;数组地址ld de,(I_Addr);计数器值add hl,deld a,0ld(Hl),a;10+20+11+7+7=55T(每字节)。

不出一身汗就能快25%。然而,尽管我将i变量设为静态以使我和编译器的工作变得更容易,但这还远不是很好的Z80代码!

实际的完整循环将采用(7+6+13)*10-5=255/10~25.5t状态/字节。这并不是真正的优化代码,这是一种编写优化无关紧要的代码。可以进行部分展开,可以确保数组c不会跨越256个字节的边界,并将inchl替换为incl。最快的填充实际上是使用堆栈完成的。换句话说,Z80不适合C范例。

当然,可以用C编写类似的循环(使用指针而不是数组,使用倒计时循环而不是向上计数),这会增加将其转换成像样的Z80代码的机会。然而,这并不是您编写常规的C代码;这是您在试图将C转换成Z80时尝试解决C的限制。

当Raffzahn说人们不必为局部变量使用堆栈时,他是正确的。但是,如果您想要递归函数,就必须有某种类型的堆栈。所以让我们试着用PC的方式,通过堆栈。如何实现对如下内容的调用。

假设即使x当前值也在您的一个寄存器中,比如HL。所以,你会有一些像这样的东西。

我们如何实际恢复x的地址(和值)?它存储在SP+2中。但是,我们必须小心使用SP,因为我们想要返回到调用程序,所以我们可能会这样做

Addr_Inc:ld hl,2 add hl,sp ld e,(Hl)增量hl ld d,(Hl);10+11+7+6+7=41T。

因此,当人们抱怨Z80的C编译器时,并不意味着它不可能做到。这完全是另一回事。在任何一种编程中,都有模式,有好的,也有不好的。我的观点是,从Z80编码的角度来看,C所做的很多事情都是不好的模式。在Z80上,人们根本做不到C语言要求您非常流利的事情。

关于Z80的IDK,但是如果编译器对这样的i值使用16位,那么它就是垃圾编译器。大多数用于8位微控制器的现代编译器都知道,当您不使用I##**地址的phuclv时,应该针对这些情况进行优化。

您的编译器很大程度上需要使用16位值作为i。这根本不是真的。任何现代编译器都足够聪明,知道您示例中i的值都在0..9范围内,并且任何现代编译器都足够聪明,可以分配最适合保存这些值的任何寄存器,并将它们用作数组索引。唯一的问题是,是否存在具有如此智能的编译器,并且能够以Z80为目标。-Solomon Slow。

我觉得奇怪的是,我所知道的Z80或6502的C编译器都没有像PIC和8051编译器那样处理局部变量的选项--通过静态分配局部变量,以便可以同时使用的变量获得不同的地址,但是生命周期不重叠的变量可以被覆盖。逻辑并不难,它可以极大地提高为不需要支持递归或重入的函数生成代码的效率。-美国超级猫。

编译器已经知道如何将数组索引转换为指针增量,这样做是为了保存寄存器,并减少x86上的指令大小(索引需要额外的字节)。还有其他优势,比如不会破坏沙桥家族的微融合,或者可以将哈斯韦尔的port7agu用于商店。对于Trip-Count是编译时间常量的这种情况,完全有理由期望编译器生成一个像您的Inc hl/djnz循环一样的循环。否则就有点合理了。-彼得·科德斯(Peter Cordes)。

@phuclv大多数8位微控制器的现代编译器都知道,当您不使用I的地址时,应该针对这些情况进行优化--现代8位微控制器通常有32到128个通用寄存器。Z80有6个(ISH),其中的2个基本上必须保留用作几乎所有重要代码的指针。这为那些架构的编译器提供了更大的优化空间。--约翰·朱尔斯(Jules)。

CPU(非?)适宜性对C程序的主要缺点是缺乏在不使用ALU的情况下将多个寄存器组成一个地址的能力。

大多数现代CPU可以使用基址+索引+偏移寄存器寻址模式来寻址复杂的数据结构,如数组和结构-Z80需要煞费苦心地通过4位ALU将偏移量+索引添加到基址寄存器(如HL)-大多数现代CPU对各种寻址模式使用单独的地址计算实例。

另一个原因是缺乏真正的多用途寄存器-你不能简单地对Z80中的每个寄存器做所有事情-它的纯寄存器数量有点令人印象深刻,但使用备用寄存器集对于编译器来说可能太复杂了,因此编译器可能选择的寄存器是有限的。这对于寄存器更少的6502更有效。

还有一个缺点是:你不能为Z80-clang或GCC找到一个像样的现代C编译器,因为他们积极的优化器不会为这些旧的CPU费心,而且业余爱好者的产品就是没有那么复杂。即使你能做到,GCC和克朗也集中精力优化代码的局部性,没有缓存的中央处理器甚至不能从中受益,而是真正提高了现代中央处理器的性能。

我个人认为(即使不是最优的)编译器对旧CPU也不会毫无用处--程序中总是有很多东西做起来一点都不好玩,用汇编语言编写也很乏味(毕竟,我们仍然这样做的唯一原因是好玩,不是吗?)-所以我倾向于用C语言编写程序中枯燥的、与时间无关的部分,而另一部分就是有趣的部分。我喜欢用C语言编写程序的其他部分,也就是有趣的部分,而不是用C语言编写程序的另一个部分,那就是用C语言编写程序的另一部分,那就是枯燥乏味的部分,也就是用汇编器编写程序的另一个部分,也就是有趣的部分,这是为什么我们仍然会这样做的唯一原因,不是吗?两个世界都很完美。

我在我写的一款Z80(Spectrum And Other)游戏中就做到了这一点。游戏的核心是汇编语言,像排行榜和帮助逻辑这样的东西是用C语言编写的。

@LưUVĩnhPhúc你认为LDRLS x,[R1,R0,LSL#2]然后(ARM)是什么?

ARM并不是真正的RISC ISA。它有点RISCy,或者共享它们的一些功能,比如固定宽度的指令(Thumb2.除外),但是ISA的指令在1到16之间进行加载或存储,这取决于指令中位字段中的位,它不是RISC。(我说的是ARM的推送{R4,R5,R6,.,LR}又称机顶盒和相应的POP指令。加载/存储多个指令是微编码的,因为它们太复杂了,并且执行的工作量是可变的。-彼得·科德斯(Peter Cordes)。

@PeterCordes Prety当教条(RISC)与现实发生冲突时,@PeterCordes Prety是一个很好的例子-现实赢了…。除了OFC,和教条牧师在一起。-拉夫扎恩(Raffzahn)

通常,人们不知道如何使用编译器,或者不完全理解他们编写的代码的后果。在z80c编译器中正在进行优化,但是它不像GCC那样完整。我经常看到人们在编译时没有打开优化。

在Introspec的帖子中有一个例子,由于名誉点的原因,我不允许对此发表评论:

这段代码有很多他没有考虑到的问题。通过将i声明为char,他可能会让它签名(这是编译器的自由裁量权)。这意味着,在比较中,8位量在比较之前是符号扩展的,因为通常情况下,除非您在代码中正确指定,否则c编译器可能会在执行这些比较之前提升为int。通过使其成为全局的,他确保编译器不能将for循环索引保存在循环内的寄存器中。

z88dk中有两个C编译器。一个是sccz80,它是Ron Cain从70年代末开始的原始编译器的最高级版本;它现在主要是C90。该编译器不是优化编译器-它的目的是生成小代码。因此,您将看到许多编译器基元在子例程调用中执行。其背后的想法是,z88dk提供了一个完全用asm语言编写的重要c库,因此c编译器的目的是生成粘合代码,而执行时间则花在手写汇编器上。

另一个c编译器是sdcc的分支,称为zsdcc。这个已经改进了,并且产生了比SDCC本身更好、更小的代码。SDCC是一个优化编译器,但是它倾向于生成比sccz80更大的代码,并且过度使用z80的索引寄存器。z88dk中的版本zsdcc修复了许多此类问题,现在使用--opt-code-size开关时生成的代码大小与sccz80相当。

(-O3开关用于减少代码大小,但大多数情况下我更喜欢默认的-O2)。

._main ld hl,0;const a,l ld(_I),a jp i_4.i_2 ld hl,_i调用l_gchar Inc hl ld a,l ld(_I),a dec hl.i_4 ld hl,_i调用l_gchar ld de,10;const ex de,hl调用l_lt jp nc,i_3 ld hl,_data push hl ld hl,_i调用l_gchar popde add hl,de ld(Hl),#(0%256)ld l,(Hl)ld h,0 jp i_2.i_3 ret。

在这里,您可以看到子例程调用编译器原语,以及编译器被迫使用内存来保存for循环索引的事实。";l_lt";是带符号的比较。

_Main:ld hl,_i ld(Hl),0x00l_main_00102:ld hl,(_I)ld h,0x00 ld BC,_data add hl,BC xor a,a ld(Hl),a ld hl,_i ld a,(Hl)inca ld(Hl),a sub a,0x0a Jr C,l_main_00102 ret。

默认情况下,zsdcc中的字符是无符号的,并且注意到比较可以在8位内完成。C语言规则说两端都应该升级为int,但是如果编译器能够计算出可以用另一种等价的方式进行比较,那么不这样做是可以接受的。如果您没有指定您的字符是无签名的,此提升可能会导致插入符号扩展代码。

._main十二月sp pophl ld l,#(0%256)Push hl jp i_4.i_2 ld hl,0;const add hl,sp Inc(Hl).I_4 ld hl,0;const add hl,sp ld a,(Hl)cp#(10%256)jp nc,i_3 ld,_ld data hl,2-2;const add hl,sp ld l,(Hl)ld h,0 add hl,de ld(Hl),#(0%256%256)ld l,(Hl)ld h,0 jp i_2.i_3 Inc sp ret。

现在比较是8位的,并且没有使用子例程调用。但是,sccz80不能将索引i放入寄存器-它没有携带足够的信息来执行此操作,因此它将其设置为堆栈变量。

_Main:ld bc,_data+0 ld e,0x00l_Main_00103:ld a,e sub a,0x0a ret NC ld l,e ld h,0x00 add hl,BC ld(Hl),0x00 Inc e Jr l_Main_00103。

UNSIGNED CHAR DATA[10];void main(Void){for(UNSIGNED CHAR*p=data;p!=data+10;++p)*p=0;}。

_Main:ld bc,_DATAL_Main_00103:ld a,c sub a,+((_DATA+0x000a)&;0xFF)JR NZ,l_Main_00116 ld a,b sub a,+((_DATA+0x000a)/256)Jr Z,l_Main_00105l_Main_00116:XOR a,a ld(BC),a Inc BC Jr l_Main_00103l_Main_00105:rET。

指针保存在BC中,结束条件是16位比较,结果是主循环花费的时间大致相同。

一般而言,c编译器当前不能生成常见的z80CISC指令ldir、cpir、djnz等,但它们在某些情况下会生成,如上所述。他们也不能使用exx设置。但是,z88dk附带的大量c库确实充分利用了z80体系结构,因此任何使用库的人都将从ASM级别的性能中受益(SDCC自己的库是用c编写的,因此性能水平不同)。然而,初学c语言的程序员通常也不会使用这个库,因为他们不熟悉它,而且当他们不理解c如何映射到底层处理器时,他们还会犯性能错误。

c编译器不能做所有的事情,但是他们也不是无能为力的。要得到最好的代码,您必须了解您编写的那种c代码的后果,而不是简单地拼凑在一起。

这是一个很棒的答案;它比这里的大多数其他答案都要好,它提供了全面和准确的信息,并证明了问题的前提是部分有缺陷的,而不是仅仅断言它。这是一个伟大的第一次贡献。-Wizzwizz4♦。

回答得不错!我想在这里补充一句,我将变量设置为全局变量是为了确认z88dk网站上的建议:z88dk.org/wiki/doku.php?id=Optimization上的项目2我不是故意使用Memset的,因为您编写的每个小循环都没有现成的Memset,所以我关心的是编译器在小循环上的泛型行为。我不是故意使用Memset的,因为你编写的每个小循环都没有现成的Memset,所以我关心的是编译器在小循环上的泛型行为。-自我介绍。

通过使其成为全局的,他确保编译器不能将for循环索引保存在循环内的寄存器中。同样,这纯粹是编译器不知道如何很好地优化的限制。它不是易失性的,编译器可以证明存储到data[]中的数据不是它的别名(因为它也是全局数组,而不是指针,并且编译器知道两个全局变量不会相互重叠)。因此,允许编译器将存储沉没到循环外的计数器,并在循环后执行一个10的存储。";As-if&34;规则允许在编译时对加载/存储进行重新排序。-彼得·科德斯(Peter Cordes)。

但是很明显,这是一种非常糟糕的编写代码的方式,这会使编译器的工作变得困难。真正的Z80编译器不能进行这种优化,或者将简单的数组索引转换为指针增量,这令人失望(但考虑到他们的年龄,这并不太令人惊讶)。GCC可以将循环转换为内存集调用和/或内联已知良好的内存集代码:P-Peter Cordes。

@aralbrec然而,如果你仔细想想,你会对编译器有更好的了解,官方网站上没有记录的很多技巧,你能做的最多的仍然是每字节超过60个t状态。如果这还不能说明我的观点,我不知道还有什么能说明问题。-自我介绍。

这个问题的简单答案是Z80吸盘和C吸盘--这取决于你站在哪一边。虽然它们当然是不真实的(*1),但也存在真正的问题。双方的一个主要论点是

C的核心与PDP-11(ISH)CPU架构捆绑在一起,而Z80则不是。

Z80是一款相当特殊的CPU,专注于最大能力,而不是美观。

所有这些点都是联系在一起的。与前面提到的问题一样,C暗示了一个简单且相当对称的指针模型,该模型源于PDP-11提供的内容。这包括直接转换到内存地址,从而允许跳过创建更复杂的数据模型和使用指针来实现原本由某些语言运行时处理的功能。

现在,Z80(就像它的前身8080一样)完全能够执行所需的所有操作。然而,由于其单个内存指针的(继承的)结构,它确实需要用几条机器指令替换单个(基于PDP-11的)C操作。到目前为止,这还不是一个真正的问题。除非,当汇编程序员查看结果时,他立即看到了Z80改进结果的具体方法-比如保持两个指针,并在需要时交换HL/DE。对于一个C编译器来说,这是很难理解的,因为它是基于语义的-为什么要做某事的知识-而不仅仅是被告知它是如何做的。

但这是所有高级语言的问题。它们最好编译成具有一组相等资源的简单对称CPU模型,准确地提供抽象层所需的操作。语言的抽象性越高,底层CPU级别就可以执行得越好。这就是为什么加州大学圣迭戈分校的P-Code系统在许多平台上都表现得如此出色的原因。提供它的虚拟CPU正是编译器想要的。尽管核心是解释器,但在许多机器上,性能可以与本机代码生成相媲美。

..