PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : c an assembler



DarkAvenger
30.07.2004, 09:16
So, ich schreibe mal lieber einen seperaten thread hierzu, weil es eher von allg. Natur ist.

Ich war ja am abwägen, ob ich den gcc inline assembler oder nasm benutzen soll. Ich denke, daß ich doch lieber nasm benutzen sollte, weil mir diese Lösung portabler erscheint.

Also versuche ich nun zu verstehen, wie man unter nasm Prozeduren schreibt, die man von c aus aufrufen kann.

Dabei habe ich folgendes in der nasm Doku gefunden und bin ein wenig verwirrt:



Thus, you would define a function in C style in the following way:

global _myfunc

_myfunc:
push ebp
mov ebp,esp
sub esp,0x40 ; 64 bytes of local stack space
mov ebx,[ebp+8] ; first parameter to function

; some more code

leave ; mov esp,ebp / pop ebp
ret


Ich dachte mir, wozu den Anfang, dazu gibt es doch ENTER:



B.4.65 ENTER: Create Stack Frame


ENTER imm,imm ; C8 iw ib [186]


ENTER constructs a stack frame for a high-level language procedure call. The first operand (the iw in the opcode definition above refers to the first operand) gives the amount of stack space to allocate for local variables; the second (the ib above) gives the nesting level of the procedure (for languages like Pascal, with nested procedures).


The function of ENTER, with a nesting level of zero, is equivalent to


PUSH EBP ; or PUSH BP in 16 bits
MOV EBP,ESP ; or MOV BP,SP in 16 bits
SUB ESP,operand1 ; or SUB SP,operand1 in 16 bits


This creates a stack frame with the procedure parameters accessible upwards from EBP, and local variables accessible downwards from EBP.


With a nesting level of one, the stack frame created is 4 (or 2) bytes bigger, and the value of the final frame pointer EBP is accessible in memory at [EBP-4].


This allows ENTER, when called with a nesting level of two, to look at the stack frame described by the previous value of EBP, find the frame pointer at offset -4 from that, and push it along with its new frame pointer, so that when a level-two procedure is called from within a level-one procedure, [EBP-4] holds the frame pointer of the most recent level-one procedure call and [EBP-8] holds that of the most recent level-two call. And so on, for nesting levels up to 31.


Stack frames created by ENTER can be destroyed by the LEAVE instruction: see section B.4.136.


Nun weiß ich nicht, ob ich was durcheinander schmeiße, bzw was mit nested oben gemeint ist, und wie ich den Parameter für c setzten müßte. In welcher Realtion dazu steht folg. Passus (insbesondere interessieren mich die verschiedenen offsets.



The callee may then access its parameters relative to EBP. The doubleword at [EBP] holds the previous value of EBP as it was pushed; the next doubleword, at [EBP+4], holds the return address, pushed implicitly by CALL. The parameters start after that, at [EBP+8]. The leftmost parameter of the function, since it was pushed last, is accessible at this offset from EBP; the others follow, at successively greater offsets. Thus, in a function such as printf which takes a variable number of parameters, the pushing of the parameters in reverse order means that the function knows where to find its first parameter, which tells it the number and type of the remaining ones.


Für die Parameter habe ich bei nasm noch folg gefunden:



8.1.4 c32.mac: Helper Macros for the 32-bit C Interface

Included in the NASM archives, in the misc directory, is a file c32.mac of macros. It defines three macros: proc, arg and endproc. These are intended to be used for C-style procedure definitions, and they automate a lot of the work involved in keeping track of the calling convention.

An example of an assembly function using the macro set is given here:

proc _proc32

%$i arg
%$j arg
mov eax,[ebp + %$i]
mov ebx,[ebp + %$j]
add eax,[ebx]

endproc

This defines _proc32 to be a procedure taking two arguments, the first (i) an integer and the second (j) a pointer to an integer. It returns i + *j.

Note that the arg macro has an EQU as the first line of its expansion, and since the label before the macro call gets prepended to the first line of the expanded macro, the EQU works, defining %$i to be an offset from BP. A context-local variable is used, local to the context pushed by the proc macro and popped by the endproc macro, so that the same argument name can be used in later procedures. Of course, you don't have to do that.

arg can take an optional parameter, giving the size of the argument. If no size is given, 4 is assumed, since it is likely that many function parameters will be of type int or pointers.



So, meine Fragen nun:

- kann ich einfach ENTER statt push und mov und sub für den Anfang benutzen? Oder gibt es einen Grund dies nciht zu tun.

- Woher weiß ich, wieviel stack space ich reservieren muß für "lokale Var."? (erster Paramter ENTER.)

- Was soll der 2. Parameter bei ENTER und im Zshng mit c.

- Was hat das mit den ebp offsets (s.o.) in + und in - Richtung auf sich?

- Ich habe ja verstanden, daß der return Wert in eax stehen sollte. Im obigen Bsp mit einer Übergabe als pointer, da reicht es doch (um dem c standrad zu genügen) und die verweiste Speicherstelle upzudaten, wenn ich den Wert, auf den der pointer zeigt, ändere?

- Bzgl der nasm Syntax/Makro arg: Woher weiß ich anhand des Quelltextes, was für ein Datentyp es ist? Sehe ich es nur implizit, was ich damit anstelle? Bzw oben steht einfach "integer", woher weiß nasm ob es byte, word oder dword ist?

DarkAvenger
30.07.2004, 12:21
Ach ja, wo ich schon dabei bin: Jemand ne AHnung, wie ich nasm in kdevelop einbinden kann?

i_hasser
30.07.2004, 13:20
So, meine Fragen nun:

- kann ich einfach ENTER statt push und mov und sub für den Anfang benutzen? Oder gibt es einen Grund dies nciht zu tun.

- Woher weiß ich, wieviel stack space ich reservieren muß für "lokale Var."? (erster Paramter ENTER.)

- Was soll der 2. Parameter bei ENTER und im Zshng mit c.

- Was hat das mit den ebp offsets (s.o.) in + und in - Richtung auf sich?

- Ich habe ja verstanden, daß der return Wert in eax stehen sollte. Im obigen Bsp mit einer Übergabe als pointer, da reicht es doch (um dem c standrad zu genügen) und die verweiste Speicherstelle upzudaten, wenn ich den Wert, auf den der pointer zeigt, ändere?

- Bzgl der nasm Syntax/Makro arg: Woher weiß ich anhand des Quelltextes, was für ein Datentyp es ist? Sehe ich es nur implizit, was ich damit anstelle? Bzw oben steht einfach "integer", woher weiß nasm ob es byte, word oder dword ist?



1. das kommt darauf an wie das Stack Frame normalerweise vom C-Compiler angelegt wird. Jeder macht da sein eigenes, ENTER liefert dir den Code für eine bestimmte Variante.
Ob das nun die ist die auch der C-Compiler verlangt weis ich net, hab ENTER bisher noch nie gesehen. Und es spricht auch nix dagegen das nicht zu nehmen, die 10 Byte Code machens dann auch nicht mehr.

2. Wenn du in C in einer Funktion ein "int a" stehen hast, braucht der integer a 4 Byte lokalen Speicher, der normalerweise dann eben vom Stack genommen wird.
Bei selbstgeschriebenen Assemblerroutinen nimmt man idR keine lokalen Variablen, da zu unübersichtlich nur mit dem Offsets zu arbeiten (die hast du dann eben in der Form [EBP+4] oder so).

3. ENTER ist so wie ich das mitbekommen hab doch ein x86 Assembler Befehl (oder? - hab den ehrlich gesagt noch nie gesehen...). Der hat dann nix mit C zu tun.

4. Es gibt den ESP und den EBP. Stack Pointer und Base Pointer.
Ich habs inzwischen leider vergessen, aber die brauchst du beide um lokale Variablen anlegen zu können (musst googeln wie das genau funktioniert).
Ich glaub man hat den Stack Pointer gesichert, den BP auf SP-xyz gesetzt und dann per EBP+x auf die lokalen Variablen zugegriffen.

5. Eigentlich sollte der auf dem Stack liegen, die Register werden zur Argumentübergabe normalerweise nicht genutzt, da jede Arch andere hat.
Es reicht aber wenn du der Funktion einen Pointer übergibst wo diese dann Daten verändern kann.

6. kann ich dir ehrlich auch nicht weiterhelfen, ich hab für sowas nicht die NASM Funktionen genommen sondern die Parameterauswertung etc. selbst geschrieben.

DarkAvenger
30.07.2004, 13:38
Danke schon mal für die Antworten. muß die noch ein paar mal durchlesen, um es zu verstehen. ;)

soviel vorweg:

2) Ich meinte ja, daß du in nasm auch Variablen deklarieren kannst. Mußt du für diese dann stack reservieren? Kann man das nciht einfach haben, daß sich nasm darum kümmert?

3) Guck auf mein Zitat. Ab 186 gibts den Befehl.

i_hasser
30.07.2004, 13:58
Also den Befehl kenne ich tatsache nicht, hab ihn auch noch nicht gesehen - durchaus möglich, wenn der C Compiler zb. lokale Variablen anders anlegt als Intel das beim 186 ersonnen hat.

Mit Nasm hab ich noch keine Variablen deklariert, da kommt einfach ein DB, DW oder DD irgendwohin und damit hat sich das ;).

DarkAvenger
30.07.2004, 14:46
Nur daß ich das verstanden habe: mit DD etwa definiert man sich DWORD *Konstanten*, richtig?

i_hasser
30.07.2004, 15:15
Nein.

Hör auf in einer Hochsprache zu denken, die Zeit ist jetzt vorbei ;D.

Mit DD 0x0a sorgst du dafür, dass an dieser Stelle im Code ein Double Double Word (also ein Quad Word, 8 Byte) mit dem Wert 0xa steht. Also genaugenommen in Hexadezimal 0x000000000000000a (nach Big Endian, 0x0a00000000000000 nach little Endian).

Wenn du jetzt in Nasm sowas schreibst wie "var1 DD 0" wird an dieser Stelle im Code auch nur ein DD mit dem Wert 0 geschrieben. var1 ist ein einfaches Label, und wenn du zb. MOV EAX, [var1] ausführst, wird var1 während des assemblierens in ein einfaches Offset umgewandelt, an dessen Stelle sich zufälligerweise DD 0 befindet ;).


E: Das war jetzt natürlich ausgemachter Unsinn, man wird eben alt.

DD steht für Data Double Word, ist also nur 4 Byte lang. Entsprechend gibts DB (Data Byte), DW (Data Word), DD (Data Double Word) und DQ (Data Quad Word).

DarkAvenger
30.07.2004, 15:21
Ok, das mit der Größe habe ich verpeilt. ;)

Was würde denn passieren, wenn ich versuchte an betreffendes Offset zu schreiben? Gehts das gut oder nicht? Wenn es denn ginge, wozu gibt es dann segment .bss bei nasm? Nur für uninitialisierte (aber reservierte) Speicherbereiche? (Ich versuche das Wort Variable zu vermeiden. ;))

i_hasser
30.07.2004, 15:28
Nein, das mit der Größe hab ich verpeilt ;). Habs oben editiert.

Sitze hier gerade in einem Netten Dachzimmer bei 34°C... also nicht wundern *chatt*.


An das Offset kannst du schreiben, und die Segmente brauchst du bei NASM eigentlich garnicht zu beachten. Das ist nicht für die eigentliche Laufzeit, sondern für die Ausgabedatei die NASM erzeugt - bei zb. ELF (und so ziemlich allen anderen Formaten auch) wird alles in einzelne Bereiche untergliedert, damit beim Ausführen der Datei auch alles an die richtigen Stellen geladen wird. Bei x86 hast du da weniger ein Problem, weil es zb. kein NX Flag gibt (bzw. das nicht verbreitet ist).

Bei anderen Architekturen wird da streng getrennt, und da müsste das "var1 DD 0" in einen Datenbereich, damit du lesen/schreiben kannst.
Dein Code dagegen muss in einen Codebereich, damit du den ausführen kannst.

Wie gesagt, das betrifft aber nur die Ausgabedatei, nicht das Prog selbst zur Laufzeit. In der .ELF steht dann zb. sowas drinnen:

.code -> an offset x laden
.daten -> an offset y laden
.stack -> an offset z laden (wir haben ja einen linearen Speicher, ohne Segmente oder sowas)

entsprechend werden die Pages dann geladen und konfiguriert (code/daten/daten). Wenn in der ELF steht, dass .daten nach 0x100000 geladen wird und var1 das erste ist was in .daten steht, bekommt var1 eben das Offset 0x100000. Und von/auf diese/r Stelle im Speicher kannst du dann lesen/schreiben.

.code dagegen wird in einem Bereich liegen, den du höchstens lesen kannst, aber auf jeden Fall ausführen kannst. Bei IA32 existiert die Trennung nur der Form halber, IA32 kann solche Bereiche nicht in Hardware unterscheiden, wesswegen du da auch in den .code Bereich schreiben kannst.

DarkAvenger
30.07.2004, 15:38
Aha, Mann bin ich froh, daß du da bist, so langsam lichtet sich das Dunkel. :)

So, dann wäre im wentlichen doch nur eine Sache, die ich immer noch nciht ganz gecheckt habe. Wieder zum Bspcode:



_myfunc:
push ebp
mov ebp,esp
sub esp,0x40


DIe 0x40 entsprechen ja dem ersten Paramter bei ENTER. Ich frage mal so: Wann muß ich da einen Wert !=0 angeben? Ich habe jetzt ein paar Bsplistings gesehen und da wurde eigentlich immer ENTER 0,0 übergeben.

Muß ich mir den Stack "reservieren", wenn ich in der assembler Routine etwas auf den drauf pushe? (Wäre irgendwie unlogisch aus meiner Sicht.) Oder wenn der "caller", also die C Funkt lokale Var enthält. (noch unlogischer...) Denn mittlerweile sehe ich nciht, wie ich dennin meiner assembler Routine lok. Var hätte. durch .data und .bss kriege ich irgendwelche Speicherbereiche, Ok, nur in jenen Bsplistings wurde trotz diesen ENTER 0,0 benutzt.

i_hasser
30.07.2004, 15:56
Es gibt statische und lokale Daten. Ist ein wesentlicher Unterschied.

Statische Daten belegen die gesamte Programmlaufzeit über einen Speicherbereich, und verlieren niemals ihren Wert. Wenn du in C in einer Funktion etwas deklarierst ist das nicht statisch, wenn du dazu static angibst wird es statisch gemacht.

Lokale Daten dagegen liegen im Stack, genauer gesagt in einem Bereich der zu Funktionsaufruf "reserviert" und zum Funktionsende wieder "freigegeben" wird.

Also erstmal wie der Stack funktioniert:

Für den Stack wird irgend ein Anfangspunkt festgelegt, meinentwegen xyz. Der ESP (extended Stack Pointer) speichert dabei die Position, wo gerade das Ende vom Stack liegt - am Anfang, wenn der Stack leer ist gilt also ESP=xyz.

Wird nun was auf den Stack gelegt, wächst der "rückwärts" - ESP wird also immer kleiner.

So ungefähr:

; ESP=xyz
PUSH AL
; ESP=xyz-1
PUSH EAX
; ESP=xyz-5
PUSH BX
; ESP=xyz-7
POP EAX
; ESP=xyz-3
PUSH ESP ; ja, das geht auch ;)
; ESP=xyz-7


usw. usf... btw. der 8086 hat bei PUSH SP übrigens einen kleinen Bug ;).


Wie gesagt, dabei bin ich schon etwas eingerostet. Na mal sehen ob ich das zusammenbekomme...
Den alten EBP dürfen wir nicht überschreiben, daher wird der auf den Stack gelegt. Den neuen EPB setzen wir auf das aktuelle Stackende und ziehen vom ESP selbst 40 Byte ab, damit haben wir 40 byte für lokale Variablen (per ESP+1..40).

Wenn du die Variablen in .data oder so packst werden die statisch und nicht lokal ;). Dann kannst du da eine 0 nehmen

DarkAvenger
30.07.2004, 16:02
Ok, wie der stack funktioniert (im Groben) habe ich schon verstanden. Frage ich mal anders herum: Wann brauch ich das in assembler bzw wie kann ich da lokale Daten haben? Hättest du ein kleines bsp listing?

BTW 0x40 != 40 bytes. ;) Ja ja, die Hitze. ;D

i_hasser
30.07.2004, 16:37
:P

Das stimmt natürlich, 0x40=64 Bytes.

Lokale Daten, tja, kannst du einfach haben.

Wenn du eben mal mehr Daten brauchst kannst du statt statischer Daten oder statt Registern eben auch lokale Daten nehmen.
Das musst du schon selbst entscheiden, was du nimmst ;). Wenn du zb. für eine Init-Routine 1MB Speicher brauchst wirst du den sicher lokal wählen wollen, weil der sonst dauerhaft vergeben ist. So wird der Speicher nur beim Funktionsaufruf benötigt.

Von der Sache her fragst du gerade, ob du nun "int a" oder "static int a" nehmen sollst ;). Praktisch gibts nur in seltenen Fällen einen Unterschied, in Assembler sind statische Daten besser lesbar wohingegen in C lokale Variablen sauberer sind.

Also du könntest eben sowas machen:



push ebp
mov ebp,esp
sub esp,0x40

; tu was kompliziertes

; Speichere Ergebnis
MOV [ESP+4], EBX

; tu nochwas kompliziertes was alle Register braucht

; rechne mit altem Ergebnis weiter
MOV EBX, [ESP+4]



Wenn du die Funktion verlässt sind die Daten auf jeden Fall ungültig.

DarkAvenger
30.07.2004, 17:07
Naja, so ganz habe ich das immer noch nicht gecheckt. dein mov [esp+4], ebx ist doch äquivalent zu push ebx, oder? Was würde denn passieren, wenn ich das sub nciht vor dem push durchführen würde? Bei etl. Routinen sehe ich ja ein pusha, obwohl enter 0,0 aufgerufen wird. Naja, ich werde mal weite nach code umsehen, wo enter x,0 benutzt wird. Evtl geht mir dann ein Licht auf. ;) Ich denke im Moment komme ich auch so ganz gut aus.

Ne andere Frage: Solange ich auf den stack nicht zugreife, kann die Werten von ebp und esp einfach etwa in einem .data Bereich auslagern und dann mit zwei weiteren Registern hantieren (und dann wieder alles zurückhole)?


Noch ne andere Frage: Unter welcher IDE entwickelst du assembler? Ich kriege das wie gesagt mit kdevelop nciht so ganz hin. :(

i_hasser
30.07.2004, 19:01
Ach ich ...

Hätte natürlich [EPB+...] heißen müssen :P

Das ist ja gerade der Vorteil, denn so kann man trotzdem Daten auf dem Stack ablegen - EBP verändert sich nicht, wohingegen sich ESP ja verändert.
Und der Stack fängt auch nach den lokalen Variablen an, kommt mit denen also auch net ins Gehege.

i_hasser
30.07.2004, 21:34
Also ich blick im Moment auch nicht mehr so durch, ist wie gesagt schon etwas her.

DarkAvenger
31.07.2004, 11:21
Naja nicht weiter schlimm. Hast mir ja schon gut weitergeholfen und werde mal sehen wie weit ich komme. :)

DarkAvenger
01.08.2004, 14:01
OK, meine ersten Gehversuche. Habe mich mal an OpenAL versucht und eine Funktion in assembler umgewandelt und es geht auch. Kannst ja mal gucken und sagen, was ich besser machen könnte. Ich habe zwei Varianten für die Schleife. Ich denke die zweoite wird besser sein.

original


void _alChannelify2Offset( ALshort *dst, ALuint offset,
ALshort **srcs, ALuint size ) {
ALshort *src0 = &srcs[0][offset / sizeof *srcs];
ALshort *src1 = &srcs[1][offset / sizeof *srcs];
ALuint k;

size /= sizeof *dst;

for( k = 0; k < size; k++ ) {
dst[0] = src0[k];
dst[1] = src1[k];

dst += 2;
}

return;
}


meine Assembler Funktion:


%include "c32.mac"

proc _alChannelify2Offset

%$dst arg(4)
%$offset arg(4)
%$srcs arg(4)
%$size arg(4)

push esi
push edi
push ebx

mov eax, [ebp + %$offset]

mov ebx, [ebp + %$srcs]
mov ecx, [ebx]
add ecx,eax ;source 0 start
add ebx, 4
mov edx, [ebx]
add ecx,eax ;source 1 start

mov ebx, [ebp + %$size]
shr ebx, 1

xor esi, esi

mov edi, [ebp + %$dst]

.loop
;original loop
;mov ax, [ecx+esi*2]
;mov [edi], ax

;mov ax, [edx+esi*2]
;mov [edi+2], ax

;modified version
mov ax, [edx+esi*2] ;source1 will be in high word
shl eax, 16

mov ax, [ecx+esi*2]
mov [edi], eax

add edi, 4
inc esi

cmp esi,ebx
jne .loop
;loop_end

pop ebx
pop edi
pop esi

endproc

i_hasser
01.08.2004, 14:25
Hab nur mal kurz drübergelesen, heute Abend schau ich mir das vielleicht noch genauer an. Versuch so oft wie möglich 32bit Register und 32bit Speicherzugriffe zu nehmen, 16bit Zugriffe sind inzwischen langsamer als 32bit Zugriffe.


Hmmm... die Funktion kopiert ja src nur nach dst, und das wars.

Einfachste Variante wäre die Stringbefehle zu benutzen, das wird zwar inzwischen nicht mehr so gerne gesehen weil die auf Byte-Basis arbeiten aber da brauchst du nur einen einzelnen Befehl.

Ich weis dummerweise nicht mehr welcher das war *buck*, aber das sollte dann so ungefähr aussehen:



LEA EDI, [src]
LEA ESI, [dst]
MOV ECX, [k]
STOSB EDI, ESI


STOSB (glaub das war er, könnte sein, dass es da inzwischen auch 32bit Entsprechungen gibt, glaub mal was von STOSW gelesen zu haben) läuft solange bis ECX gleich 0 ist.

Eine alternative wäre auch REP. Hab aber gerade meine Assemblerlektüre nicht zur Hand, kann also nicht nachgucken wie der Syntax ist (Assembler vergess ich irgendwie immer :]).

DarkAvenger
01.08.2004, 14:50
Nee, ist leider keine einfach Kopierfunktion. Dachte ich beim ersten mal auch. Die Funktion erzeugt aus 2 streams einen interleaved stream.

Das mit den 32bit Zugriffen hatte ich schon gedacht, darum denke ich, daß meine 2. Variante besser sein wird.

i_hasser
01.08.2004, 16:13
Achso. Wie groß sind denn die einzelnen Elemente? Da guck wir mal ROR/ROL an, damit bekommst du das sicherlich schneller und besser hin.

DarkAvenger
01.08.2004, 16:20
??? Ich wüßte nicht, wie mir Rotation der bits da weiterhelfen sollte?

i_hasser
01.08.2004, 22:23
Hmm weis ich jetzt ehrlich gesagt auch nicht... es war wieder warm heute ;)

Mir ist aber noch eine gute Lösung eingefallen, komplett mit 32bit Zugriffen.



MOV EAX, [src0]
MOV EBX, [src1]
MOV ECX, EAX
MOV EDX, EBX

SHR EAX, 16
SHL ECX, 16
SHR EBX, 16
SHL EDX, 16

OR EAX, EDX
OR EBX, ECX

MOV [dst], EAX
MOV [dst+4], EBX


Da hast du in einem Rutsch gleich 8 Byte interleaved, mit 16bit Blöcken. Falls irgendwelche Fehler drinnen sein sollten ist das kein Wunder, mir brummt der Schädel ein bisschen.

DarkAvenger
01.08.2004, 22:56
Ich habe deines noch nciht ganz durchdacht, aber ich hatte schon an was ähnliches gedacht. Den Brummschädel habe ich heute auch. ;) Nur müßte ich bei meinen Überlegungen ein zusätzliches Register bemühen und auch zwichen gerade und ungerader size unterscheiden. Deines ist aber ganz 32bit, was gut sein sollte. Mal gucken...

OK, deines müßte klappen, allerdings sind die Register bei dir anders organisiert und müßte verstärkt auf den Speicher zurückgreifen. Bei deiner Lsg müßte ich etwa die offsets von src und dst in den Speicher schreiben, während es bei mir in den Registern bleibt. Man müßte mal messen, was schneller ist.

i_hasser
02.08.2004, 16:24
Na wie du die Adressen in die Register bekommst musst du dir noch überlegen, war nur ein Beispiel.

2 könntest du in ESI und EDI speichern, die 3. Adresse vielleicht in einem FPU Register (oder du schaltest vorher auf MMX um, dann ab damit ins MMX Register).

DarkAvenger
02.08.2004, 17:48
Werde mal gucken, wie ich deine Variante reinmache... Habe aber mal meine gemessen und eine interessante Beobachtung gemacht:

Meine zweite Variante ist in der Tat schneller als die erste, wenn auch nur knapp, und die erste ist etwas schneller als optimal übersetzter c-code.



mov (e)ax, [edx+esi*2] ;source1 will be in high word, using eax is faster than ax!
shl eax, 16

mov ax, [ecx+esi*2]
mov [edi], eax


Doch nun die Überraschung: Ändere ich das erste mov ax,.. in mov eax,... bin ich 10% schneller! Wenn ich auch das zweite ändere werde ich wieder langsamer (auch ist das Ergebnis dann falsch...). Aber, wenn ich nur das zweite ändere, bin ich wieder so schnell (wenn auch natürlich falsches Ergebnis).

Hast du eine Erklärung hierfür?

[edit] Es scheint, als daß der speed-up vom übergebenen offset abhängt...

Noch eine Sache. In OpenAL hat es funkt, in meinem Testprog nicht Ich mußte fogl abändern:

mov edx, [ebx+eax]

zu

mov edx, [ebx] ;source 1 start
add edx, eax

Warum ist das nicht äquivalent? Ich dachte ich hätte das mit indirekter Adressierung zzgl offset verstanden...

i_hasser
02.08.2004, 19:38
Den ersten Teil muss ich mir mal in Ruhe überlegen, der 2. Teil ist klar.

Sagen wir EAX=1, EBX=2.

Dann ergibt MOV EDX, [EBX+EAX] ein MOV EDX, [3] - also das Double Word von Offset 3 wird gelesen, EDX ist danach also [3] (speicherinhalt bei 3).

Wenn du aber MOV EDX, [EBX] berechnest ergibt das EDX=[2], und danach rechnest du nochmal EAX=1 drauf, also ist EDX=[2]+1.

Je nachdem was an den Offsets steht ergibt das verschiedene Ergebnisse.

DarkAvenger
02.08.2004, 20:08
Ah, OK, da habe ich was durcheinandergebracht...

Übrigens, in deinem Bsp ist doch ein Fehler, da die Kanäle nciht richtig interleaved werden.

Ich habe übrigens einen bessere Idee, statt mmx Register direkt mmx zu benutzen: PUNPCKLWD und PUNPCKHWD eignen sich hierzu perfekt. Muß mal meine ersten Erfahrungen mit mmx sammeln. :)

DarkAvenger
02.08.2004, 21:06
SO, die erste mmx Variante ist fertig (naja die Grenzen müßten noch richtig überprüft werden...), wenn auch nur etwa 15%-20% schneller:



PREFETCH [ecx+esi*4+32]
movd mm0,[ecx+esi*4]
PUNPCKLWD mm0,[edx+esi*4]
movq [edi+esi*8],mm0


Ich denke, wenn ich per movq übertrage bin ich schneller, aber muß dann in den Registern umherschieben...

Tja, doch nciht. Grrr. hier meine zweite Variante, die nich wirklich schneller als obiges ist: (Zähler etc natürlich angepaßt...)



PREFETCH [ecx+esi*4+32]
PREFETCH [edx+esi*4+16]
movq mm0,[ecx+esi*4]
movq mm1,mm0
movq mm2,[edx+esi*4]
movq mm3,mm2

PUNPCKLWD mm0,mm2
movq [edi+esi*8],mm0

PUNPCKHWD mm1,mm3
movq [edi+esi*8+8],mm1


Ich bin übrigens nicht sicher, ob ich den prefetch Befehl richtig verstanden habe...

So, habe nochmal für kleinere Größen (size Paramter) gemessen, wie die bei OpenAL vorkommen, und da sind meine mmx Varianten doch mehr als doppelt so schnell wie x86 code. :)

i_hasser
05.08.2004, 00:04
So, jetzt hab ich wieder Zeit und Lust ;D

Werd mir mal bis morgen Abend eine ordentlich optimierte Variante einfallen lassen.

Kannst du mir da mal deinen Funktionsheader usw. geben? Bin zu faul das alles nochmal nach Doku abzuschreiben ;).

DarkAvenger
05.08.2004, 00:18
Welchen Funktionsheader meinst du? Auf Seite 1 findest du den kompletten x86 code. Habe übrgnes schon ne mmx Version für 4 Kanal interleavign gebastelt und die bracuht immerhin nur 66% Zeit vom optimiert kompiliertem C.

i_hasser
05.08.2004, 10:03
Den C-Code dazu. Also wie der Funktionsprototyp aussieht.

Ach ja, und die c32.mac - scheint bei meinem Slackware irgendwie zu fehlen :]

EDIT: Hat sich erledigt, hab mir einfach nochmal das NASM Paket runtergeladen.

DarkAvenger
05.08.2004, 10:29
Naja, die c Funktion hatte ich doch auch gepostet oder? Egal:


void _alChannelify2Offset( ALshort *dst, ALuint offset,
ALshort **srcs, ALuint size );

Und die (primitven) Makros:


; NASM macro set to make interfacing to 32-bit programs easier -*- nasm -*-



%imacro proc 1 ; begin a procedure definition

%push proc

global %1

%1: push ebp

mov ebp,esp

%assign %$arg 8

%define %$procname %1

%endmacro



%imacro arg 0-1 4 ; used with the argument name as a label

%00 equ %$arg

%assign %$arg %1+%$arg

%endmacro



%imacro endproc 0

%ifnctx proc

%error Mismatched `endproc'/`proc'

%else

leave

ret

__end_%$procname: ; useful for calculating function size

%pop

%endif

%endmacro

DarkAvenger
05.08.2004, 10:30
Tja, das kam 2 Minuten zu spät. ;)

i_hasser
05.08.2004, 10:39
Stimmt, da steht er ja schon.

Der ist ja nicht gerade übersichtlich (find ich). Na ich nehm mal einen anderen Header.

DarkAvenger
05.08.2004, 10:43
Naja, ich wollte die OpenAL ABI nicht verändern...

i_hasser
05.08.2004, 10:55
Da kannst du dann in C einen Wrapper schreiben, aber in Assembler sollte man wirklich nur das machen was unbedingt nötig ist. Da kommt zu schnell ein Fehler rein, und - aus Erfahrung - kann ich dir sagen, Debuggen in Assembler macht überhauptkeinen Spaß ;)

Andererseits verteilen wir die Funktion dann auch wieder, was eben auch Blöd ist. Am besten wäre da Inline Assembler, da geht aber nur der Gnu Syntax :(

DarkAvenger
05.08.2004, 11:12
Ja, jetzt verstehst du ja, vor welchem Problem ich stand. :) das mit dem debuggen habe ich schon bemerkt. Wie machst du daS? Ich habe mich mit extern printf beholfen....

Ich fande es aber in nasm nicht so schlimm die C Parameter zu übernehmen. Dank des arg Makros verliert man nicht so schnell den Überblick.

i_hasser
05.08.2004, 11:21
So, meine erste Variante ist jetzt auch fertig geworden :).

Hab leider keine Vergleichswerte, da ich mich an deinem Header erst garnet versucht hab *buck*
Du kannst ja mal den C-Code posten, der bei dir die Assemblerroutine testet - dann schreib ich einen Wrapper für meine Funktion.

Er braucht zwischen 9ms und 10ms um ein Interleave aus 2 Listen zu erzeugen, die eine Länge von je 18MB haben. Ist dummerweise noch ein klitze kleiner Sync-Fehler drinnen (betrifft nur den ersten und letzten Wert) - aber den mach ich nachher raus.

Der Speicherdurchsatz wäre mal interessant... hmm... also 72MB in 9ms - das macht 8GB/s Durchsatz. Nicht schlecht :). Komisch, dabei hat mein Rechenknecht nur 384kB Cache. Das Ergebnis stimmt aber *noahnung*.



%include "c32.mac"

PROC _alChannelify2Offset2

%$src0 arg(4)
%$src1 arg(4)
%$dst arg(4)
%$len arg(4)

;----------
; Init Code
;----------

; Save old Regs

PUSH ESI
PUSH EDI
PUSH EBX

; Read Pointers
; src0 -> EDI
; src1 -> ESI
; dst -> EDX

MOV EDI, [EBP + %$src0]
MOV ESI, [EBP + %$src1]
MOV EDX, [EBP + %$dst]

; Set Counter

MOV ECX, [EBP + %$len]

;------------------------
; Start Interleaving Loop
;------------------------
.DO_INT

; Load Data

MOV EAX, [EDI]
MOV EBX, [ESI]

; Interleave

ROR EBX, 16
XCHG AX, BX
ROR EAX, 16

; Store Data

MOV [EDX], EAX
MOV [EDX+4], EBX

; Move Pointers

LEA EDI, [EDI+2]
LEA ESI, [ESI+2]
LEA EDX, [EDX+4]

; Loop around

DEC ECX
JNZ .DO_INT

;------------
; Return Code
;------------

POP EBX
POP EDI
POP ESI

ENDPROC


Und das ist die auf die Schnelle gehackte Testfunktion



#include <stdlib.h>
#include <time.h>

// prototype
extern _alChannelify2Offset2(short int* src0, short int* src1, short int* dst, int len);

int main()
{
// Create Strings
short int *src0, *src1, *dst0;
int size;
int i;

clock_t t1, t2;

size=9000000;

src0=malloc(sizeof(short int)*size);
src1=malloc(sizeof(short int)*size);
dst0=malloc(sizeof(short int)*size*2);


for(i=0;i<size;i++)
{
src0[i]=(i%65536);
src1[i]=(size-i-1)%65536;
}

t1=clock();
_alChannelify2Offset2(src0,src1,dst0,size);
t2=clock();

printf("\n %.2f ms needed", (double)(t2-t1)/(double)CLOCKS_PER_SEC);

// print output
//for(i=0;i<99;i++) printf("\n A: %ld, B: %ld", dst0[i*2], dst0[i*2+1]);

printf("\n");
return 0;
}


EDIT Fehler gefunden, da muss noch ein zusätzliches DEC ECX rein (vor die Schleife, bei C zählen wir ja von 0 bis kleiner maxWert, bei Assembler zählen wir =maxWert bis =0, was 1 mehr ist).


Ach auch nicht... hab jetzt keine Lust den Fehler zu suchen ;).

DarkAvenger
05.08.2004, 11:48
ICh werde bei Gelegenheit mal deinen code testen. Ich lese in miener ROutine nur ein paar WAV Werte ein, und lasse ein paar Tausend mal einen Bereich von etwa 4kb durch die Routine jagen. (Sind in etwa die Größen, die auch OpenAL benutzt, muß noch mal nachgucken).

Eine Frage zu deinem Code:

Welchen Vorteil hat es LEA zu benutzen? Ich bin ja ohne den Befehl ausgekommen.

LEA EDI, [EDI+2]

ist doch dasselbe wie

ADD EDI, 2

oder?

Wenn nicht, frage ich mich ob der LEA code sogar falsch ist?

i_hasser
05.08.2004, 11:57
Nö, LEA ist genau das Selbe. Müsste man schauen was schneller ist, LEA wird nämlich von den AGUs und nicht von den Integereinheiten abgearbeitet (zumindest beim K7).

Das Ergebnis ist genau das selbe. Der LEA Syntax verwirrt immer ein bisschen - Load Effective Adress - die eckigen Klammern um den 2. Operanden sprechen ja erstmal für einen Speicherzugriff, aber LEA lädt dir nur das Offset von den Daten an dieser Position.

DarkAvenger
05.08.2004, 12:01
Hmm, habe mal deinen code bei mir reingefriemelt, aber bekomme einen segfault... Mal gucken...

OK, es geht jetzt, allerdings bin ich mir nicht sicher, ob ich nicht nur size/2 habe dir messe. Müßte mal genauer gucken.

Was hast du für eine Maschine??? Bei mir sind deine und meine x86 Routine gleich schnell, brauchen 40ms (wenn ich deine ROutine auch wirklich mit gleich viel Arbeti beauftragt habe...s.o.). (edit) Meine mmx Routine ist hier doch nicht meßbar schneller.

Ok, nee ist schon alles richtig. Unsere x86 ROutinen sind gleich schnell.

Ahc ja, ich habe zur Zeit noch ein paar Hintergrundprozesse laufen. Evlt ist daher meine Kiste etwas langsamer als die es sein sollte.

Also, wenn ich 10x die ROutine aufrufe ist mein x86 code minimal schneller: 290ms vs 310ms.
Die mmx Routine bräuchte hier etwa 260ms. Naja, umso mehr Daten gefüttert werden, umso langsmaer wird die mmx. Das prefetching oder so müßte verbessert werden. Wie gesgt, bei einigen kb an Daten schlägt die mmx Routine den x86 code haushoch.

i_hasser
05.08.2004, 12:18
clock() misst nur die Zeit, die der Prozess auch wirklich in Anspruch genommen hat.

Ansonsten musst du mal das hier nehmen:



#define RDTSC(llptr) { \
__asm__ __volatile__ ( \
"rdtsc" \
: "=A" (llptr) \
); }


Der ließt den TimeStamp Counter der CPU, damit kannst du einzelne Takte messen (also überaus genau ;)). Die Zeit bekommst du dann einfach über die Taktrate. Die Messung mit clock() ist nämlich manchmal ziemlich ungenau, frag aber nicht wieso.

In meinem Rechenknecht werkelt ein TbredB auf 2.2GHz, dazu ein Nforce2 Brettl mit 2*512MB PC400 Ram im DC.

Ach ja, RDTSC musst du dann einen Pointer auf einen long long int übergeben, der TimeStamp ist nämlich 64bit breit.

DarkAvenger
05.08.2004, 12:24
Hmm unsere HW ist fast identisch, also liegt es doch am Muli... ;) (Wird wohl den Speicher belasten und dadurch das caching weniger effizient für das Testprog machen. Werde auch mal später ohne externe Last messen.)

Mißt der timestamp nur den eigenen Prozeß oder nur "absolute" Takte?

i_hasser
05.08.2004, 12:27
Nur absolute Takte (der TimeStamp gibt dir an, wie viele Takte die CPU seit dem Start absolviert hat). Was für ein OS hast du denn? So funktioniert das erstmal nur mit dem GCC, aber auch mit den Win32 Ports.

DarkAvenger
05.08.2004, 12:29
Linux

i_hasser
05.08.2004, 12:39
Ach so, na dann ist ja alles ok ;D

PuckPoltergeist
06.08.2004, 12:05
DarkAvenger:

Spielst du jetzt nur zum Spass mit Assembler rum, oder willst du das wirklich irgendwo einsetzen? Bei heutigen Maschinen ist handoptimierter Code doch weitgehend überflüssig und zumeist noch kontraproduktiv. ???

i_hasser
06.08.2004, 12:45
Schau dir mal Prime95 an...

DarkAvenger
06.08.2004, 23:48
@Puck

Mir geht es nicht primär um reinen x86 assembler, auch wenn ich den dann einsetzen will, wenn C nicht sinnvoll nutzbar ist (siehe thread mit loop unrolling), sondern um SIMD. Das können die compiler ja nicht sinnvoll umsetzen. Auch wenn es ja vector extensions oder intrinsincs (oder so) gibt, ist die performance weit weg von richtigem mmx, sse, 3dnow assembler.

Aber selbst mir reinem x86 code kann man compiler schlagen, wenn man vorher gründlich überlegt.

PuckPoltergeist
09.08.2004, 10:59
Original geschrieben von DarkAvenger
@Puck

Mir geht es nicht primär um reinen x86 assembler, auch wenn ich den dann einsetzen will, wenn C nicht sinnvoll nutzbar ist (siehe thread mit loop unrolling), sondern um SIMD. Das können die compiler ja nicht sinnvoll umsetzen. Auch wenn es ja vector extensions oder intrinsincs (oder so) gibt, ist die performance weit weg von richtigem mmx, sse, 3dnow assembler.

Das lohnt meist nicht. Zumindest was SSE(2) angeht, weiß ich das aus Erfahrung, und MMX/3DNow! dürfte da nicht wesentlich anders sein. Der Aufwand ist für SIMD nicht unerheblich, und die meisten Anwendungen profitieren nicht genügend davon. Für AMD-Prozessoren kann man auf Vektoroperationen mit SSE(2) gleich ganz verzichten, damit holt man kaum was raus gegenüber skalaren Operationen. Es kommt am ende mehr bei rum, wenn man da den Compiler SSE nutzen läßt, und sonst nicht weiter optimiert.



Aber selbst mir reinem x86 code kann man compiler schlagen, wenn man vorher gründlich überlegt.

Sicherlich, handoptimierter Code dürfte da nahezu immer gewinnen. Es stellt sich halt nur die Frage, ob das den Aufwand rechtfertigt. Das Aufwand-Nutzen-Verhältnis stimmt meist nicht. Da macht es mehr Sinn, den Code anderwertig nach Engstellen zu untersuchen und zu optimieren (passende Datenstrukturen nutzen, unnötiges kopieren oder Systemaufrufe vermeiden...).

DarkAvenger
09.08.2004, 11:18
Ob deine AUssagen stimmen, werde ich in den nächsten Wochen ja selbst verifizieren können. ;)

PuckPoltergeist
09.08.2004, 11:29
Wäre fein, wenn du den entsprechenden Code dann auch posten könntest. Würde mich interessieren, inwiefern sich der Assembler-Teil von den instrinsics unterscheidet.

DarkAvenger
09.08.2004, 11:33
Wenn der code brauchbar wird, werde ich wohl einen OpenAL patch zur Verfüfung stellen.