atari-home.de - Foren
Software => Coding => Thema gestartet von: Mado am Mo 27.06.2022, 07:35:44
-
Ich hatte gestern ein kleines erstes Programm geschrieben, wo ich eine Assembler-Datei asmmul.s habe, die ich in ein C-Programm einbinden möchte. Die Assembler-Unterroutine soll nichts anderes machen, als zwei WORD-Werte zu einem LONG multiplizieren und dann über D0 wieder an das C-Programm übergeben. Ich erhalte aber ein komisches Ergebnis:
a x b = 24240
Vielleicht kann mir ein alter Hase bei meinen Gehversuchen helfen? Hier mein Code:
Makefile:
# For Linux
CC = m68k-atari-mint-gcc
CFLAGS = -mshort -O2 -Wall
OBJS = main.o asmmul.o
all: asmmul.prg
asmmul.prg: $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
main.o: main.c
$(CC) $(CFLAGS) -c $<
asmmul.o: asmmul.s
$(CC) $(CFLAGS) -c $<
C-Programm main.c:
#include <stdio.h>
extern long asmmul(int factor, int value);
int main(void) {
int f = 5;
int v = 10;
long ret = 0;
ret = asmmul(f, v);
printf("a x b = %d\n", ret);
return 0;
}
Und das Assembler-Stückchen assmul.s:
.text
.global _asmmul
_asmmul:
clr.l d1
clr.l d0
move.w 2(sp),d1
move.w 4(sp),d0
muls d1,d0
rts
.end
Da ich nur die Scratch-Register D0 und D1 an Registern verwende, save ich keine Register auf dem Stack.
-
Der erste Parameter liegt bei 4(SP) auf dem Stack.
PS: Vorsicht mit -mshort. Die Standard-MiNTLib ist nicht damit compiliert. Eventuell weiß @Thorsten Otto, ob es eine MiNTLib dafür gibt. (libcmini kann man als "mshort"-Version bauen.)
-
Ah, jetzt funktioniert es. So habe ich jetzt den Code angepasst:
.text
.global _asmmul
_asmmul:
clr.l d1
clr.l d0
move.w 4(sp),d1
move.w 6(sp),d0
muls d1,d0
rts
.end
Und beim printf im C-Programm muss es natürlich ein long bei der Ausgabe sein:
printf("a x b = %ld\n", ret);
Beim Programmieren bin ich momentan komplett nur in TOS unterwegs. Soweit ich es sehen kann, ist im TOS jeder int und auch alle Übergaberegister etc 16 Bit weit. Wenn ich also auf 32 Bit aufbauen würde, müsste ich immer explizit short int (oder WORD???) angeben. Ebenfalls ist es ja so, dass bestimmte Assember-Direktiven in short schneller sind, als in long. Warum wurde dieser Weg gewählt?
-
Die TOS-Bindings sind natürlich in MiNTLib korrekt, d.h. es werden 16-Bit-Worte übergeben, wo dies nötig ist, ohne dass Du explizit "short" dran schreiben müsstest. Generell gilt beim Portieren von Unix-Software (was ja die Stärke der MiNTLib ist), dass das besser funktioniert, wenn ein "int" eben 32 Bit lang ist. (Nicht jede Software ist mit intNN_t und uintNN_t geschrieben.)
-
Ah, ok, verstanden. Ich wurschtel gerade im EmuTOS rum, also nicht im Mint. Das EmuTOS ist ja mit -mshort kompiliert. Ich möchte herausfinden, welche Grafikroutinen sich ggf. durch schnellere Assembler-Routinen ersetzen lassen. Ich komm irgendwie nicht drüber hinweg, dass ich NVDI installiere und teilweise um Faktoren schneller bin.
Ich habe mir mal einige Routinen als Assembly angeschaut und nur die Hände über dem Kopf zusammen geschlagen. Was der C-Compiler da baut, scheint mir nicht die vollen CISC-Features bzw. besonders leistungsfähige Kommandos des Prozessors auszunutzen.
Da ich m68k-Assembler immer nur im ganz kleinen Bereich genutzt habe und das auch schon 25 Jahre her ist, werd ich wohl ein ziemlich langes Weilchen brauchen, bis ich da überhaupt was zustande bringe. Siehst ja. ;-)
-
Natürlich ist es kein Problem für gcc etwas als "-mshort" zu compilieren. Ich schrieb ja bloß: Vorsicht beim Linken gegen MiNTLib.
Und was EmuTOS und Assembler-Routinen angeht: Beachte, dass EmuTOS auch auf ColdFire funktionieren muss. Somit hast Du in Assembler eigentlich immer #ifdef-Strecken, die die Wartbartkeit des Codes reduzieren.
-
PS: Vorsicht mit -mshort. Die Standard-MiNTLib ist nicht damit compiliert. Eventuell weiß @Thorsten Otto, ob es eine MiNTLib dafür gibt. (libcmini kann man als "mshort"-Version bauen.)
Nein, mintlib gibt es nicht (mehr) für -mshort. Theoretisch könnte man die wohl bauen, aber das würde vermutlich nur zu noch mehr Verwirrung führen, und auch wenig bringen, weil sie ja hauptsächlich zum port von unix-tools verwendet wird, und die würden mit 16bit-ints sowieso i.d.R nicht klar kommen. Auch müsste man dann alle anderen verwendeten Bibliotheken auch mit -mshort kompilieren.
Gemlib gibt es allerdings noch für -mshort.
-
Ah, jetzt funktioniert es. So habe ich jetzt den Code angepasst:
.text
.global _asmmul
_asmmul:
clr.l d1
clr.l d0
move.w 4(sp),d1
move.w 6(sp),d0
muls d1,d0
rts
.end
Das wäre immer noch falsch. Der erste Parameter is bei 4(sp), aber das low-word davon is bei 6(SP). Der zweite Parameter ist bei 8(sp), das low-word davon bei 10(sp).
Im Zweifelsfall einmal kurz anschauen was GCC damit macht:
int asmmul(short x, short y)
{
return x * y;
}
$ m68k-atari-mint-gcc -fomit-frame-pointer -S -o - -O2 bla.c
.text
.even
.globl _asmmul
_asmmul:
move.w 6(%sp),%d0
muls.w 10(%sp),%d0
rts
Wie man daran auch schön sieht, nutzt auch ein explizites "short" nichts, der Compiler pusht immer ein int auf den Stack.
Das ist auch der Grund für die ganzen komischen macros in osbind.h von GCC. Ohne die wäre GCC (ohne -mshort) nicht in der Lage, das für GEMDOS-Calls nötige Stack-layout zu erzeugen.
-
Ich habe mir mal einige Routinen als Assembly angeschaut und nur die Hände über dem Kopf zusammen geschlagen. Was der C-Compiler da baut, scheint mir nicht die vollen CISC-Features bzw. besonders leistungsfähige Kommandos des Prozessors auszunutzen.
Ja, manchmal sieht der Code schon komisch aus. Aber insgesamt macht er schon einen ganz guten Job. Kannst ja mal probeweise ein paar benchmarks einmal mit Pure-C und einmal mit gcc übersetzen. I.d.R. ist da nicht viel Unterschied.
-
Das wäre immer noch falsch. Der erste Parameter is bei 4(sp), aber das low-word davon is bei 6(SP). Der zweite Parameter ist bei 8(sp), das low-word davon bei 10(sp).
Siehe oben: Martin baut mit "-mshort". Da sind die Parameter auf dem Stack tatsächlich 16-bit aligned.
Kannst ja mal probeweise ein paar benchmarks einmal mit Pure-C und einmal mit gcc übersetzen. I.d.R. ist da nicht viel Unterschied.
??? Im Vergleich zwischen Pure-C und gcc gewinnt gcc meilenweit.
-
Kann mir jemand im Detail erklären, was die Assembler-Direktive lea macht? Ich habe hier ein Stückchen Beispiel-Code:
#ifdef __mcoldfire__
lea -12(sp),sp
movem.l d1/a0-a1,(sp)
#else
movem.l d1/a0-a1,-(sp)
#endif
Im Falle von Nicht-Coldfire werden die angegebenen Register auf den Stack gesichert, der Stackpointer wird jeweils vorher dekrementiert.
Ich verstehe das lea Kommando oben nicht, der Rest ist klar. Warum ist das nicht einfach ein sub #12,sp? lea lädt doch eine Adresse, aber warum sind im linken Teil dann Klammern um sp, das würde doch bedeuten, dass der Inhalt der Speicherzelle referenziert wird, auf die sp zeigt?
Und, was ist der Unterschied zwischen dem Laden einer Adresse und einer effektiven Adresse? Bei allen Büchern, die ich gelesen habe, habe ich es nicht verstanden.
-
ein
suba.w #12,sp
gibt's beim ColdFire nicht (mehr).
suba.l #12,sp
gibt's noch, der Befehl braucht aber (weil #-12 da ein Langwort ist) 3 Worte.
lea -12(sp),sp
macht effektiv dasselbe mit zwei Words.
-
lea kann man im Grunde mit dem Adress-operator (&) in C vergleichen. Es wird lediglich die berechnete Adresse ins Adress-Register geladen, aber nicht dereferenziert.
lea ist auf eigentlich auf allen Prozessoren schneller als ein suba (u.a. glaube ich weil dafür eine andere ALU benutzt wird als für Arithmetik-Operationen).
-
lea kann man im Grunde mit dem Adress-operator (&) in C vergleichen. Es wird lediglich die berechnete Adresse ins Adress-Register geladen, aber nicht dereferenziert.
Ah, danke, das war die entscheidende Erklärung, jetzt habe ich es verstanden. Es wird also in der Adressierungsart auf einen Inhalt verwiesen und die effektive Adresse ist dann die "Hausnummer".
-
lea -12(sp),sp
macht effektiv dasselbe mit zwei Words.
Das ist dann der nächste Schritt: Zu wissen, welcher Code schnell läuft und welcher langsam. Danke.
-
lea -12(sp),sp
macht effektiv dasselbe mit zwei Words.
Das ist dann der nächste Schritt: Zu wissen, welcher Code schnell läuft und welcher langsam. Danke.
Was Schnelligkeit angeht, ist es für ColdFire (fast) wurscht (beim 060 dürfte das genauso sein). lea und sub brauchen beide genau einen Taktzyklus (vorausgesetzt, der Instruction-Cache ist gefüllt und es tritt kein Pipeline-Stall auf).
Allerdings ist natürlich bei 3-Wort-Befehlen der Cache "schneller leer" als bei zweien und die Wahrscheinlichkeit eines unaligned access ist höher (dann gibt's u.U. "Straftakte").
-
Oje. Ich habe eine komplette Routine aus dem EmuTOS-ROM in Assembler implementiert, vom Algorithmus der C-Implementierung folgend. Aber ich bin nicht schneller, als der C-Compiler mit Optimierung und einer kleinen Loop, die auch im C-Original als Inline-Assembler eingebettet ist. Frust - und viel gelernt.
Ich glaube, ich habe so ungefähr jeden Fehler gemacht, den man als Assembler-Anfänger machen kann. Aber immerhin habe ich meine Routine komplett zum Laufen gebracht. :-)
Ich habe:
- WORD-Arrays nur BYTEweise indiziert und damit nette kleine Address Errors provoziert,
- Ich habe mit MOVEM weniger Register gesichert, als ich brauchte und mich über komische Abstürze gewundert,
- Ich habe Register aus versehen doppelt verwendet
- Ich habe Schleifen von C nach Assembler falsch umgesetzt, C-For-Schleifen prüfen am Anfang!
- Ich habe vorzeichenbehaftete Zahlen so behandelt, wie welche ohne
- ... und vermutlich noch viele Sachen mehr, die ich schon wieder vergessen habe
Ich habe aber auch massiv viel gelernt. Letzte Woche hatte ich eine berufliche Fahrt nach Köln und habe mir während der Fahrt komplett "ATARI ST – Programmieren in Maschinensprache", das schwarze Buch vom SYBEX-Verlag, reingetan. Dafür, dass ich gerade erst anfange, bin ich eigentlich ganz zufrieden mit mir.
Was ich auch gelernt habe: Bis auf einige Mikro-Optimierungen, die man auch in C einbetten kann, lohnt sich bei den heutigen Compilern eine Programmierung in Assembler offenbar kaum noch, außer, man will hardware-nahe Interfaces machen, oder sowas.
-
Was ich auch gelernt habe: Bis auf einige Mikro-Optimierungen, die man auch in C einbetten kann, lohnt sich bei den heutigen Compilern eine Programmierung in Assembler offenbar kaum noch,
Ganz so weit würde ich nicht gehen, aber es ist definitiv ein ganz anderer Unterschied zwischen heutigen Compilern und denen von 1986, und handgestricktem Assembler.
-
Ich bin auch schon reingefallen mit Optimierungsversuchen, die das Gegenteil bewirkt haben. Was ich in einem Vierteljahrhundert Programmierung gelernt habe:
Ganz schlecht: C-Code angucken, denken "das kann keinen guten Code ergeben" und umständlicher hinschreiben. Compiler ‒ jedenfalls moderne ‒ sind gut im Optimieren, insbesondere, wenn man den Code eben nicht vorab händisch versucht zu verbessern.
Schon besser: Compiler-Output angucken und denken "was soll denn das?". Ja, vielleicht ist eine spezifische Stelle suboptimal übersetzt. Aber wenn diese Stelle nicht in einem "heißen" Teil des Codes ist, lohnt es die Mühe dennoch nicht, sie in (Inline-)Assembler umzuschreiben.
Tipp: Hatari hat auch einen Profiler. Zumindest für die ST/68000-Emulation ist er zyklusexakt. Damit kannst Du Dir z.B. eine VDI-Funktion vornehmen und sehen, welche Codestellen "heiß" sind.
-
Ich hatte gedacht, wenn ich soweit es geht alles in Registern unterbringe, anstatt immer auf Speicherstellen im Stack-Frame zuzugreifen, dann müsste es richtig schnell werden. Aber offensichtlich macht der Compiler genau das gleiche bei entsprechenden Optimierungsoptionen, -fomit-frame-pointer etc ...
Und das mit dem "heißen Teil" ist wohl ganz wichtig.
Wenn das NVDI Dinge irgendwo doppet so schnell macht, wie EmuTOS, dann liegt es auf jeden Fall nicht an der Routine in vdi_line.c, die ich mir da gegriffen habe.
Was ich auch gespürt habe ist, dass C-Code wirklich wesentlich übersichtlicher ist, als Assembler. Ich glaube nicht, dass es nur Gewohnheit ist.
-
Wenn das NVDI Dinge irgendwo doppet so schnell macht, wie EmuTOS, dann liegt es auf jeden Fall nicht an der Routine in vdi_line.c, die ich mir da gegriffen habe.
Auch die line-Routine ist in NVDI sicherlich schneller als in EmuTOS/TOS. Das liegt aber im wesentlichen daran, daß NVDI für sehr viele spezielle Fälle (insbesondere die die auch vom AES benutzt werden), spezielle Routinen benutzt, die entsprechend optimiert sind. Auch gibt es für jede Bit-Tiefe (1/2/4/8 plane etc) eigene Routinen in den jeweiligen Treibern. Sowas kann man in EmuTOS schon aus Platz-Gründen nicht implementieren.
Vermutlich könnte mit Assembler schon ein bisschen rausholen, aber vermutlich nur in Grössenordnungen die sich lediglich mit einem Benchmark messen lassen, und beim täglichen Gebrauch kaum auffallen, und das ganze auch nur mit erheblichem Aufwand.
Aber das soll dich nicht davon abhalten es weiterhin zu versuchen ;) Wenn nichts anderes, kann man dabei auch eine Menge lernen
-
Zum Thema NVDI: NVDI macht auch einige "dreckige" Annahmen, wie z.B., dass gewissen Variablen in den ersten 32 kiB RAM stehen und somit über den xxx.W-Adressierungsmodus erreichbar sind. Wir mussten einmal EmuTOS deswegen anpassen.
Zum Thema kluge Compiler: Ein Beispiel. Divisionen sind sehr langsame Operationen (selbst auf modernen CPUs). Programmierer versuchen sie zu vermeiden. Aber nehmen wir an, eine Division durch drei sei nun einmal nötig:
uint16_t divide_by_three(uint16_t a)
{
uint16_t res;
res = a / 3;
return res;
}
Bestimmt 95% der Programmierer würden, wenn sie das händisch in Assembler formulieren müssten, in den sauren Apfel beißen und halt notgedrungen ein "DIVU.W #3,Dx" hinschreiben. Was gcc daraus macht, überrascht vielleicht den einen oder anderen: https://tinyurl.com/2cz88ktv
Auf einem 68000 braucht der naive Weg (mit DIVU) 136 Zyklen, gccs Code (obwohl mehrere Instruktionen) nur 76 Zyklen. Demnach gut 40% schneller.
-
... und wer wissen will wie (und warum) das funktioniert:
https://gmplib.org/~tege/divcnst-pldi94.pdf
-
Hilfääääää! ;)
@thh Naja, weiter versuchen – erstmal nicht. Ich könnte Loop-Unrolling machen und dann die Loop austauschen gegen eine Tabelle von Move-Anweisungen, in die ich dann ab der richtigen Anzahl Anweisungen rein springe etc, das hat aber wieder Nachteile in der Code-Größe und skaliert auch nicht auf immer größere Displays... Der Code ist schon gut so. ;) Dann lad ich mir für sowas halt NVDI (Deines).
Ach ja, den Code vereinfacht für nur ST-High hatte ich in C schon mal, hat aber kaum was gebracht, da die Verwaltung der Planes kaum Laufzeit braucht. War irgendwie 3-4% schneller. Dafür lohnt es sich nicht.
-
... und wer wissen will wie (und warum) das funktioniert:
https://gmplib.org/~tege/divcnst-pldi94.pdf
Echt interessant, nach dem heutigem Tag verdau ich das aber eher nicht. ;)
Muss ich irgendwann mal in Ruhe lesen.