在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可以完成单个字符的工作。
这个问题有哪些其他方法?
答案 0 :(得分:12)
memmove
和memcpy
不起作用,因为它不是用于移动或复制内存的有用语义。在Z80中能够填充内存很方便,但为什么你会期望一个名为“memmove”的函数用一个字节填充内存?这是为了移动内存块。无论块如何重叠,它都是为了得到正确的答案(源字节移动到目的地)而实现的。它有助于为移动内存块获得正确的答案。
如果你想填充内存,请使用memset,它专为你想要的而设计。
答案 1 :(得分:11)
使用堆栈可以更快地消隐内存区域。虽然LDI和LDIR的使用非常普遍,但David Webb(以各种方式推动ZX Spectrum,如包括边界在内的全屏幕倒计时)提出了这种技术,速度提高了4倍:
以上解释取自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(参见下面的帖子),但我怀疑这实际上是嵌套循环,甚至可能调用初始化函数