C/C ++/RTLにLDIR機能のようなZ80がないのはなぜですか?
質問
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はこのように振る舞うのですか?
この種の配列初期化を行う合理的な方法はありますか?
Arrayの初期化については、Char array [size] = {0}をすでに知っています
Memsetがシングルキャラクターの仕事をすることをすでに知っています。
この問題には他にどんなアプローチがありますか?
解決
これは、CとC ++のデザイン哲学に当てはまると思います。として Bjarne Strooustrup 一度 言った, 、C ++の設計の主要な指針の原則の1つは、「あなたが使用しないもの、あなたが支払わないもの」です。そしてその間 デニス・リッチー まったく同じ言葉でそれを言っていないかもしれませんが、それは彼のCの設計(およびその後の人々によるCの設計)を知らせる指針であると思います。今、あなたはメモリを割り当てるならば、それは自動的にnullのものに初期化されるべきであり、私はあなたに同意する傾向があると思うかもしれません。しかし、それにはマシンサイクルが必要であり、すべてのサイクルが重要な状況でコーディングしている場合、それは許容可能なトレードオフではないかもしれません。基本的にCとC ++はあなたの邪魔をしないようにしてみてください - したがって、何かを初期化したい場合は、自分でやらなければなりません。
他のヒント
memmove
と memcpy
メモリの移動やコピーの有用なセマンティックではないため、そのように機能しないでください。 Z80ではメモリを埋めることができるのは便利ですが、なぜ「Memmove」という名前の関数が単一のバイトでメモリを埋めることを期待するのでしょうか?メモリのブロックを動かすためです。ブロックの重複に関係なく、正しい回答(ソースバイトが宛先に移動される)を取得するために実装されています。メモリブロックを移動するために正しい答えを得るのに役立ちます。
メモリを埋めたい場合は、Memsetを使用してください。これは、必要なことだけを行うように設計されています。
スタックを使用してメモリの領域をより迅速に叩く方法がありました。 LDIとLDIRの使用は非常に一般的でしたが、David Webb(ボーダーを含むフルスクリーン番号カウントダウンのようなあらゆる種類の方法でZXスペクトルをプッシュしました)は、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サイクルごとに1バイトをコピーします。インナーループは24サイクルごとに2バイトをコピーします - 11サイクル PUSH HL
そして13のため DJNZ LOOP
. 。 4倍近く速く獲得するには、単純に内側のループを展開します。
LOOP:
PUSH HL
PUSH HL
...
PUSH HL ; repeat 128 times
DEC C
JP NZ,LOOP
これは、2バイトごとに11サイクル近くで、LDIRのバイトあたり21サイクルよりも約3.8倍高速です。
間違いなく、この手法は何度も再発明されています。たとえば、以前に登場しました TRS-80のSub-Logicのフライトシミュレーター1 1980年。
なぜMemmoveとMemcpyはこのように振る舞うのですか?
おそらく、Z80ハードウェアを標的とする特定の最新のC ++コンパイラがないからでしょうか?書く。 ;-)
言語は、特定のハードウェアが何かを実装する方法を指定していません。これは、まったくコンパイラとライブラリのプログラマー次第です。もちろん、考えられるハードウェア構成ごとに独自の高度に指定されたバージョンを書くことは、多くの作業です。それが理由になります。
この種の配列初期化を行う合理的な方法はありますか?この種の配列初期化を行う合理的な方法はありますか?
まあ、他のすべてが失敗した場合、常にインラインアセンブリを使用できます。それ以外は、私は期待しています std::fill
優れたSTL実装で最高のパフォーマンスを発揮します。そして、はい、私は私の期待が高すぎて、それが std::memset
多くの場合、実際にはパフォーマンスが向上します。
あなたが示すZ80シーケンスは、1978年にそれを行うための最速の方法でした。それは30年前でした。それ以来、プロセッサは多くの進歩を遂げており、今日ではそれが最も遅い方法です。
Memmoveは、ソースと宛先の範囲が重複するときに動作するように設計されているため、メモリの塊を1バイトで動かすことができます。これは、CおよびC ++標準による指定された動作の一部です。 Memcpyは特定されていません。コンパイラがどのように実装することを決定するかによって、MemMoveと同じように機能するか、異なる場合があります。コンパイラは、MemMoveよりも効率的な方法を無料で選択できます。
ハードウェアレベルでいじっている場合、一部のCPUには、メモリのブロックを非常に迅速に埋めることができるDMAコントローラーがあります(CPUができるよりもはるかに速い)。私はこれをフリースケールの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()
Z80をターゲットにするコンパイラで説明するように実装される場合があります。
それはそうかもしれません memcpy()
また、そのコンパイラで同様のシーケンスを使用する場合があります。
しかし、なぜZ80のまったく異なる命令セットを持つCPUを標的とするコンパイラが、これらのタイプのものにZ80イディオムを使用することが期待されるのでしょうか?
X86アーキテクチャには、コピー、記入、またはメモリのブロックをコピー、入力、または比較するなどのことを繰り返し実行させるために、REPオプコードをプレフィックスすることができる同様の命令セットがあることを忘れないでください。ただし、Intelが386(または486だったかもしれません)で発表されるまでに、CPUは実際にループの単純な指示よりもゆっくりとそれらの命令を実行するでしょう。そのため、コンパイラはしばしば、担当者指向の指示の使用を停止しました。
真剣に、C/C ++を書いている場合は、単純なFor-Loopを書いて、コンパイラを気にしてください。例として、この正確なケース(テンプレートサイズを使用)用に生成されたコード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)
Loopとコンパイラの代わりに、同一の出力が生成されました。
PowerPCを使用している場合、_dcbz()。
定義された動作がメモリ範囲の開始部分を全体にコピーすることであった「MemSpread」関数を持つことが有用な状況がいくつかあります。 Memset()は、目標が単一のバイト値を広めることである場合に問題ありませんが、EGが同じ値で整数の配列を埋めたい場合があります。多くのプロセッサの実装では、ソースから宛先まで一度にバイトをコピーすることは、それを実装するためのかなり粗末な方法ですが、適切に設計された関数は良い結果をもたらす可能性があります。たとえば、データの量が32バイト程度であるかどうかを確認することから始めます。その場合は、Bytewiseコピーを実行してください。それ以外の場合は、ソースと宛先のアライメントを確認します。それらが揃っている場合は、サイズを最も近い単語(必要に応じて)まで下げて、どこにでも最初の単語をコピーし、次の単語をどこにでもコピーします。
私も時々、ボトムアップのMEMCPYとして機能するように指定された関数を望んでいました、 意図されました 重複する範囲で使用するため。標準的なものがない理由については、誰もそれが重要だとは思わないと思います。
memcpy()
その動作が必要です。 memmove()
メモリのブロックが重複している場合、その種の動作を避けるためにバッファーの端から始まる内容をコピーしても、設計によるものではありません。ただし、バッファを特定の値で埋めるには、使用する必要があります memset()
cまたは std::fill()
C ++では、ほとんどの最新のコンパイラが適切なブロック充填命令(X86アーキテクチャのREP STOSBなど)に最適化するものです。
前述のように、memset()は目的の機能を提供します。
memcpy()は、ソースと宛先バッファが重複しない、またはdest <sourceのすべての場合において、メモリのブロックを移動するためです。
memmove()は、バッファーが重複してdest>ソースのケースを解決します。
x86アーキテクチャでは、優れたコンパイラは、メムセットコールをインラインアセンブリの指示に直接置き換えます。宛先バッファーのメモリを非常に効果的に設定し、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 ++コードを検出できる可能性があります(以下の投稿を参照)が、これは実際にネストされたループで発生し、おそらく初期化関数を呼び出すこともできます。