Domanda

Nel codice macchina Z80, una tecnica conveniente per inizializzare un buffer a un valore fisso, dicono tutti gli spazi. Così un pezzo di codice potrebbe essere simile a questo.

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...

Il risultato è che il pezzo di memoria a destinazione è riempito completamente vuoto. Ho sperimentato con memmove e memcpy, e non può replicare questo comportamento. Mi aspettavo memmove per essere in grado di farlo in modo corretto.

Perché i memmove e memcpy comportarsi in questo modo?

Esiste un modo ragionevole per fare questo tipo di inizializzazione array?

Sono già a conoscenza di array di caratteri [size] = {0} per l'inizializzazione array

Sono già a conoscenza che memset farà il lavoro per i caratteri singoli.

Quello che altri approcci ci sono a questo problema?

È stato utile?

Soluzione

Credo che questo va alla filosofia progettuale di C e C ++. Come Bjarne Stroustrup una volta ha detto , uno dei più importanti principi guida del progetto di C ++ è 'quello che non si usa, non si paga per'. E mentre Dennis Ritchie non può aver detto esattamente le stesse parole, credo che era una guida principio di informare il suo disegno di C (e la progettazione di C da parte di persone successive) pure. Ora si potrebbe pensare che se si alloca la memoria che dovrebbe automaticamente essere inizializzato a NULL di e sarei tendenzialmente d'accordo con te. Ma questo richiede cicli macchina e se stai codifica in una situazione in cui ogni ciclo è fondamentale, che non può essere un accettabile compromesso. Fondamentalmente C e C ++ cercano di stare fuori dalla vostra strada - quindi se si desidera qualcosa inizializzata dovete farlo da soli.

Altri suggerimenti

memmove e memcpy non funzionano in questo modo perché non è un utile semantica per lo spostamento o la copia della memoria. E 'utile in Z80 di fare essere in grado di riempire la memoria, ma perché ci si aspetterebbe una funzione denominata "memmove" per riempire la memoria con un singolo byte? E 'per lo spostamento di blocchi di memoria in giro. E 'implementato per ottenere la risposta giusta (i byte di origine vengono spostati nella destinazione) a prescindere da come i blocchi si sovrappongono. E 'utile per esso per ottenere la risposta giusta per spostare i blocchi di memoria.

Se si vuole riempire la memoria, utilizzare memset, che è stato progettato per fare proprio quello che vuoi.

C'è stato un modo più veloce di tranciatura un'area di memoria utilizzando lo stack. Sebbene l'uso di LDI e LDIR era molto comune, David Webb (che ha spinto la ZX Spectrum in tutti i modi, come a schermo intero alla rovescia numero tra il confine) si avvicinò con questa tecnica che è 4 volte più veloce:

  • salva lo Stack Pointer e poi sposta alla fine della schermata.
  • carica la coppia registro HL con pari a zero,
  • va in un loop di massa Spingendo HL nello stack.
  • La Pila si muove su e giù lo schermo attraverso la memoria e nel processo, pulisce lo schermo.

La spiegazione di cui sopra è stata presa dal recensione di David Webb gioco Starion .

La routine Z80 potrebbe apparire un po 'come questo:

  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

Tuttavia, questa routine è un po 'meno di due volte più veloce. copie LDIR un byte ogni 21 cicli. Le copie ciclo interno due byte ogni 24 cicli - 11 cicli per PUSH HL e 13 per DJNZ LOOP. Per ottenere quasi 4 volte più veloce semplicemente srotolare il ciclo interno:

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

Questo è portata quasi 11 cicli ogni due bytes che è circa 3,8 volte più veloce di 21 cicli per byte di LDIR.

Indubbiamente la tecnica è stato reinventato molte volte. Ad esempio, è apparso in precedenza in del sub-Logic Flight Simulator 1 per il TRS-80 nel 1980.

  

Perché non memmove e memcpy comportarsi in questo modo?

Probabilmente perché non c'è specifica, moderno compilatore C ++ che ha come bersaglio l'hardware Z80? Scrivi una. ; -)

Le lingue non specificano come un dato di hardware implementa nulla. Ciò è del tutto fino ai programmatori del compilatore e le librerie. Naturalmente, scrivere una propria versione altamente specificato per ogni configurazione hardware che si possa immaginare è un sacco di lavoro. Che sarà il motivo.

  

C'è un modo ragionevole per fare questo tipo di inizializzazione array? Esiste un modo ragionevole per fare questo tipo di inizializzazione array?

Bene, se tutto il resto fallisce si può sempre utilizzare assembly inline. Oltre a questo, mi aspetto std::fill per eseguire meglio in una buona implementazione STL. E sì, io sono pienamente consapevole che le mie aspettative sono troppo alte e che std::memset esibisce spesso meglio in pratica.

La sequenza Z80 si mostra è stato il modo più veloce per farlo - nel 1978. Questo è stato 30 anni fa. I processori hanno progredito molto da allora, e oggi questo è solo circa il modo più lento per farlo.

memmove è progettato per funzionare quando i campi di origine e di destinazione si sovrappongono, in modo da poter spostare un pezzo di memoria da un byte. Questo fa parte del suo comportamento specificato dalla standard C ++ C e. Memcpy non è specificato; Potrebbe funzionare in modo identico a memmove, o potrebbe essere diverso, a seconda di come il compilatore decide per la sua attuazione. Il compilatore è libero di scegliere un metodo che è più efficiente di memmove.

Se stai giocherellare a livello hardware, quindi alcune CPU hanno controller DMA che possono riempire i blocchi di memoria estremamente rapidamente (molto più veloce rispetto alla CPU potrebbe mai fare). Ho fatto questo su una CPU Freescale i.MX21.

Questo essere realizzato in assembly x86 altrettanto facilmente. Infatti, si riduce a codice quasi identico al vostro esempio.

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

Tuttavia, è semplicemente più efficiente per impostare più di un byte alla volta, se possibile.

Infine, memcpy / memmove non sono quello che stai cercando, quelli sono per fare copie di blocchi di memoria da da un'area all'altra (memmove permette sorgente e dest di essere parte dello stesso tampone). memset riempie un blocco con un byte di tua scelta.

C'è anche calloc che alloca e inizializza la memoria a 0 prima di restituire il puntatore. Naturalmente, calloc inizializza solo a 0, non è qualcosa che l'utente specifica.

Se questo è il modo più efficace per impostare un blocco di memoria a un dato valore sul Z80, allora è possibile che memset() potrebbe essere implementato come si descrive su un compilatore che gli obiettivi Z80s.

Potrebbe essere che memcpy() potrebbe anche usare una sequenza simile a quella del compilatore.

Ma perché ci si aspetterebbe compilatori rivolte CPU completamente diversi set di istruzioni dal Z80 di usare un linguaggio Z80 per questi tipi di cose?

Ricordate che l'architettura x86 ha un simile insieme di istruzioni che potrebbero essere preceduti da un codice operativo REP che vengano eseguite più volte di fare cose come copia, riempire o confrontare blocchi di memoria. Tuttavia, per il momento Intel è uscito con il 386 (o forse era il 486) la CPU sarebbe in realtà eseguire tali istruzioni più lento di istruzioni più semplici in un ciclo. Così compilatori spesso smesso di usare le istruzioni REP-oriented.

Scherzi a parte, se si sta scrivendo C / C ++, basta scrivere un semplice ciclo for e lasciare che il compilatore fastidio per voi. A titolo di esempio, ecco qualche VS2005 codice generato per questo caso esatto (con dimensioni su modelli):

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());
}

L'output assembler è la seguente:

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

non ottenere qualsiasi più efficiente di quello. Smettila di preoccuparti e fiducia il vostro compilatore o almeno avere uno sguardo a ciò che il vostro compilatore produce prima di cercare di trovare modi per ottimizzare. Per confronto Ho anche compilato il codice utilizzando std::fill(s_, s_ + S, 'A') e std::memset(s_, 'A', S) invece del ciclo for e il compilatore prodotta l'uscita identici.

Se siete sul PowerPC, _dcbz ().

Ci sono una serie di situazioni in cui sarebbe utile avere una funzione di "memspread" il cui comportamento definito era quello di copiare la parte iniziale di un intervallo di memoria in tutta l'intera faccenda. Anche se memset () fa proprio bene se l'obiettivo è quello di diffondere un unico valore di byte, ci sono momenti in cui per esempio uno potrebbe voler compilare un array di interi con lo stesso valore. In molte implementazioni processore, copiando un byte alla volta dalla sorgente alla destinazione sarebbe un modo piuttosto scadente per la sua attuazione, ma una funzione ben progettato potrebbe dare buoni risultati. Ad esempio, iniziare vedere se la quantità di dati è meno di 32 byte o così; in tal caso, basta fare una copia byte per byte; altrimenti controllare l'allineamento di origine e di destinazione; se sono allineati, intorno alla dimensione verso il basso per la parola più vicina (se necessario), quindi copiare la prima parola ovunque va, copiare la parola successiva ovunque va, ecc.

troppo ho a volte desideravano una funzione specificato per lavorare come bottom-up memcpy, destinato per l'utilizzo con intervalli coincidenti. Per quanto riguarda il motivo per cui non c'è uno standard, immagino nessuno pensava importante.

memcpy() dovrebbe avere quel comportamento. memmove() non fa da disegno, se i blocchi di sovrapposizione memoria, copia i contenuti da estremità dei buffer per evitare questo tipo di comportamento. Ma per riempire un buffer con un valore specifico si dovrebbe utilizzare memset() in C o std::fill() in C ++, che la maggior parte dei compilatori moderni ottimizzeranno l'istruzione di blocco di riempimento appropriato (come REP STOSB su architetture x86).

Come detto prima, memset () offre la funzionalità desiderata.

memcpy () è per muoversi blocchi di memoria in tutti i casi in cui i buffer sorgente e destinazione non si sovrappongono, o dove dest

memmove () risolve il caso di buffer sovrapposti e dest> sorgente.

In architetture x86, buoni compilatori direttamente sostituiscono chiamate Memset con istruzioni per il montaggio in linea regolazione molto efficace memoria del buffer di destinazione, anche applicando ulteriori ottimizzazioni come l'utilizzo di valori di 4 byte ad occupare più a lungo possibile (se il codice non è totalmente sintatticamente corretto colpa sulla mia non utilizzando il codice assembly x86 per un lungo periodo):

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:

In realtà questo codice è molto più efficiente rispetto la versione Z80, in quanto non fa memoria alla memoria, ma registrati solo per memoria mosse. Il tuo codice Z80 è in realtà un bel trucco in quanto si basa su ogni operazione di copia dopo aver riempito la fonte della successiva copia.

Se il compilatore è a metà strada buona, potrebbe essere in grado di rilevare il codice più complicato C ++ che può essere suddiviso per memset (vedere il post qui sotto), ma dubito che questo accade in realtà per cicli annidati, probabilmente anche invocando funzioni di inizializzazione .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top