题
在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 ++试图远离您的方向 - 因此,如果您想初始化的东西,就必须自己做。
其他提示
memmove
和 memcpy
不要那样工作,因为它不是移动或复制内存的有用语义。在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 ++代码(请参见下面的帖子),但我怀疑这实际上是嵌套循环发生的,甚至可能调用初始化功能。