为什么在C / C ++ / rtl中没有类似LDIR功能的Z80?

时间:2008-12-22 22:50:17

标签: c++ c z80

在Z80机器代码中,一种将缓冲区初始化为固定值的廉价技术,比如所有空白。所以一大堆代码可能看起来像这样。

LD HL, DESTINATION             ; point to the source
LD DE, DESTINATION + 1         ; point to the destination
LD BC, DESTINATION_SIZE - 1    ; copying this many bytes
LD (HL), 0X20                  ; put a seed space in the first position
LDIR                           ; move 1 to 2, 2 to 3...

结果是DESTINATION的内存块完全填空。 我已经尝试了memmove和memcpy,并且无法复制这种行为。我希望memmove能够正确地完成它。

为什么memmove和memcpy会以这种方式运行?

有没有合理的方法来进行这种数组初始化?

我已经知道数组初始化的char数组[size] = {0}

我已经知道memset可以完成单个字符的工作。

这个问题有哪些其他方法?

14 个答案:

答案 0 :(得分:12)

memmovememcpy不起作用,因为它不是用于移动或复制内存的有用语义。在Z80中能够填充内存很方便,但为什么你会期望一个名为“memmove”的函数用一个字节填充内存?这是为了移动内存块。无论块如何重叠,它都是为了得到正确的答案(源字节移动到目的地)而实现的。它有助于为移动内存块获得正确的答案。

如果你想填充内存,请使用memset,它专为你想要的而设计。

答案 1 :(得分:11)

使用堆栈可以更快地消隐内存区域。虽然LDI和LDIR的使用非常普遍,但David Webb(以各种方式推动ZX Spectrum,如包括边界在内的全屏幕倒计时)提出了这种技术,速度提高了4倍:

  • 然后保存堆栈指针 将其移动到屏幕的末尾。
  • 用HL加载HL寄存器对 零,
  • 进入一个巨大的循环 将HL推入堆栈。
  • 堆栈向上移动屏幕并向下移动 通过记忆和过程, 清除屏幕。

以上解释取自review of David Webbs game Starion

Z80例程可能看起来像这样:

  DI              ; disable interrupts which would write to the stack.
  LD HL, 0
  ADD HL, SP      ; save stack pointer
  EX DE, HL       ; in DE register
  LD HL, 0
  LD C, 0x18      ; Screen size in pages
  LD SP, 0x4000   ; End of screen
PAGE_LOOP:
  LD B, 128       ; inner loop iterates 128 times
LOOP:
  PUSH HL         ; effectively *--SP = 0; *--SP = 0;
  DJNZ LOOP       ; loop for 256 bytes
  DEC C
  JP NZ,PAGE_LOOP
  EX DE, HL
  LD SP, HL       ; restore stack pointer
  EI              ; re-enable interrupts

然而,这个例程的速度略快一点。 LDIR每21个周期复制一个字节。内循环每24个周期复制两个字节 - PUSH HL为11个周期,DJNZ LOOP为13个周期。要获得近4倍的速度,只需展开内循环:

LOOP:
   PUSH HL
   PUSH HL
   ...
   PUSH HL         ; repeat 128 times
   DEC C
   JP NZ,LOOP

每两个字节大约有11个周期,比LDIR每个字节的21个周期快大约3.8倍。

毫无疑问,这项技术已被多次重新发明。例如,它出现在1980年sub-Logic's Flight Simulator 1 for the TRS-80的早期。

答案 2 :(得分:8)

我相信这符合C和C ++的设计理念。正如Bjarne Stroustrup said一样,C ++设计的主要指导原则之一是“你不使用什么,你不付钱”。虽然Dennis Ritchie可能没有用完全相同的词来表达,但我认为这也是一个指导原则,也是他设计C(以及后续人员设计C)的指导原则。现在您可能会认为如果您分配内存,它应该自动初始化为NULL,我倾向于同意您的看法。但这需要机器周期,如果你在每个周期都很关键的情况下进行编码,这可能不是一个可接受的权衡。基本上C和C ++试图避开你的方式 - 因此如果你想要初始化的东西,你必须自己做。

答案 3 :(得分:6)

  

为什么memmove和memcpy会这样表现?

可能是因为没有针对Z80硬件的特定的现代C ++编译器?写一个。 ; - )

语言没有指定给定硬件如何实现任何东西。这完全取决于编译器和库的程序员。当然,为每个可以想象的硬件配置编写一个自己的,高度指定的版本是很多工作。那就是原因。

  

有没有合理的方法来进行这种数组初始化?有没有合理的方法来进行这种数组初始化?

好吧,如果一切都失败了,你总是可以使用内联汇编。除此之外,我希望std::fill在良好的STL实施中表现最佳。是的,我完全清楚我的期望太高了,std::memset在实践中往往表现得更好。

答案 4 :(得分:5)

你展示的Z80序列是最快的方式 - 1978年。那是30年前。从那时起,处理器已经取得了很大进展,而今天这只是最慢的方式。

Memmove设计为在源和目标范围重叠时工作,因此您可以将一块内存向上移动一个字节。这是C和C ++标准指定行为的一部分。 Memcpy未指定;它可能与memmove完全相同,或者可能有所不同,具体取决于编译器决定如何实现它。编译器可以自由选择比memmove更有效的方法。

答案 5 :(得分:4)

如果您正在摆弄硬件级别,那么一些CPU具有DMA控制器,可以非常快速地填充内存块(比CPU可能做得快得多)。我在飞思卡尔i.MX21 CPU上完成了这项工作。

答案 6 :(得分:3)

这很容易在x86程序集中完成。事实上,它归结为几乎完全相同的代码。

mov esi, source    ; set esi to be the source
lea edi, [esi + 1] ; set edi to be the source + 1
mov byte [esi], 0  ; initialize the first byte with the "seed"
mov ecx, 100h      ; set ecx to the size of the buffer
rep movsb          ; do the fill

但是,如果可以的话,一次设置多个字节会更有效。

最后,memcpy / memmove不是你想要的,它们是用于从一个区域到另一个区域复制内存块(memmove允许源和dest成为相同的缓冲区)。 memset用您选择的字节填充一个块。

答案 7 :(得分:2)

还有calloc在返回指针之前将内存分配并初始化为0。当然,calloc只会初始化为0,而不是用户指定的内容。

答案 8 :(得分:2)

如果这是在Z80上将内存块设置为给定值的最有效方法,那么很可能在您针对Z80s的编译器上描述memset()时实现。{/ p >

可能memcpy()也可能在该编译器上使用类似的序列。

但是为什么针对具有与Z80完全不同的指令集的CPU的编译器应该使用Z80成语用于这些类型的事情?

请记住,x86架构有一组类似的指令,可以使用REP操作码作为前缀,让它们重复执行,以执行复制,填充或比较内存块等操作。然而,当英特尔推出386(或者可能是486)时,CPU实际上会比循环中的简单指令更慢地运行这些指令。因此编译器经常停止使用面向REP的指令。

答案 9 :(得分:2)

说真的,如果您正在编写C / C ++,只需编写一个简单的for循环,让编译器为您烦恼。作为一个例子,这里是针对这个确切情况生成的一些代码VS2005(使用模板化大小):

template <int S>
class A
{
  char s_[S];
public:
  A()
  {
    for(int i = 0; i < S; ++i)
    {
      s_[i] = 'A';
    }
  }
  int MaxLength() const
  {
    return S;
  }
};

extern void useA(A<5> &a, int n); // fool the optimizer into generating any code at all

void test()
{
  A<5> a5;
  useA(a5, a5.MaxLength());
}

汇编程序输出如下:

test PROC

[snip]

; 25   :    A<5> a5;

mov eax, 41414141H              ;"AAAA"
mov DWORD PTR a5[esp+40], eax
mov BYTE PTR a5[esp+44], al

; 26   :    useA(a5, a5.MaxLength());

lea eax, DWORD PTR a5[esp+40]
push    5               ; MaxLength()
push    eax
call    useA

比这更有效率。在尝试寻找优化方法之前,请不要担心并信任您的编译器,或至少查看编译器生成的内容。为了比较,我还使用std::fill(s_, s_ + S, 'A')std::memset(s_, 'A', S)而不是for-loop编译代码,编译器生成相同的输出。

答案 10 :(得分:2)

如果你在PowerPC上,_dcbz()。

答案 11 :(得分:2)

在许多情况下,使用“memspread”函数会很有用,其定义的行为是在整个事物中复制内存范围的起始部分。虽然如果目标是扩展单个字节值,memset()会很好,但有时候例如可能想要填充具有相同值的整数数组。在许多处理器实现中,一次从源到目标复制一个字节将是一种非常糟糕的实现方式,但是设​​计良好的函数可以产生良好的结果。例如,首先查看数据量是否小于32个字节左右;如果是这样,只需按字节顺序复制;否则检查源和目标对齐方式;如果它们是对齐的,则将大小缩小到最接近的单词(如果需要),然后在任何地方复制第一个单词,在任何地方复制下一个单词等等。

我有时也希望有一个功能被指定为自下而上的memcpy,意图用于重叠范围。至于为什么没有标准的,我想没有人认为这很重要。

答案 12 :(得分:1)

memcpy()应该有这种行为。 memmove()没有设计,如果内存块重叠,它会从缓冲区末尾开始复制内容以避免这种行为。但是要填充具有特定值的缓冲区,您应该在C中使用memset()或在C ++中使用std::fill(),大多数现代编译器将优化到适当的块填充指令(例如x86架构上的REP STOSB)

答案 13 :(得分:-1)

如前所述,memset()提供了所需的功能。

memcpy()用于在源和目标缓冲区不重叠的所有情况下移动内存块,或者dest&lt;源。

memmove()解决了缓冲区重叠和dest&gt;的问题。源。

在x86架构上,优秀的编译器直接用内联汇编指令替换memset调用,非常有效地设置目标缓冲区的内存,甚至应用进一步的优化,例如使用4字节值尽可能长地填充(如果下面的代码不是完全的话)在语法上正确归咎于我长时间不使用X86汇编代码):

lea edi,dest
;copy the fill byte to all 4 bytes of eax
mov al,fill
mov ah,al
mov dx,ax
shl eax,16
mov ax,dx
mov ecx,count
mov edx,ecx
shr ecx,2
cld
rep stosd
test edx,2
jz moveByte
stosw
moveByte:
test edx,1
jz fillDone
stosb
fillDone:

实际上这个代码比Z80版本效率更高,因为它不会对内存执行内存,而只会注册到内存移动。你的Z80代码实际上是一个非常黑客,因为它依赖于每个复制操作填充了后续副本的来源。

如果编译器好一半,它可能能够检测到更复杂的C ++代码,可以分解为memset(参见下面的帖子),但我怀疑这实际上是嵌套循环,甚至可能调用初始化函数