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

スタックを使用してメモリ領域を消去する、より迅速な方法がありました。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

ただし、そのルーチンは 2 倍弱の速さです。LDIR は 21 サイクルごとに 1 バイトをコピーします。内側のループは、24 サイクルごとに 2 バイトをコピーPUSH HLDJNZ LOOPます。内側のループを展開するだけで、ほぼ 4 倍の速さになります。

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

これは、2 バイトごとにほぼ 11 サイクルであり、LDIR の 1 バイトあたり 21 サイクルよりも約 3.8 倍高速です。

間違いなく、この技術は何度も再発明されてきました。たとえば、1980 年に TRS-80 用の sub-Logic の Flight Simulator 1 に登場しました。

于 2008-12-23T08:56:12.203 に答える
12

memmovememcpyメモリを移動またはコピーするための有用なセマンティックではないため、そのように動作しないでください。Z80 ではメモリをいっぱいにできるのは便利ですが、なぜ「memmove」という名前の関数がメモリを 1 バイトでいっぱいにすることを期待するのでしょうか? これは、メモリのブロックを移動するためのものです。ブロックがどのようにオーバーラップするかに関係なく、正しい答えを取得する (ソース バイトが宛先に移動される) ように実装されています。メモリブロックを移動するための正しい答えを得るのに役立ちます。

メモリをいっぱいにしたい場合は、必要なことだけを行うように設計された memset を使用してください。

于 2008-12-22T23:32:15.400 に答える
8

これは、C および C++ の設計哲学に当てはまると思います。Bjarne Stroustrupがかつて言ったように、C++ の設計の主要な指針の 1 つは、「使用しないものにはお金を払わない」ということです。そしてデニス・リッチーがまったく同じ言葉で言ったわけではないかもしれませんが、それは彼の C の設計 (およびその後の人々による C の設計) にも影響を与える指針だったと思います。メモリを割り当てると、自動的に NULL に初期化されるはずだと思うかもしれませんが、私はあなたに同意する傾向があります。しかし、それにはマシン サイクルが必要であり、すべてのサイクルが重要な状況でコーディングしている場合、それは許容できるトレードオフではない可能性があります。基本的に、C と C++ は邪魔にならないようにしようとします。したがって、何かを初期化したい場合は、自分で行う必要があります。

于 2008-12-22T23:35:21.710 に答える
6

memmoveとmemcpyがこのように動作するのはなぜですか?

おそらく、Z80ハードウェアを対象とする特定の最新のC ++コンパイラがないためですか?1つ書いてください。;-)

言語は、特定のハードウェアがどのように何かを実装するかを指定していません。これは完全にコンパイラとライブラリのプログラマー次第です。もちろん、考えられるすべてのハードウェア構成に対して独自の高度に指定されたバージョンを作成するのは大変な作業です。それが理由になります。

この種の配列の初期化を行うための合理的な方法はありますか?この種の配列の初期化を行うための合理的な方法はありますか?

さて、他のすべてが失敗した場合は、常にインラインアセンブリを使用できます。それ以外はstd::fill、優れたSTL実装で最高のパフォーマンスを発揮することを期待しています。そして、はい、私は私の期待が高すぎることを十分に認識しており、それstd::memsetは実際にはしばしばより良いパフォーマンスを発揮します。

于 2008-12-22T22:54:49.857 に答える
5

あなたが示すZ80シーケンスは、それを行うための最速の方法でした-1978年。それは30年前のことです。それ以来、プロセッサは大きく進歩してきましたが、今日ではそれが最も遅い方法です。

Memmoveは、送信元と宛先の範囲が重複する場合に機能するように設計されているため、メモリのチャンクを1バイト上に移動できます。これは、CおよびC++標準で指定されている動作の一部です。Memcpyは指定されていません。memmoveと同じように機能する場合もあれば、コンパイラが実装を決定する方法に応じて異なる場合もあります。コンパイラーは、memmoveよりも効率的な方法を自由に選択できます。

于 2008-12-23T04:02:56.677 に答える
4

ハードウェア レベルをいじっている場合、一部の CPU には DMA コントローラーがあり、メモリ ブロックを非常に高速に (CPU よりもはるかに高速に) 満たすことができます。Freescale 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

ポインタを返す前にメモリを0に割り当てて初期化するcallocもあります。もちろん、callocは0に初期化されるだけで、ユーザーが指定するものではありません。

于 2008-12-22T22:56:33.020 に答える
2

PowerPCを使用している場合は、_dcbz()。

于 2009-01-14T18:13:44.887 に答える
2

これがZ80でメモリブロックを特定の値に設定する最も効率的な方法である場合、Z80memset()をターゲットとするコンパイラで説明したように実装される可能性は十分にあります。

memcpy()そのコンパイラでも同様のシーケンスを使用する可能性があります。

しかし、Z80 とはまったく異なる命令セットを持つ CPU をターゲットとするコンパイラが、これらの種類のものに Z80 イディオムを使用することが期待されるのはなぜでしょうか?

x86 アーキテクチャには、REP オペコードを前に付けて繰り返し実行し、メモリ ブロックのコピー、フィル、比較などを行うことができる同様の一連の命令があることに注意してください。ただし、Intel が 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

それ以上の効率は得られません。心配するのをやめてコンパイラを信頼するか、少なくとも最適化の方法を見つける前にコンパイラが生成するものを確認してください。比較のために、for ループの代わりにstd::fill(s_, s_ + S, 'A')andを使用してコードをコンパイルしたところ、コンパイラは同じ出力を生成しました。std::memset(s_, 'A', S)

于 2008-12-23T09:14:43.413 に答える
2

全体を通してメモリ範囲の開始部分をコピーするという動作が定義されている「memspread」関数があると便利な状況がいくつかあります。memset() は、目的が 1 バイトの値を分散することである場合には問題ありませんが、整数の配列を同じ値で埋めたい場合などがあります。多くのプロセッサの実装では、コピー元からコピー先に一度に 1 バイトずつコピーするのはかなり厄介な方法ですが、適切に設計された関数は良い結果をもたらす可能性があります。たとえば、データ量が 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 コードは、各コピー操作が後続のコピーのソースを埋めることに依存しているため、実際にはかなりのハックです。

コンパイラが中途半端に良ければ、memset に分解できるより複雑な C++ コードを検出できるかもしれません (以下の記事を参照)。

于 2008-12-29T16:05:27.370 に答える