5

在 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 array[size] = {0} 用于数组初始化

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

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

4

14 回答 14

12

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

如果您想填充内存,请使用 memset,它旨在满足您的需求。

于 2008-12-22T23:32:15.400 回答
12

有一种使用堆栈清空内存区域的更快方法。尽管 LDI 和 LDIR 的使用非常普遍,但 David Webb(他以各种方式推动 ZX Spectrum,例如包括边框在内的全屏数字倒计时)提出了这种快 4 倍的技术:

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

上面的解释取自对 David Webbs 游戏 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 个周期复制两个字节—— 11 个周期 forPUSH HL和 13 个 for DJNZ LOOP。要获得近 4 倍的速度,只需展开内部循环:

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

这几乎是每两个字节 11 个周期,比 LDIR 的每字节 21 个周期快大约 3.8 倍。

毫无疑问,这项技术已经被重新发明了很多次。例如,它出现在 1980 年用于 TRS-80 的 sub-Logic 的 Flight Simulator 1中。

于 2008-12-23T08:56:12.203 回答
8

我相信这与 C 和 C++ 的设计理念有关。正如Bjarne Stroustrup曾经说过的,C++ 设计的主要指导原则之一是“你不使用什么,你就不用付钱”。而丹尼斯·里奇可能没有用完全相同的话说出来,我相信这也是指导他设计 C(以及后来的人设计 C)的指导原则。现在您可能会认为,如果您分配内存,它应该自动初始化为 NULL,我倾向于同意您的看法。但这需要机器周期,如果您在每个周期都很关键的情况下进行编码,那么这可能不是一个可以接受的权衡。基本上,C 和 C++ 尽量不妨碍你——因此,如果你想要初始化某些东西,你必须自己做。

于 2008-12-22T23:35:21.710 回答
6

为什么 memmove 和 memcpy 会这样?

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

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

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

好吧,如果一切都失败了,你总是可以使用内联汇编。除此之外,我希望std::fill在一个好的 STL 实现中表现最好。是的,我完全意识到我的期望太高了,而且std::memset在实践中通常表现更好。

于 2008-12-22T22:54:49.857 回答
5

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

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

于 2008-12-23T04:02:56.677 回答
4

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

于 2008-12-22T23:34:08.577 回答
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 允许 source 和 dest 成为同一缓冲区的一部分)。memset用您选择的字节填充块。

于 2008-12-23T04:21:06.097 回答
2

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

于 2008-12-22T22:56:33.020 回答
2

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

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

但是,为什么编译器针对具有与 Z80 完全不同的指令集的 CPU 会期望使用 Z80 习语来处理这些类型的事情呢?

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

于 2008-12-23T08:27:31.370 回答
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')andstd::memset(s_, 'A', S)而不是 for 循环编译了代码,编译器产生了相同的输出。

于 2008-12-23T09:14:43.413 回答
2

如果你在 PowerPC 上,_dcbz()。

于 2009-01-14T18:13:44.887 回答
2

在许多情况下,拥有一个“memspread”函数会很有用,该函数的定义行为是在整个事物中复制内存范围的起始部分。尽管如果目标是传播单个字节值, memset() 做得很好,但有时可能希望用相同的值填充整数数组。在许多处理器实现中,一次将一个字节从源复制到目标将是一种非常糟糕的实现方式,但设计良好的函数可以产生良好的结果。比如先看数据量是不是小于32字节左右;如果是这样,只需按字节复制;否则检查源和目标对齐;如果它们对齐,则将大小向下舍入到最接近的单词(如有必要),然后将第一个单词复制到任何地方,

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

于 2011-04-19T22:59:28.767 回答
1

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

于 2008-12-23T03:26:01.980 回答
-1

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

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

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

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

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

于 2008-12-29T16:05:27.370 回答