zatzen hat geschrieben:Oh, ich wäre an dieser Unit interessiert, hatte dann aber gern den Quellcode und nicht nur die TPU, denn ich möchte genau verstehen was ich da verwende. Wenn das okay wäre kannst Du mir ja ne PN schreiben.
Genau zu verstehen was man benutzt, finde ich lobenswert und unterstützenswert. Viel zu viele "Programmierer" benutzen Zeug, von dem sie keine Ahnung haben und können dann nicht damit umgehen, wenn das Programm/der Algorithmus usw auch nur die kleinste Änderung erfordert und dies das "Fertigbauteil" zufällig nicht in seiner Grundanordnung unterstützt.
Ich werde die Unit dazu noch ein wenig "ausmisten" müssen. (Meine Units haben manches alte - auskommentierte - Zeug drin, das nur "herumliegt".)
zatzen hat geschrieben:Geniale Sache, das ist dann ja quasi der Unrealmode im Realmode, nur eben auf < 640K begrenzt.
Würde ich so nicht sagen. Ich arbeite nur mit dem normalen Realmode. Ich weiß, Du beziehst Dich da wahrscheinlich darauf, daß Bereiche größer als 64kByte reservierbar sind. Aber an sich ist das kein Hexenwerk.
zatzen hat geschrieben:Ich frag mich nur ob es da nicht Einbußen mit der Geschwindkeit gibt, d.h. wenn man auf eine einzelne Variable zugreifen will, ob das dann wirklich genauso schnell ist als wenn man einfach auf ein normales Array zugreift? Du schriebst mal sowas, dass Units Sachen zwar modular und übersichtlich machen, aber u.U. langsamer sind als wenn man alles direkt ins Hauptprogramm schreibt.
Das hast Du falsch verstanden. Gemeint ist: Wenn man
VÖLLIG OHNE UNTERPROGRAMME (also ohne procedures/functions) arbeitet, hat man einen SEHR GERINGFÜGIGEN Geschwindigkeitsvorteil.
Erklärung: Werden Subroutinen aufgerufen, wird ja ein CALL ausgeführt und bei Verlassen derselben ein RET. (oder bei FAR CALLs eben RETF.) Die paar Taktzyklen, die für das Ausführen dieser beiden Befehle benutzt werden, sind der "Geschwindigkeitsnachteil".
Ich hatte dies nur der Vollständigkeit halber erwähnt.
Ich bringe mal ein Beispiel, wo dieser Rechenzeitverlust ein Problem ist:
Eine Pixelroutine.
Nehmen wir an, man will eine "Kachel", ein Sprite, ein Bild o.ä. anzeigen.
Was macht man? Man macht zwei Schleifen:
Code: Alles auswählen
for y:=0 to maxY do
for x:=0 to maxX do
pixel(x,y,bild[x,y]);
Richtig? - FALSCH! Warum? Weil man (maxY+1)*(maxX+1) mal die Subroutine pixel(x,y,farbe) aufruft, (d.h. CALL und RET) dieser Subroutine jedesmal X- und Y- Koordinate übergeben werden, die Subroutine jedesmal die Position im Grafikspeicher ausrechnet....
Das ist MEGA DÄMLICH!
Warum? Wenn man etwas darstellt, wo die Pixel sowieso direkt neben-/untereinander liegen, braucht man nur den Startpunkt im Grafikspeicher "ausrechnen" (oder mit Hilfe von Tabelle, wenn mal Multiplikationen ganz vermeiden will). Und dann jeweils mit schlichten Additionen die Position erweitern. Und mit einer "Sprite-Routine" oder "Rechteck-Routine" usw... das ganze Objekt darstellen, anstatt tausendemale eine generalisierte Pixelroutine anzuspringen!
Das wäre ein Beispiel für die Verschwendung von Rechenzeit. Kann man sich ganz leicht merken: Je "innerer" die Schleife, umso weniger darf sie enthalten. Es multipliziert sich ja mit jeder Anzahl Schleifendurchläufe jeder übergeordneten Schleife. Angenommen: Spielbildschirm, 320x200 (64000 Pixel), soll mit 70Hz (MCGA-Frequenz für Mode 13h) Bildrate laufen. Sind also 64000*70 = 448000 Pixel pro Sekunde. Will man pro Sekunde 448000 Multiplikationen durchführen lassen und 448000 mal pro Sekunde eine generalisierte Routine aufrufen? Und das ist wirklich eine recht kleine Auflösung. Dann kommt ja noch dazu, daß die Daten noch irgendwo hergeholt werden müssen und daß im Programm (z.B. ein Spiel) ja auch noch andere Dinge passieren sollen, die auch noch "in dieser Sekunde" abgearbeitet werden sollen...
ANDERERSEITS:
Man kommt nicht ohne Subroutinen aus! Wenn ein Projekt ein wenig komplexer wird, kann man ja nicht alles hunderte Male in den Code reinbasteln, nur weil man kein Unterprogramm aufrufen will... Da wägt man dann eben wirklich ab. Es ist zwar möglich, auch ein total komplexes Programm völlig ohne Aufruf von Subroutinen, sondern allein mit Verzweigungen und Entscheidungsmatrizen o.ä. zu schreiben - aber im Endeffekt ist es fraglich, ob die "Selbstorganisation" eines solchen Programms nicht den (theoretisch vorhandenen) Geschwindigkeitsgewinn dann wieder auffrißt...
Es hat aber nichts damit zu tun, ob etwas in einer Unit oder direkt im Hauptprogramm steht.
zatzen hat geschrieben:Und dann wäre da noch eine Frage mangels Erfahrung, können mehrere TPUs die ich benutze
jeweils diese Speicherverwaltungs TPU nutzen, ohne dass diese dann mehrmals in die EXE einwandert, ist der Compiler so intelligent?
Ja, der Borland-Pascal-Compiler IST so intelligent! Wenn man die Units mehrmals benutzt (auch als "Unter-Unit" in eine andere eingebaut) wird diese nur einmalig eingefügt. Zusätzlich ist der Compiler auch SO intelligent, daß er auch wirklich nur DIE Konstanten, Variablen, Procedures und Functions (und auch alles andere) nur DANN einbaut, wenn es mindestens einmal irgendwo benutzt/aufgerufen wird!
zatzen hat geschrieben:Die Sache mit dem "absoluten" Zugriff auf Daten, die auf bis 192 verschiedenen Pointern liegen, löse ich bis jetzt einfach damit, dass ich einen Pointer definiere auf dem die "absolute"-Variable
definiert ist, und wenn ich auf diese oder jene Daten der 192 Pointer zugreifen will schreibe
ich vorher kurz sinngemäß " data_pointer := data_pointer_array[99] " oder welcher wert in der
Klammer auch immer.
Ja, so mache ich das auch oft. Man kann ja auch einfach das Segment erhöhen. Siehe nächster Absatz
zatzen hat geschrieben:Oh und noch eine Frage: Wie kann ich denn mittels einer Struktur wie
" datenfeld: ^datenfeldtype absolute daten_pointer "
auf einen Bereich > 65535 Byte zugreifen, wenn eine mit "type" definierte
Struktur nicht größer als 65535 Byte sein darf?
Löst Du das in der Unit durch eine Function mit einem longint als Eingangsparameter?
Naja, erstens benutze ich immer den Compilerschalter für offene Array-Grenzen.
D.h. wenn man ein
deklariert, kann man trotzdem sowas machen:
und bekommt dann den Wert an der Stelle angezeigt, die von A aus 1000 Bytes entfernt ist.
(Nur wenn man write(A[1000]); - also mit einer Konstante - schreibt, meckert der Compiler, weil außerhalb Arraygrenzen. Aber wenn man mit Variablen arbeitet, "weiß" der Compiler nicht, welchen Wert B zur Laufzeit des Programms haben wird und erlaubt es.
Man sollte sowieso alle Dinge, die Überläufe u.ä. testen, mit den entsprechenden Compilerschaltern abschalten. Aus 3 Gründen:
1.) Wenn ein Programm Dinge tut, die es nicht soll, gehört es debugged und nicht ge-watchdogged!
2.) Wenn diese "Watchdog" Routinen drin sind, wird ein Programm größer und langsamer.
3.) Mitunter WILL man diese Überläufe benutzen, da sie einem komplizierte Abfragen oder "Drumherumprogrammieren" ersparen.
Beispielprogramm:
Ich habe mal angenommen einem Pointer Ptemp mit meiner Unit 262144 Bytes (256kByte) Speicher zugewiesen.
Wie greife ich im Realmode darauf zu? (Jetzt mal ganz einfach, um es zu zeigen)
Code: Alles auswählen
function GetByteFromPointer(P:Pointer;Address:longint):byte;
var PZ:record O,S:word;end absolute P;
var AZ:record L,H:word;end absolute Address;
type TM=array[0..$FFFE]of byte;
var M:^TM absolute P;
begin
with PZ do with AZ do
begin inc(S,H shl 12);GetByteFromPointer:=M^[L];end;
end;
Und mit GetByteFromPointer(Ptemp,87654) erhält man dann das Byte, das in Ptemp an der 87654. Stelle steht (von 0 an gezählt).
Man kann natürlich auch mal ganze Ketten von Bytes lesen:
Code: Alles auswählen
const ADDER:array[false..true]of word=(0,4096);
var P:Pointer;
var PZ:record O,S:word;end absolute P;
var AZ:record L,H:word;end absolute Address;
var B:^byte absolute P;
var KETTE:array[0..1023];
var K:word;
begin
with PZ do with AZ do
begin K:=O;inc(O,L);inc(H,ord(O<K));inc(S,H shl 12);
for K:=0 to 1023 do
begin KETTE[K]:=B^;inc(O);inc(S,ADDER[O=0]);end;
end;
end;
(Man kann auch statt inc(S,ADDER[O=0]); schreiben: if O=0 then inc(S,4096); und die ADDER-Konstante weglassen...)
Das mit dem Array KETTE ist jetzt nur ein Beispiel.
So oder so ähnlich mache ich das auch, wenn ich da in Hochsprache etwas baue.
Das liegt einfach an diesem segmentierten Speichermodell im Realmode. Ein Segment fängt immer an einer 16er Adresse an. Oder andersherum: 65536 Bytes (64kByte) sind 4096 Segmente. D.h. wenn man durch 65536 Bytes (Offset) durch ist, muß man das Segment um 4096 erhöhen (also $1000).
Man kann das auch völlig anders programmieren, das sind nur Beispiele.
in assembler kann man z.B. den Segmentwechsel so machen:
(angenommen, das Segment ist in ES und der Offset in DI:
inc DI;jz @INCSEG;
:
:
@INCSEG: mov AX,ES;add AH,$10;mov ES,AX;jump... {wieder an Schleifenanfang ...}
Oder wenn man um mehr als 1 erhöht (z.B. 5)
add DI,5; jc @INCSEG; ... usw. {Bei add funktioniert das Carry-Flag für den Überlauf}
Der Möglichkeiten gibt es viele. Ich verwende teilweise recht raffinierte Methoden, um platz- und speichersparende Dinge mit diesem Kram zu tun... Manches passe ich direkt auf den jeweiligen Zweck an.
Ich WEIß natürlich, daß hier ein FLAT Zugriff schneller und einfacher wäre. Dann brauche ich aber auch ein restliches "Konstrukt drumherum", das auch mit diesem FLAT Mode (oder wenn gewünscht auch Protected Mode) umgehen kann. Und das normale Borland-Pascal hat das eben nicht. Ich schätze dafür andere Dinge daran - wie direktes Einfügen von ASM-Routinen/Befehle in den Sourcecode oder die Möglichkeit der direkten Übernahme von Variablen(Adressen) in den ASM-Source-Teil. Wenn man sich darauf einläßt, kann man mit der Kombination Pascal / Assembler eine Menge Dinge tun. Die nicht-386er-Code-Fähigkeit habe ich mittlerweile gelernt zu umgehen, indem ich entsprechende Befehle (oder Präfix-Bytes) selbst in den Code einsetze. Man kann sich dran gewöhnen. (Immer noch besser, als unter Windows programmieren zu müssen...)
Es kann auch sein, daß ich in o.g. Codeschnipsel irgendwelche Fehler drin hab, hab jetzt einfach aus dem Kopf irgendwas schnell hingetackert. Habe es nicht getestet.
Man muß auch nicht mit Variablen umgehen, wenn es sowieso nur normale Typen sind.
Ob man oben B^ benutzt oder gleich mem[S:O]; spielt wahrscheinlich keine große Rolle, da B^ in diesem Fall auf CPU-Ebene sowieso aus Segment S, Offset O geholt werden muß.
Ich weiß nicht GANZ GENAU, wie Pascal das dann macht. In ASSEMBLER hat man ja die Chance, den Wechsel eines Segmentregisters "so selten wie möglich" durchzuführen (da Segmentregisterwechsel etwas Zeit kosten).
Besser wäre oben übrigens ohne AX zu arbeiten, sondern zu Anfang das Segmentregister an eine Speicherstelle als Word zu speichern, dann im @INCSEG: Teil nur diese Speicherstelle (also ihr Highbyte) um $10 (16) zu erhöhen und ES daraus zu laden. (Aus Speicherstellen können Segmentregister "direkt" geladen werden, brauchen dann nicht den "Umweg" über ein Basisregister.)
Naja, usw. Würde jetzt zu weit führen.
Methoden wie diese und ähnliche verwende ich eben in meinen Programmen - wenn ich sie auf <640kB beschränke. Mittlerweile habe ich aber auch die HIMEM.SYS Routinen "liebgewonnen", d.h. ich verwende auch ganz gern mal XMS. Und, daß das ganze angeblich sooooo langsam sein soll, kann ich - zumindest auf meinem Rechner - nicht bestätigen. Ich finde es erstaunlich schnell, wie das Ganze da "hin-und-her-kopiert" wird. Man sollte es natürlich trotzdem intelligent genug programmieren, um sich auf "blockweises" Kopieren einzustellen und nicht "byteweise" zwischen "Realmode-Speicher" und XMS so Zeug hin- und herzuschaufeln...
Ich habe übrigens auch eine "XMS"-Unit gemacht, die fast 100% ASM ist, allerdings mit Procedures/Functions mit Pascal-Headern - so daß man mit einfachen Pascal-Procedures/Functions die Funktionen von HIMEM.SYS nutzen kann (XMS reservieren/freigeben, innerhalb der Bereiche kopieren/verschieben... Außerdem habe ich auch dafür etwas gemacht, das ähnlich funktioniert wie meine SpeicherUnit (mit den Pointerzuweisungen), nur daß als "Pointer" hier dann einfache Longints genutzt werden (die die Offset-Adresse im XMS-Handle enthalten).
Naja, wahrscheinlich schon wieder "too much information"...