在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...

结果是,目的地的内存部分完全空白。我已经尝试了memmove和memcpy,无法复制这种行为。我希望Memmove能够正确执行此操作。

为什么Memmove和Memcpy以这种方式行事?

有什么合理的方法可以执行这种数组初始化吗?

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

我已经知道Memset将为单个字符完成工作。

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

有帮助吗?

解决方案

我相信这是C和C ++的设计理念。作为 Bjarne Stroustrup 一次 , ,C ++设计的主要指导原则之一是“您不使用的东西,不用付费”。然后 丹尼斯·里奇(Dennis Ritchie) 可能没有用完全相同的话说,我认为这是一个指导原则,可以告知他对C的设计(以及随后的人的设计)。现在,您可能会认为,如果您分配内存,则应自动将其初始化为Null的记忆,我倾向于同意您的看法。但这需要机器周期,如果您要在每个周期至关重要的情况下进行编码,那可能不是可以接受的权衡。基本上,C和C ++试图远离您的方向 - 因此,如果您想初始化的东西,就必须自己做。

其他提示

memmovememcpy 不要那样工作,因为它不是移动或复制内存的有用语义。在Z80中,这很方便,可以填充内存,但是为什么您期望一个名为“ memmove”的函数用单个字节填充内存呢?这是为了移动内存块。它已实现以获取正确的答案(源字节已移至目的地),而不管块如何重叠。为其为移动内存块找到正确的答案对于它很有用。

如果您想填充内存,请使用MEMSET,该MEMSET旨在完成您想要的工作。

有一种更快的方法可以使用堆栈清除内存区域。尽管使用LDI和LDIR非常普遍,但David Webb(以各种方式推动了ZX频谱,例如包括边界在内的全屏号码倒数)提出了这种技术,该技术快4倍:

  • 保存堆栈指针,然后将其移至屏幕末端。
  • 加载HL寄存器对的零,
  • 进入巨大的循环,将HL推到堆栈上。
  • 堆栈向上移动屏幕并通过内存向下移动,并在过程中清除屏幕。

上面的解释取自 大卫·韦伯斯游戏的评论.

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个周期复制两个字节 - 11个周期 PUSH HL 和13 DJNZ LOOP. 。要获得近4倍的快速,只需展开内部循环:

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

每两个字节近11个循环,比LDIR的21个周期快3.8倍。

毫无疑问,这项技术已经重新发明了很多次。例如,它出现在 TRS-80的子逻辑的飞行模拟器1 1980年。

为什么Memmove和Memcpy以这种方式行事?

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

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

有什么合理的方法可以执行这种数组初始化吗?有什么合理的方法可以进行此类初始化?

好吧,如果其他所有失败,您都可以始终使用内联装配。除此之外,我希望 std::fill 在良好的STL实现中表现最好。是的,我完全知道我的期望太高了, std::memset 通常在实践中表现更好。

您展示的Z80序列是最快的方法 - 1978年。那是30年前。从那时起,处理器已经取得了很多进展,今天,这几乎是最慢的方法。

MEMMOVE设计为当源和目的地范围重叠时工作,因此您可以通过一个字节移动大量内存。这是C和C ++标准的指定行为的一部分。 memcpy未指定;它可能与memmove相同,或者可能有所不同,具体取决于您的编译器如何决定实施它。编译器可以自由选择一种比Memmove更有效的方法。

如果您在硬件级别上摆弄了,那么一些CPU具有DMA控制器,可以极快地填充内存块(比CPU所能快得多)。我已经在Freescale I.MX21 CPU上完成了此操作。

这可以在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 用您选择的字节填充一个块。

还有 calloc 在返回指针之前,这将内存和初始化为0。当然,Calloc仅初始化为0,而不是用户指定的内容。

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

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

但是,为什么要针对CPU的编译器与Z80完全不同的指令集对这些类型的事物使用Z80成语?

请记住,X86体系结构具有类似的指令集,可以将其前缀带有REP OPODE,以使它们重复执行以执行诸如复制,填充或比较内存块之类的事情。但是,到英特尔出现386(或者也许是486)时,CPU实际上运行的指令比循环中的简单说明要慢。因此,编译器经常停止使用面向REP的说明。

认真地说,如果您正在编写C/C ++,只需写一个简单的循环,然后让编译器为您打扰。例如,这是针对此确切情况生成的一些代码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) 而不是循环和编译器产生相同的输出。

如果您在PowerPC上,则_dcbz()。

在许多情况下,拥有一个“ memspread”函数将有用,其定义行为是在整个过程中复制内存范围的起始部分。尽管MEMSET()如果目标是传播单个字节值,则确实可以,但是有时候,例如人们可能想填充具有相同值的整数数组。在许多处理器实现中,一次将字节从源复制到目的地将是实现它的一种非常谨慎的方法,但是精心设计的功能可以产生良好的结果。例如,首先查看数据量是否小于32个字节左右;如果是这样,只需进行字节副本即可;否则检查源和目的地对齐;如果它们对齐,则将大小圆形到最近的单词(如有必要),然后在任何地方复制第一个单词,请复制到处都有的下一个单词,等等。

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

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

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

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

memmove()解决了缓冲区重叠和dest>源的情况。

在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代码实际上是一个黑客攻击,因为它依赖于每个副本操作填充了后续副本的来源。

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top