Pourquoi n'y a-t-il pas de fonctionnalité Z80 comme LDIR en C / C ++ / RTL?

StackOverflow https://stackoverflow.com/questions/387654

  •  23-08-2019
  •  | 
  •  

Question

Dans le code machine Z80, une technique bon marché pour initialiser un tampon à une valeur fixe, disons tous les blancs. Ainsi, un morceau de code peut ressembler à ceci.

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

Le résultat est que le morceau de mémoire à destination est complètement rempli. J'ai expérimenté Memmove, et Memcpy, et je ne peux pas reproduire ce comportement. Je m'attendais à ce que Memmove puisse le faire correctement.

Pourquoi Memmove et Memcpy se comportent-ils de cette façon?

Existe-t-il un moyen raisonnable de faire ce type d'initialisation du tableau?

Je suis déjà conscient de Char Array [Size] = {0} pour l'initialisation du tableau

Je suis déjà conscient que Memset fera le travail pour des personnages uniques.

Quelles autres approches y a-t-il à ce problème?

Était-ce utile?

La solution

Je crois que cela va à la philosophie de conception de C et C ++. Comme Bjarne Stroustrup une fois que a dit, l'un des principaux principes directeurs de la conception de C ++ est "ce que vous n'utilisez pas, vous ne payez pas". Et tandis que Dennis Ritchie Peut-être ne l'a peut-être pas dit exactement dans ces mêmes mots, je crois que c'était un principe directeur informant sa conception de C (et la conception de C par les personnes suivantes). Vous pouvez maintenant penser que si vous allouez de la mémoire, il devrait être automatiquement initialisé à Null et aurais tendance à être d'accord avec vous. Mais cela prend des cycles de machine et si vous codiez dans une situation où chaque cycle est critique, ce n'est peut-être pas un compromis acceptable. Fondamentalement, C et C ++ essaient de rester hors de votre chemin - donc si vous voulez quelque chose d'initialisé, vous devez le faire vous-même.

Autres conseils

memmove et memcpy Ne fonctionne pas de cette façon car ce n'est pas un sémantique utile pour déplacer ou copier la mémoire. Il est pratique dans le Z80 pour pouvoir remplir la mémoire, mais pourquoi vous attendez-vous à une fonction nommée "Memmove" pour remplir la mémoire avec un seul octet? C'est pour déplacer des blocs de mémoire autour. Il est implémenté pour obtenir la bonne réponse (les octets de source sont déplacés vers la destination), quelle que soit la façon dont les blocs se chevauchent. Il est utile pour qu'il obtienne la bonne réponse pour déplacer des blocs de mémoire.

Si vous souhaitez remplir la mémoire, utilisez MEMSET, qui est conçu pour faire exactement ce que vous voulez.

Il y avait un moyen plus rapide de bloquer une zone de mémoire en utilisant la pile. Bien que l'utilisation de LDI et LDIR ait été très courante, David Webb (qui a poussé le spectre ZX de toutes sortes de façons comme le compte à rebours du numéro plein écran, y compris la frontière) est venu avec cette technique qui est 4 fois plus rapide:

  • Enregistre le pointeur de pile, puis le déplace vers la fin de l'écran.
  • Charge la paire de registres HL avec zéro,
  • va dans une boucle massive poussant HL sur la pile.
  • La pile remonte l'écran et vers le bas à travers la mémoire et dans le processus, efface l'écran.

L'explication ci-dessus a été tirée du Revue du jeu David Webbs Strion.

La routine Z80 peut ressembler un peu à ceci:

  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

Cependant, cette routine est un peu moins deux fois plus vite. LDIR copie un octet tous les 21 cycles. La boucle intérieure copie deux octets tous les 24 cycles - 11 cycles pour PUSH HL et 13 pour DJNZ LOOP. Pour obtenir près de 4 fois plus rapidement, il suffit de dérouter la boucle intérieure:

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

Cela représente presque 11 cycles tous les deux octets, ce qui est environ 3,8 fois plus rapide que les 21 cycles par octet de LDIR.

Sans aucun doute, la technique a été réinventée à plusieurs reprises. Par exemple, il est apparu plus tôt dans Simulator de vol de Sub-Logic 1 pour le TRS-80 en 1980.

Pourquoi Memmove et Memcpy se comportent-ils de cette façon?

Probablement parce qu'il n'y a pas de compilateur C ++ moderne spécifique qui cible le matériel Z80? Écrivez un. ;-)

Les langues ne spécifient pas comment un matériel donné implémente quoi que ce soit. Ceci est entièrement à la hauteur des programmeurs du compilateur et des bibliothèques. Bien sûr, écrire une propre version hautement spécifiée pour chaque configuration matérielle imaginable est beaucoup de travail. Ce sera la raison.

Existe-t-il un moyen raisonnable de faire ce type d'initialisation du tableau? Existe-t-il un moyen raisonnable de faire ce type d'initialisation du tableau?

Eh bien, si tout le reste échoue, vous pouvez toujours utiliser l'assemblage en ligne. À part ça, je m'attends std::fill pour exécuter le mieux dans une bonne implémentation STL. Et oui, je suis pleinement conscient que mes attentes sont trop élevées et que std::memset fonctionne souvent mieux dans la pratique.

La séquence Z80 que vous montrez était le moyen le plus rapide de le faire - en 1978. C'était il y a 30 ans. Les processeurs ont beaucoup progressé depuis lors, et aujourd'hui, c'est à peu près le moyen le plus lent de le faire.

Memmove est conçue pour fonctionner lorsque les plages de source et de destination se chevauchent, vous pouvez donc déplacer un morceau de mémoire par un octet. Cela fait partie de son comportement spécifié selon les normes C et C ++. MEMCPY n'est pas spécifié; Cela pourrait fonctionner de manière identique à Memmove, ou cela peut être différent, selon la façon dont votre compilateur décide de l'implémenter. Le compilateur est libre de choisir une méthode plus efficace que Memmove.

Si vous jouez au niveau matériel, certains CPU ont des contrôleurs DMA qui peuvent remplir les blocs de mémoire extrêmement rapidement (beaucoup plus rapidement que le processeur ne pourrait le faire). Je l'ai fait sur un processeur I.MX21 freescale.

Cela est accompli en assemblage x86 tout aussi facilement. En fait, cela se résume à un code presque identique à votre exemple.

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

Cependant, il est tout simplement plus efficace de définir plus d'un octet à la fois si vous le pouvez.

Pour terminer, memcpy/memmove ne sont pas ce que vous recherchez, ce sont des copies de blocs de mémoire de la zone à une autre (Memmove permet à la source et au dest de faire partie du même tampon). memset remplit un bloc avec un octet de votre choix.

Il y a aussi calloc Cela alloue et initialise la mémoire à 0 avant de retourner le pointeur. Bien sûr, Calloc initialise uniquement à 0, pas quelque chose que l'utilisateur spécifie.

Si c'est le moyen le plus efficace de définir un bloc de mémoire sur une valeur donnée sur le Z80, alors il est tout à fait possible que memset() Peut être implémenté comme vous le décrivez sur un compilateur qui cible Z80.

C'est peut-être que memcpy() pourrait également utiliser une séquence similaire sur ce compilateur.

Mais pourquoi les compilateurs ciblant les CPU avec des ensembles d'instructions complètement différents du Z80 devraient-ils utiliser un idiome Z80 pour ces types de choses?

N'oubliez pas que l'architecture X86 a un ensemble similaire d'instructions qui pourraient être préfixées avec un Rep Opcode pour les faire exécuter à plusieurs reprises pour faire des choses comme la copie, remplir ou comparer des blocs de mémoire. Cependant, au moment où Intel sortit avec le 386 (ou peut-être que c'était le 486), le CPU exécuterait en fait ces instructions plus lentement que les instructions plus simples dans une boucle. Les compilateurs ont donc souvent cessé d'utiliser les instructions répétitées.

Sérieusement, si vous écrivez C / C ++, écrivez simplement une boucle simple et laissez le compilateur déranger pour vous. À titre d'exemple, voici un code VS2005 généré pour ce cas exact (en utilisant la taille des modèles):

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

La sortie de l'assembleur est la suivante:

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

Cela fait ne pas soyez plus efficace que cela. Arrêtez de vous inquiéter et faites confiance à votre compilateur ou au moins jetez un œil à ce que votre compilateur produit avant d'essayer de trouver des moyens d'optimiser. À titre de comparaison, j'ai également compilé le code en utilisant std::fill(s_, s_ + S, 'A') et std::memset(s_, 'A', S) Au lieu de la boucle et du compilateur ont produit la sortie identique.

Si vous êtes sur le PowerPC, _dcbz ().

Il existe un certain nombre de situations où il serait utile d'avoir une fonction "memspread" dont le comportement défini était de copier la partie de départ d'une plage de mémoire tout au long du tout. Bien que memset () soit très bien si l'objectif est d'écarter une valeur d'octet unique, il y a des moments où l'on peut vouloir remplir un tableau d'entiers avec la même valeur. Sur de nombreuses implémentations de processeurs, la copie d'un octet à la fois de la source à la destination serait un moyen assez minable de l'implémenter, mais une fonction bien conçue pourrait donner de bons résultats. Par exemple, commencez par voir si la quantité de données est inférieure à 32 octets environ; Si c'est le cas, faites simplement une copie bytewise; Sinon, vérifiez l'alignement source et de destination; S'ils sont alignés, arrondissez la taille vers le mot le plus proche (si nécessaire), puis copiez le premier mot partout, copiez le mot suivant partout, etc.

Moi aussi, je souhaite parfois une fonction qui a été spécifiée pour fonctionner comme un MemCpy ascendante, prévu pour une utilisation avec des plages de chevauchement. Quant à savoir pourquoi il n'y a pas de standard, je suppose que personne ne le pensait important.

memcpy() devrait avoir ce comportement. memmove() ne fait pas par conception, si les blocs de mémoire se chevauchent, il copie le contenu commençant aux extrémités des tampons pour éviter ce type de comportement. Mais pour remplir un tampon d'une valeur spécifique, vous devriez utiliser memset() en c ou std::fill() En C ++, que la plupart des compilateurs modernes optimiseront à l'instruction de remplissage de bloc appropriée (comme REP STOSB sur les architectures x86).

Comme indiqué précédemment, Memset () offre la fonctionnalité souhaitée.

MEMCPY () est pour se déplacer autour de blocs de mémoire dans tous les cas où les tampons de source et de destination ne se chevauchent pas, ou où dest <source.

Memmove () résout le cas des tampons se chevauchant et dest> source.

Sur les architectures x86, de bons compilateurs remplacent directement les appels MEMSET par des instructions d'assemblage en ligne de définition de la mémoire du tampon de destination, même en appliquant d'autres optimisations comme l'utilisation de valeurs de 4 octets à remplir le plus longtemps possible (si le code suivant n'est pas totalement syntaxiquement correct de blâme il ne mon utilise pas le code d'assemblage x86 pendant longtemps):

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:

En fait, ce code est beaucoup plus efficace que votre version Z80, car elle ne fait pas de mémoire à la mémoire, mais s'inscrivez uniquement aux mouvements de mémoire. Votre code Z80 est en fait tout à fait un piratage car il s'appuie sur chaque opération de copie ayant rempli la source de la copie suivante.

Si le compilateur est à mi-chemin, il pourrait être en mesure de détecter le code C ++ plus compliqué qui peut être décomposé à Memset (voir le post ci-dessous), mais je doute que cela se produise réellement pour les boucles imbriquées, invoquant probablement même les fonctions d'initialisation.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top