Frage

Im Z80-Maschinencode eine kostengünstige Technik zum Initialisieren eines Puffers auf einen festen Wert, beispielsweise alle Leerzeichen.Ein Teil des Codes könnte also etwa so aussehen.

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

Das Ergebnis ist, dass der Speicherbereich bei DESTINATION vollständig leer ist.Ich habe mit memmove und memcpy experimentiert und kann dieses Verhalten nicht reproduzieren.Ich habe erwartet, dass memmove es richtig machen kann.

Warum verhalten sich memmove und memcpy so?

Gibt es eine vernünftige Möglichkeit, diese Art der Array-Initialisierung durchzuführen?

Mir ist char array[size] = {0} für die Array-Initialisierung bereits bekannt

Mir ist bereits bekannt, dass memset die Aufgabe für einzelne Zeichen übernehmen wird.

Welche anderen Ansätze gibt es zu diesem Problem?

War es hilfreich?

Lösung

Ich glaube, das hängt mit der Designphilosophie von C und C++ zusammen.Als Bjarne Stroustrup einmal sagte, Eines der wichtigsten Leitprinzipien des C++-Designs lautet: „Was Sie nicht verwenden, zahlen Sie nicht.“Und während Dennis Ritchie Vielleicht hat er es nicht genau mit den gleichen Worten gesagt, aber ich glaube, das war ein Leitprinzip, das auch seinen Entwurf von C (und den Entwurf von C durch spätere Leute) prägte.Nun denken Sie vielleicht, dass, wenn Sie Speicher zuweisen, dieser automatisch auf NULL initialisiert werden sollte, und ich würde Ihnen eher zustimmen.Dafür sind jedoch Maschinenzyklen erforderlich, und wenn Sie in einer Situation programmieren, in der jeder Zyklus kritisch ist, ist das möglicherweise kein akzeptabler Kompromiss.Grundsätzlich versuchen C und C++, Ihnen aus dem Weg zu gehen – wenn Sie also etwas initialisieren möchten, müssen Sie es selbst tun.

Andere Tipps

memmove Und memcpy Funktionieren Sie nicht auf diese Weise, da es sich nicht um eine nützliche Semantik zum Verschieben oder Kopieren von Speicher handelt.Beim Z80 ist es praktisch, den Speicher füllen zu können, aber warum sollte man von einer Funktion namens „memmove“ erwarten, dass sie den Speicher mit einem einzigen Byte füllt?Es dient zum Verschieben von Speicherblöcken.Es ist implementiert, um die richtige Antwort zu erhalten (die Quellbytes werden zum Ziel verschoben), unabhängig davon, wie sich die Blöcke überlappen.Es ist nützlich, um die richtige Antwort zum Verschieben von Speicherblöcken zu erhalten.

Wenn Sie den Speicher füllen möchten, verwenden Sie memset, das genau das tut, was Sie wollen.

Es gab eine schnellere Möglichkeit, einen Speicherbereich mithilfe des Stapels auszublenden.Obwohl die Verwendung von LDI und LDIR weit verbreitet war, entwickelte David Webb (der das ZX Spectrum auf vielfältige Weise vorangetrieben hat, wie z. B. Countdowns für Vollbildzahlen einschließlich des Randes) diese Technik, die viermal schneller ist:

  • Speichert den Stapelzeiger und bewegt ihn dann am Ende des Bildschirms.
  • Lädt das HL -Registerpaar mit Null,
  • Geht in eine massive Schleife, die HL auf den Stapel drückt.
  • Der Stapel bewegt den Bildschirm und runter durch den Speicher und löscht den Bildschirm.

Die obige Erklärung wurde dem entnommen Rezension des David Webbs-Spiels Starion.

Die Z80-Routine könnte in etwa so aussehen:

  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

Allerdings ist diese Routine etwas weniger als doppelt so schnell.LDIR kopiert alle 21 Zyklen ein Byte.Die innere Schleife kopiert alle 24 Zyklen zwei Bytes – also 11 Zyklen PUSH HL und 13 für DJNZ LOOP.Um fast viermal so schnell zu kommen, rollen Sie einfach die innere Schlaufe ab:

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

Das sind fast 11 Zyklen alle zwei Bytes, was etwa 3,8-mal schneller ist als die 21 Zyklen pro Byte von LDIR.

Zweifellos wurde die Technik viele Male neu erfunden.Es erschien zum Beispiel früher in sub-Logics Flight Simulator 1 für den TRS-80 im Jahr 1980.

Warum verhalten sich memmove und memcpy so?

Wahrscheinlich, weil es keinen speziellen, modernen C++-Compiler gibt, der auf die Z80-Hardware abzielt?Schreiben Sie eins.;-)

Die Sprachen geben nicht an, wie eine bestimmte Hardware etwas implementiert.Dies liegt ganz bei den Programmierern des Compilers und der Bibliotheken.Natürlich ist es eine Menge Arbeit, für jede erdenkliche Hardwarekonfiguration eine eigene, hochspezifizierte Version zu schreiben.Das wird der Grund sein.

Gibt es eine vernünftige Möglichkeit, diese Art der Array-Initialisierung durchzuführen? Gibt es eine vernünftige Möglichkeit, diese Art der Array-Initialisierung durchzuführen?

Wenn alles andere fehlschlägt, können Sie jederzeit die Inline-Assembly verwenden.Ansonsten erwarte ich std::fill um in einer guten STL-Implementierung die beste Leistung zu erzielen.Und ja, mir ist völlig bewusst, dass meine Erwartungen zu hoch sind und das std::memset schneidet in der Praxis oft besser ab.

Die von Ihnen gezeigte Z80-Sequenz war der schnellste Weg, dies zu erreichen – im Jahr 1978.Das war vor 30 Jahren.Seitdem haben sich die Prozessoren stark weiterentwickelt, und heute ist das fast der langsamste Weg, dies zu erreichen.

Memmove ist so konzipiert, dass es funktioniert, wenn sich Quell- und Zielbereich überschneiden, sodass Sie einen Speicherblock um ein Byte nach oben verschieben können.Das ist Teil des von den C- und C++-Standards spezifizierten Verhaltens.Memcpy ist nicht spezifiziert;Es funktioniert möglicherweise genauso wie memmove oder anders, je nachdem, wie Ihr Compiler sich für die Implementierung entscheidet.Dem Compiler steht es frei, eine Methode zu wählen, die effizienter ist als memmove.

Wenn Sie auf der Hardware-Ebene herumspielen, dann verfügen einige CPUs über DMA-Controller, die Speicherblöcke außerordentlich schnell füllen können (viel schneller, als es die CPU jemals könnte).Ich habe dies auf einer Freescale i.MX21-CPU gemacht.

Dies lässt sich im x86-Assembly genauso einfach bewerkstelligen.Tatsächlich läuft es auf einen nahezu identischen Code wie in Ihrem Beispiel hinaus.

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

Allerdings ist es einfach effizienter, wenn möglich mehr als ein Byte gleichzeitig festzulegen.

Endlich, memcpy/memmove sind nicht das, wonach Sie suchen, sie dienen dazu, Kopien von Speicherblöcken von einem Bereich in einen anderen zu erstellen (memmove ermöglicht, dass Quelle und Ziel Teil desselben Puffers sind). memset füllt einen Block mit einem Byte Ihrer Wahl.

Es gibt auch calloc Dadurch wird der Speicher reserviert und auf 0 initialisiert, bevor der Zeiger zurückgegeben wird.Calloc initialisiert natürlich nur auf 0, nicht auf etwas, das der Benutzer angibt.

Wenn dies der effizienteste Weg ist, einen Speicherblock auf dem Z80 auf einen bestimmten Wert zu setzen, dann ist das durchaus möglich memset() könnte, wie Sie es beschreiben, auf einem Compiler implementiert werden, der auf Z80s abzielt.

Das könnte es sein memcpy() könnte auch eine ähnliche Sequenz auf diesem Compiler verwenden.

Aber warum sollte man von Compilern, die auf CPUs mit völlig anderen Befehlssätzen als dem Z80 abzielen, erwarten, dass sie für solche Dinge ein Z80-Idiom verwenden?

Denken Sie daran, dass die x86-Architektur über einen ähnlichen Satz von Anweisungen verfügt, denen ein REP-Opcode vorangestellt werden kann, damit sie wiederholt ausgeführt werden, um beispielsweise Speicherblöcke zu kopieren, zu füllen oder zu vergleichen.Als Intel jedoch den 386 (oder vielleicht auch den 486) herausbrachte, führte die CPU diese Anweisungen tatsächlich langsamer aus als einfachere Anweisungen in einer Schleife.Daher haben Compiler häufig auf die Verwendung der REP-orientierten Anweisungen verzichtet.

Im Ernst, wenn Sie C/C++ schreiben, schreiben Sie einfach eine einfache for-Schleife und überlassen Sie die Arbeit dem Compiler.Als Beispiel ist hier ein Code, den VS2005 genau für diesen Fall generiert hat (unter Verwendung der Vorlagengröße):

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

Die Assembler-Ausgabe ist die folgende:

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

Es tut nicht noch effizienter werden.Machen Sie sich keine Sorgen mehr und vertrauen Sie Ihrem Compiler oder schauen Sie sich zumindest an, was Ihr Compiler produziert, bevor Sie versuchen, Optimierungsmöglichkeiten zu finden.Zum Vergleich habe ich den Code auch mit kompiliert std::fill(s_, s_ + S, 'A') Und std::memset(s_, 'A', S) anstelle der for-Schleife und der Compiler erzeugte die identische Ausgabe.

Wenn Sie auf dem PowerPC sind, _dcbz().

Es gibt eine Reihe von Situationen, in denen es nützlich wäre, eine „memspread“-Funktion zu haben, deren definiertes Verhalten darin besteht, den Anfangsteil eines Speicherbereichs über das Ganze hinweg zu kopieren.Obwohl memset() gut funktioniert, wenn das Ziel darin besteht, einen einzelnen Bytewert zu verteilen, gibt es Zeiten, in denen z.B.Vielleicht möchte man ein Array von Ganzzahlen mit demselben Wert füllen.Bei vielen Prozessorimplementierungen wäre das Kopieren jeweils eines Bytes von der Quelle zum Ziel eine ziemlich umständliche Art der Implementierung, aber eine gut gestaltete Funktion könnte gute Ergebnisse liefern.Überprüfen Sie beispielsweise zunächst, ob die Datenmenge weniger als etwa 32 Byte beträgt.Wenn ja, führen Sie einfach eine byteweise Kopie durch.Andernfalls überprüfen Sie die Ausrichtung von Quelle und Ziel.Wenn sie ausgerichtet sind, runden Sie die Größe auf das nächste Wort ab (falls erforderlich), kopieren Sie dann das erste Wort überall hin, kopieren Sie das nächste Wort überall dort, wo es hinkommt usw.

Auch ich habe mir manchmal eine Funktion gewünscht, die als Bottom-up-Memcpy funktionieren sollte. beabsichtigt zur Verwendung mit überlappenden Bereichen.Warum es keinen Standard gibt, hat wohl niemand für wichtig gehalten.

memcpy() sollte dieses Verhalten haben. memmove() Dies ist nicht beabsichtigt. Wenn sich die Speicherblöcke überlappen, kopiert es den Inhalt beginnend an den Enden der Puffer, um ein solches Verhalten zu vermeiden.Aber um einen Puffer mit einem bestimmten Wert zu füllen, sollten Sie ihn verwenden memset() in C oder std::fill() in C++, den die meisten modernen Compiler auf die entsprechende Blockfüllanweisung optimieren (z. B. REP STOSB auf x86-Architekturen).

Wie bereits erwähnt, bietet memset() die gewünschte Funktionalität.

memcpy() dient zum Verschieben von Speicherblöcken in allen Fällen, in denen sich Quell- und Zielpuffer nicht überlappen oder in denen dest < Quelle ist.

memmove() löst den Fall von Pufferüberlappungen und Ziel > Quelle.

Auf x86-Architekturen ersetzen gute Compiler Memset-Aufrufe direkt durch Inline-Assembly-Anweisungen, die den Speicher des Zielpuffers sehr effektiv festlegen und sogar weitere Optimierungen anwenden, wie z es liegt daran, dass ich seit langem keinen X86-Assemblercode mehr verwende):

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:

Tatsächlich ist dieser Code weitaus effizienter als Ihre Z80-Version, da er keine Speicher-zu-Speicher-Verschiebungen durchführt, sondern nur Register-zu-Speicher-Verschiebungen.Ihr Z80-Code ist in der Tat ein ziemlicher Hack, da er darauf angewiesen ist, dass bei jedem Kopiervorgang die Quelle der nachfolgenden Kopie gefüllt wird.

Wenn der Compiler halbwegs gut ist, kann er möglicherweise komplizierteren C++-Code erkennen, der in Memset zerlegt werden kann (siehe Beitrag unten), aber ich bezweifle, dass dies tatsächlich bei verschachtelten Schleifen geschieht, wahrscheinlich sogar beim Aufrufen von Initialisierungsfunktionen.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top