Skip to content

Latest commit

 

History

History
481 lines (386 loc) · 24 KB

ml_technieken.md

File metadata and controls

481 lines (386 loc) · 24 KB
                    M L   T E C H I E K E N   ( 1  )
                                                     
      
      Het  lijkt  mij  een  goed  idee  om  naast de  cursus BASIC 
      technieken  op Sunrise  Magazine een cursus ML technieken te 
      starten op  de Special.  Ook dit  is geen  programmeercursus 
      maar  een verzameling tips en truuks en manieren hoe je iets 
      het beste  kunt aanpakken. Ik ben overigens niet van plan om 
      deze  cursus alleen  te schrijven, maar zal zeker een aantal 
      ML programmeurs  met meer ervaring vragen hun geheimen prijs 
      te  geven  in  deze  rubriek.  Deze  keer  een paar  simpele 
      problemen waar beginners tegenaan zullen lopen.
      
      Ik zal bij vergelijking van routines tellingen van klokcycli 
      en bytes geven, bij de klokcycli ga ik uit van een Z80. Over 
      het  algemeen zal  een routine  die sneller is op de Z80 ook 
      sneller zijn  op de  R800, maar dit is geen wet van Meden en 
      Perzen.
      
      
                B Y T E   U I T   T A B E L   H A L E N 
      
      Stel  je  hebt een  tabel en  je hebt  een waarde  in A  die 
      aangeeft de  hoeveelste byte  van de  tabel je  wilt hebben. 
      Deze byte moet uiteindelijk in A terechtkomen. Hiervoor moet 
      je  het adres  in de tabel uitrekenen, het liefst zou je dat 
      doen met
      
              LD      HL,Tabel
              ADD     HL,A
              LD      A,(HL)
      
      maar de instructie ADD HL,A bestaat natuurlijk niet, want je 
      kunt geen 8 bits register bij een 16 bits register optellen. 
      We zullen  de instructie  ADD HL,A dus zelf moeten maken. Er 
      zijn  op z'n  minst twee  oplossingen mogelijk. De eerste is 
      degene die ik meestal gebruik:
      
                                      klokcycli       bytes
      
              LD      E,A             5               1
              LD      D,0             8               2
              LD      HL,Tabel        11              3
              ADD     HL,DE           12              1
              LD      A,(HL)          8               1
                                     ----            ---
                                      44              8
      
      De  eerste  twee instructies  betekenen LD  DE,A. Vervolgens 
      wordt DE  bij HL  opgeteld en  de byte uit de tabel gelezen. 
      Het  nadeel hiervan  is dat je het DE register (of eventueel 
      het BC  register) gebruikt.  De volgende  oplossing doet dat 
      niet:
      
                                      klokcycli       bytes
                              
              LD      HL,Tabel        11              3
              ADD     A,L             5               1
              LD      L,A             5               1
              LD      A,0             8               2
              ADC     A,H             5               1
              LD      H,A             5               1
              LD      A,(HL)          8               1
                                     ----            ---
                                      47              10
      
      
      ADD A,L;  LD L,A  is een  substituut voor  ADD L,A (dat niet 
      bestaat). Vervolgens moet nog de carry bij H worden opgeteld 
      (LD  A,0; ADC  A,H; LD  H,A). Het voordeel is dat het DE (of 
      BC) register  niet wordt gebruikt, maar deze routine is iets 
      trager  (3  klokcylci  is 0.84  microseconde, niet  echt een 
      verschil  dat je  snel zult merken) en wat erger is: 2 bytes 
      langer. Ik  heb vaak problemen dat mijn code te groot wordt, 
      en   ik  heb   daarom  een   grote  voorkeur   voor  kortere 
      oplossingen, zelfs als dat een paar klokcycli trager is.
      
      Er is nog een alternatief dat sterk op de vorige lijkt, hier 
      wordt alleen  een andere manier gekozen om de carry bij H op 
      te tellen:
      
                                      klokcycli       bytes
                              
              LD      HL,Tabel        11              3
              ADD     A,L             5               1
              LD      L,A             5               1
              JR      NC,Label        13/8            2
              INC     H               0 /5            1
      Label:  LD      A,(HL)          8               1
                                     ----            ---
                                      42              9        
      
      Het  grappige  van  deze  routine  is  dat  hij  ondanks  de 
      voorwaardelijke  sprong altijd  even snel is, dit komt omdat 
      als hij  springt (13  klokcycli) hij  de INC H niet doet, en 
      als  hij  niet  springt  (8  klokcycli)  hij  de  INC  H  (5 
      klokcycli)  er nog bij doet, waardoor de LD A,0; ADD A,H; LD 
      H,A constructie  van de  vorige oplossing  (18 klokcycli,  4 
      bytes)  dus wordt  vervangen door  13 klokcycli  en 3 bytes. 
      Vandaar de 5 klokcycli en 1 byte winst.
      
      Deze  oplossing is  dus sneller  dan de oplossing met DE (44 
      klokcycli), maar  het kost  weer een  byte meer (9 in plaats 
      van 8). Als je ruim in je geheugen zit en die ene byte extra 
      je  geen bal  kan schelen  is deze  oplossing dus  het beste 
      (lees: snelste),  en als  je het  DE register  al voor  iets 
      anders  in gebruik hebt is het zeker de beste. Maar als elke 
      byte telt  geef ik  de voorkeur aan de eerste oplossing. Nog 
      een  nadeel van  deze oplossing  is dat  het een extra label 
      geeft en  of dat  een nadeel is is vooral afhankelijk van de 
      assembler die je gebruikt.
      
      Tot slot nog even hoe het niet moet:
      
                                      klokcycli       bytes
      
              LD      IX,Tabel        15              4
              LD      (Poke+2),A      14              3
      Poke:   LD      A,(IX+0)        20              3
                                     ----            ---
                                      49              10
      
      Dit  is  verreweg  de  meest  lelijke  oplossing die  ik kan 
      bedenken. Hij is ook nog eens de traagste en net zo lang als 
      de langste  oplossing tot  nu toe. Bovendien doe je iets dat 
      eigenlijk  helemaal  niet  mag  (het  kan niet  eens als  je 
      programma  op ROM  wordt gezet!),  namelijk het  poken in je 
      code. Indexregisters zijn traag maar erg handig bij tabellen 
      met  een  variabel  beginadres  waaruit  je  bytes op  vaste 
      plaatsen  wilt  lezen. (Er  zijn namelijk  geen LD  A,(IX+B) 
      instructies of  zoiets, het is altijd LD A,(IX+n), waarbij n 
      een  constante tussen  -128 en +127 is.) Ik heb daar al eens 
      een tekst over geschreven op de Special. Hier is het precies 
      andersom: een  vast beginadres  en een variabele plaats. Dat 
      doe je dus NIET met indexregisters. Het ziet er in je source 
      erg  kort  uit  (3  regels,  terwijl  de  andere oplossingen 
      respectievelijk  5, 6  en 7  regels in  beslag nemen),  maar 
      instructies met IX en IY zijn gewoon erg traag.
      
      Er  is  nog  ��n  truuk die  je kunt  gebruiken als  je echt 
      maximale snelheid nodig hebt: zorg dat je tabel op een adres 
      begint waarvan de lowbyte gelijk is aan 0! Dus bijvoorbeeld:
      
      Tabel:  EQU     #D000
      
                                      klokcycli:      bytes:
      
              LD      H,#D0           8               2
              LD      L,A             5               1
              LD      A,(HL)          8               1
                                     ---             ---
                                      21              4
      
      Deze routine  is twee  keer zo snel en twee keer zo kort als 
      de   snelste  en   kortste  routines   die  we  tot  nu  toe 
      tegenkwamen, maar  dat komt  door de speciale eis die aan de 
      tabel wordt gesteld. Op zich heel mooi maar vaak onhandig om 
      toe  te passen, zeker als je veel tabellen hebt. Toch is dit 
      zeker iets  wat je in gedachten moet houden voor als je eens 
      in snelheidsnood zit.
      
      
                           J U M P T A B E L 
      
      Iets wat veel met het vorige onderwerp te maken heeft is een 
      jumptabel. Vergelijk dit maar met ON A GOTO in BASIC. Altijd 
      een  zeer snelle methode om een keuze uit een redelijk groot 
      aantal  alternatieven   te  maken.   Bij  slechts  een  paar 
      alternatieven is CP x; JP Z,y korter en sneller.
      
      Ik  heb  hiervoor  altijd een  standaardroutine met  de naam 
      Jump.  In A staat het nummer van de gewenste sprong en in HL 
      het begin van de jumptabel. Die routine is als volgt:
      
                                      klokcycli:      bytes:
      
      Jump:   DEC     A               5               1
              ADD     A,A             5               1
              LD      E,A             5               1
              LD      D,0             8               2
              ADD     HL,DE           12              1
              LD      E,(HL)          8               1
              INC     HL              7               1
              LD      D,(HL)          8               1
              EX      DE,HL           5               1
              JP      (HL)            5               1
                                     ----            ---
                                      68              11
      
      De  DEC  A  is  nodig  omdat  het eerste  adres in  de tabel 
      natuurlijk offset 0 heeft. Daarna verdubbelen we A omdat een 
      adres 2  bytes lang is. Daarna tellen we A bij HL op via DE, 
      omdat we net hebben gezien dat dat de korste oplossing is en 
      we  DE  sowieso  nodig  hebben.  Een  instructie  LD DE,(HL) 
      bestaat  helaas niet  dus doen we dat met LD E,(HL); INC HL; 
      LD D,(HL). Tenslotte verwisselen we DE en HL (want het adres 
      waarnaar  we  moeten  springen  staat in  DE en  moet in  HL 
      terecht komen)  en springen  we naar  dat adres toe. Volgens 
      mij  is  er  geen betere  oplossing hiervoor,  de eis  dat L 
      gelijk  moet zijn  aan 0 (zodat we alleen maar LD L,A hoeven 
      te doen  in plaats  van LD  E,A; LD  D,0; ADD HL,DE) is hier 
      zeer onhandig.
      
       - Deze tekst wordt vervolgd in de volgende submenu-optie -
      
      
      
                     - Dit is het tweede gedeelte -
      
                   M L   T E C H N I E K E N   ( 1  )
                                                      
      
                             C O M P A R E 
      
      Veel beginnende programmeurs hebben moeite met de instructie 
      CP,  wat  een afkorting  is voor  ComPare. Eigenlijk  is het 
      helemaal niet moeilijk als je maar weet hoe het werkt.
      
      De volgende CP instructies zijn mogelijk:
      
                                      klokcycli:      bytes:
      
              CP      (HL)            8               1
              CP      (IX+n)          20              3
              CP      (IY+n)          20              3
              CP      A               5               1
              CP      B               5               1
              CP      C               5               1
              CP      D               5               1
              CP      E               5               1
              CP      H               5               1
              CP      L               5               1
              CP      n               8               2
      
      Bij een  CP x instructie zal de Z80 de vlaggen zo zetten als 
      ook bij een SUB x instructie gebeurt, zonder de waarde van A 
      te veranderen. Er zijn dus drie gevallen mogelijk:
      
      - als x kleiner is dan A dan komt er geen carry want je kunt 
        x  gewoon  van  A aftrekken  zonder dat  je een  negatieve 
        uitkomst krijgt
      - als x gelijk is aan A dan komt er een zero want A-x is dan 
        gelijk aan  0, er  is GEEN  carry want  er is nog net geen 
        negatieve uitkomst
      - als x groter is dan A dan komt er een carry want er is een 
        nieuwe uitkomst
      
      Samengevat:
      
      A >= x          NC
      A <  x          C
      A =  x          Z
      A <> x          NZ
      
      Je hoeft  dit niet  uit je  hoofd te  kennen, je kunt gewoon 
      beredeneren  wat er met de zero- en carryflag gebeurt als je 
      x van A af zou trekken.
      
      Als  je  zoals  ik  in  het  vorige  onderdeel  al  opmerkte 
      slechts  weinig alternatieven hebt, dan is het sneller om de 
      volgende constructie toe te passen:
      
                                      klokcycli:      bytes:
      
              CP      1               8               2
              JR      Z,Keuze1        8/13            2
              CP      2               8               2
              JR      Z,Keuze2        8/13            2
      Keuze3: .....
      
      Dit neemt 8 bytes in beslag (de jumproutine 11) en het neemt 
      8  + 13  = 21  klokcycli als  A=1 en  8 +  8 +  8 +  13 = 37 
      klokcycli in  beslag als A=2 en 8 + 8 + 8 + 8 = 32 klokcycli 
      als  A=3. Zo  heb je dus bij drie alternatieven minder bytes 
      en minder  klokcycli nodig  in vergelijking  met de  routine 
      Jump.  (Indien JP nodig is in plaats van JR neemt het aantal 
      bytes met  2 toe waardoor het op 10 komt (nog steeds kleiner 
      dan  11) en  het aantal klokcycli is respectievelijk 18 voor 
      A=1 en 36 voor A=2 en A=3. Nog steeds veel sneller dan de 68 
      klokcycli van  Jump.) We  gaan er  hier overigens wel vanuit 
      dat  A alleen  de waardes  1, 2  en 3 kan aannemen, want bij 
      deze routine  zal ook  bij A=0  en A=4  t/m A=255 de routine 
      Keuze3 worden uitgevoerd.
      
      
                              L U S S E N 
      
      De Z80  heeft een simpele voorziening voor lussen in de vorm 
      van   DJNZ  (Decrease,  Jump  if  NonZero).  Hierbij  is  de 
      lusteller altijd  B en is de maximale luslengte dus 256. Een 
      luslengte  van  256  kan daarbij  bereikt worden  met 0  als 
      startwaarde van B!!! Een voorbeeld:
      
                                      klokcycli:      bytes:
      
              LD      B,10            8               2
      Lus:    DJNZ    Lus             14/9            2
                                                     ---
                                                      4
      
      In totaal  vergt deze lus 8 + 9*14 + 9 = 143 klokcycli. Deze 
      lus   doet  niets   boeiend,  alleen   even  wachten,  40.04 
      microseconde  om  precies  te  zijn.  Je  kunt  het  ook  zo 
      programmeren:
                                      
                                      klokcycli:      bytes:
      
              LD      D,10            8               2
      Lus:    DEC     D               5               1
              JR      NZ,Lus          13/8            2
                                                     ---
                                                      5
      
      Ik heb  het expres  met D  gedaan om te laten zien dat je nu 
      niet meer aan B bent gebonden als lusvariabele. Het kost ��n 
      byte meer en verder vergt deze lus 8 + 10*5 + 9*13 + 8 = 183 
      klokcycli  (51.24  microseconde).  Met DJNZ  bespaar je  dus 
      zowel ruimte als tijd, maar alleen B kan gebruikt worden als 
      lusvariabele.
      
      Grotere  lussen  kunnen  gemaakt worden  door geneste  DJNZ- 
      lussen, bijvoorbeeld:
      
                      LD      B,100
      BuitensteLus:   PUSH    BC
                      LD      B,100
      BinnensteLus:   ......
                      ......
                      DJNZ    BinnensteLus
                      POP     BC
                      DJNZ    BuitensteLus
      
      Hierbij  wordt  de  binnenste  lus 100  * 100  = 10000  maal 
      uitgevoerd. Dit  is alleen geschikt voor vaste luslengte, en 
      dan  vooral als  er ook  echt sprake is van een binnenste en 
      een buitenste  lus. Bijvoorbeeld  de binnenste lus doet iets 
      met alle dingen van ��n regel en de buitenste lus loopt alle 
      regels  langs.  Bij  een  variabele  luslengte  of  als  het 
      eigenlijk  gewoon de bedoeling is dat de binnenste lus 10000 
      maal  wordt  herhaald, is  de volgende  oplossing het  meest 
      geschikt:
      
                      LD      BC,10000
      Lus:            ......
                      ......
                      DEC     BC
                      LD      A,B
                      OR      C
                      JR      NZ,Lus
      
      Bij een  8 bits  DEC (DEC  A, B,  C, etc.) wordt de Zeroflag 
      gezet  als 0  wordt bereikt. Bij een 16 bits DEC is dit niet 
      het geval, DEC BC zet de Z-flag dus NIET als BC gelijk wordt 
      aan 0.  Daarom kijken  we met  LD A,B; OR C of zowel B als C 
      gelijk zijn aan 0, want dan is automatisch BC gelijk aan 0.
      
      Dit doet me trouwens denken aan een verschil tussen DEC A en 
      SUB  1.  Op het  eerste gezicht  doen deze  twee instructies 
      exact  hetzelfde,  namelijk het  verlagen van  A met  1. Het 
      verschil zit  hem echter  in de  vlaggen: bij  DEC wordt  de 
      carryflag niet be�nvloed, bij SUB 1 wel! Dus als A gelijk is 
      aan  0 en  je doet  DEC A, dan wordt er geen carry gezet, en 
      als A  gelijk is  aan 0 en je doet SUB 1, dan wordt de carry 
      wel  gezet. Dit  is echt  zo'n irritant  verschil waar je op 
      moet letten,  want het  kan soms  hele vage bugs veroorzaken 
      omdat  je ervan  uitgaat dat de carryflag gezet wordt en het 
      gebeurt niet.  [Ik had  ergens SUB 1 in een source staan, en 
      toen  zei iemand aan wie ik die source liet zien dat ik daar 
      DEC  A  van moest  maken, omdat  dat hetzelfde  zou doen  en 
      sneller is  en minder bytes in beslag neemt. Ik had dat toen 
      veranderd  en merkte pas later, toen diegene alweer weg was, 
      dat het programma niet meer werkte!]
      
      
                          1 6   B I T S   L D 
      
      Veel  16 bits  instructies die  je vaak  nodig hebt zoals LD 
      DE,HL; LD IX,HL en LD DE,IY bestaan niet. Hiervoor zullen we 
      dus andere oplossingen moeten zoeken.
      
      Ten eerste deze groep:
      
      LD      BC,DE
      LD      BC,HL
      LD      DE,BC
      LD      DE,HL
      LD      HL,DE
      LD      HL,BC
      
      Deze  instructies  bestaan  helaas  niet  maar ze  zijn heel 
      makkelijk te maken met 8 bits LD instructies:
      
      LD      BC,DE   wordt:  LD      B,D
                              LD      C,E
      LD      DE,HL   wordt:  LD      D,H
                              LD      E,L
      etc.
      
      Zo'n  "16 bits  LD" kost  2 bytes  en 10  klokcycli. Sommige 
      programmeurs gebruiken de volgende methode:
      
      LD      BC,DE   wordt:  PUSH    DE
                              POP     BC
      LD      DE,HL   wordt:  PUSH    HL
                              POP     DE
      
      Dit kost ook 2 bytes maar is veel trager, want een PUSH kost 
      12  klokcycli en een POP 11, waardoor het totaal op 23 komt. 
      Ofwel meer dan twee keer zo lang.
      
      Voor de  tweede groep  is dit  (als je  je aan  de offici�le 
      instructieset van de Z80 houdt) niet mogelijk:
      
      LD      IX,HL
      LD      IX,DE
      LD      IX,BC
      LD      IX,IY
      LD      HL,IX
      LD      DE,IX
      LD      BC,IX
      LD      IY,IX
      
      en  dezelfde instructies  met IY kun je alleen maken met een 
      PUSH en een POP. Bijvoorbeeld:
      
                                              klokc:  bytes:
      
      LD      IX,HL   wordt:  PUSH    HL      12      1 
                              POP     IX      15      2
                                             ----    ---
                                              27      3
      
      LD      BC,IY   wordt:  PUSH    IY      16      2
                              POP     BC      11      1
                                             ----    ---
                                              27      3
      
      Op  de R800  [of met  illegale instructies van de Z80 die je 
      dus niet  mag gebruiken  omdat ze  bij de  Z80's niet getest 
      zijn  en dus  fout kunnen  gaan] kan  het wel  met 8 bits LD 
      instructies. Dit  kan echter  alleen met  DE en BC, niet met 
      HL!  Ook LD  IX,IY en LD IY,IX kunnen alleen met PUSH en POP 
      worden  opgelost.   We  schakelen  nu  bij  het  tellen  van 
      klokcycli  nu even  over op  klokcycli van  de R800. Ik laat 
      voor  LD  IX,DE  en  LD  BC,IY  van  beide  alternatieven de 
      tellingen zien:
      
      LD      IX,DE   wordt:  PUSH    DE      4       1
                              POP     IX      4       2
                                             ---     ---
                                              8       3
      
                      of:     LD      IXH,D   2       2
                              LD      IXL,E   2       2
                                             ---     ---
                                              4       4
      
      LD      BC,IY   wordt:  PUSH    IY      5       2
                              POP     BC      3       1
                                             ---     ---
                                              8       3
      
                      of:     LD      B,IYH   2       2
                              LD      C,IYL   2       2
                                             ---     ---
                                              4       4
      
      Op de R800 is de variant met 8 bits LD dus twee keer zo snel 
      maar  neemt wel  een byte  meer in beslag. Bovendien moet je 
      wel goed  nadenken omdat niet alles mogelijk is. Maar als je 
      daar  een fout mee maakt zal de compiler je wel waarschuwen. 
      Nog een  nadeel hiervan  is dat  GEN80 en  WB-ASS2 geen IXL, 
      IXH,  IYL en  IYH ondersteunen  en je dus met DB's moet gaan 
      werken (DB  #DD voor  IX en  DB #FD  voor IY gevolgd door de 
      instructie op H of L).
      
      Dit was het voor deze keer. Veel succes met het programmeren 
      in  ML, met  vragen kun je natuurlijk altijd bij de redactie 
      terecht.
      
                                                      Stefan Boer