Hallo,
ich wundere mich etwas über die Zeit, die meine Funktionen zur
Ausführung brauchen. Ohne den Quellcode zu kennen ist es natürlich
schwer eine Aussage zu machen, aber mir geht es nicht um ein paar
Prozentpunkte sondern darum, ob ich etwas so gravierend falsch gemacht
habe, dass die Ausführzeit beispielsweise doppelt so hoch ist wie
erreichbar.
Als Beispiel meinen ultra light weight TCP/IP Stack der folgendes macht:
- ein paar Felder in den Protokoll Headern setzen (DIX, IP, UDP)
- UDP und IP checksums berechnen (dauert wohl am längsten)
- Header in den TX Buffer kopieren (Daten von 444 Byte sind schon drin).
Das ganze braucht 140us. Der Prozessor Takt ist 180MHz, die Master Clock
90MHz. Es ist ein AT91SAM9260 MCU mit SDRAM. DCache ist für die TX
Buffer nicht aktiviert.
Nun meine Fragen:
Hat jemand Erfahrung mit anderen TCP/IP Stack auf einem ähnlichen
Prozessor und kann mir etwas über die Dauer sagen? Vielleicht hat ja
auch jemand einen Link oder Buchtipp, wo ich etwas über die Ausführzeit
von Funktionen und Algorithmen lernen kann oder muss ich selbst durch
messen und ausprobieren ein Gefühl dafür entwickeln? 140us kommt mir
ziemlich lang vor und es ist schon sehr hilfreich, wenn man von
vornherein Zeiten abschätzen kann.
Ich freue mich auf Tipps und Hinweise!
Viele Grüße
Daniel
Ich habe nun nochmal den Checksum algorithmus überarbeitet und dabei die
Tatsache genutzt, dass man die berechnung von big endian daten auch in
little endian durchführen kann. somit ist die routine nun fast doppelt
so schnell (89us). Kommt mir aber immer noch ziemlich lang vor.
Zuvor habe ich die 16bit wörter in little endian umgewandelt zur
berechnung.
hier der code meiner UDP-checksum routine:
1
unsignedshorteth_chksum_udp(structPD_typ*RxPd)
2
{
3
4
intwords;
5
inti;
6
longsum=0;
7
unsignedshort*ptr;
8
union{
9
unsignedcharu8[20];
10
unsignedshortu16[10];
11
unsignedlongu32[5];
12
}header;
13
14
/*
15
* assemble pseudo header
16
*/
17
/* IP addresses */
18
header.u32[0]=RxPd->ip_header->srcIP;
19
header.u32[1]=RxPd->ip_header->destIP;
20
/* append zero-byte */
21
header.u8[8]=0;
22
/* protocol-ID */
23
header.u8[9]=RxPd->ip_header->prot;
24
/* UDP length field */
25
header.u16[5]=RxPd->udp_header->length;
26
27
/*
28
* copy udp-header
29
*/
30
31
memcpy(header.u8+12,RxPd->udp_header,6);
32
33
/* zero udp-checksum field */
34
header.u16[9]=0;
35
36
/*
37
* calculate checksum
38
*/
39
40
/* calculation for pseudo and udp header */
41
for(i=0;i<10;i++){
42
sum+=header.u16[i];
43
sum+=(sum&0x10000)>>16;
44
sum=sum&0xffff;
45
}
46
47
/* calculate 16-bit words in UDP payload */
48
words=((endians(RxPd->udp_header->length))-8)/2;
49
50
/* set payload pointer */
51
ptr=(unsignedshort*)RxPd->PMI_prot;
52
53
/* calculation for UDP payload */
54
for(i=0;i<words;i++){
55
sum+=ptr[i];
56
sum+=(sum&0x10000)>>16;
57
sum=sum&0xffff;
58
}
59
60
/* if the length is odd, append last byte and a zero-byte */
Sieh Dir doch einmal den Ass.-Code an, der vom Compiler erzeugt wird. Da
erkennt man am besten, was nicht optimal ablaufen könnte.
Ausdrücke wie: sum += ptr[i]; können u.U. uneffizient sein, wenn ptr nur
hier gebraucht wird. ptr++ wird ggf. schneller ausgeführt.
Ich weiß nicht, wie groß 'int i' ist. 16bit oder 32bit? Nimm immer den
Datentyp, den der Prozessor intern verwendet; das sollte hier 32bit
sein. Andernfalls muß bei jeder Operation ggf. erst auf 32bit erweitert
werden.
Nutze internes RAM solange es geht; externe Speicherzugriffe brauchen
eine Ewigkeit. Dein Stack liegt hoffentlich auch im internen RAM?
Und selbst wenn alles optimal übersetzt ist: auch ein 200MHz µC braucht
seine Zeit für die Programmabarbeitung.
Hallo Daniel
Daniel G. schrieb:
> Ich habe nun nochmal den Checksum algorithmus überarbeitet und dabei die> Tatsache genutzt, dass man die berechnung von big endian daten auch in> little endian durchführen kann. somit ist die routine nun fast doppelt> so schnell (89us).
Aber die zuerst genannten 140µs waren doch die Zeit, die "das ganze"
(header, check sum, copy) gedauert hat, wenn ich Dich recht verstehe.
Die Routine liest die Daten, kopiert sie aber nicht. Wenn das woanders
nochmal gemacht wird kostet es viel Zeit -- besonders wenn die Daten
nicht im Cache liegen. Aber ich habe gerade etwas den Überblick
verloren, welche Deiner Daten als non-cacheable markiert waren :-)
Soweit ich das sehe, kannst Du die Prüfsumme auch für 32bit Worte
bilden und am Ende auf 16bit zusammenfalten. In beiden Fällen hilft
inline Assembler, mit dem man Zugriff auf das Carry Flag hat. Ist
schon einige Takte flotter als das was man mit einem C compiler
hinbekommt. Bevor Du dort optimierst, solltest Du allerdings unbedingt
die Speicherzugriffe überprüft haben.
Weitere Tips: http://tools.ietf.org/html/rfc1071#section-2
Gruß
Marcus
http://www.doulos.com/arm
Danke für die Antworten!
das was am gesamten TCP/IP Stack mit EMAC Treiber überhaupt nennenswerte
Zeit in Anspruch nimmt, ist allein die UDP checksum routine. Der Rest
braucht nur wenige us.
Die Daten auf die die checksum routine zugreift ist weder cached noch
buffered, nämlich der EMAC TX buffer Bereich des AT91SAM9260.
Ich weiß noch nicht, in wie weit es Sinn macht, in die Optimierung Zeit
zu stecken (weil die Latenz möglicherweise schon klein genug ist), aber
das mit dem zusammenrechnen der 32-bit wörter werde ich mal probieren!
Auch werde ich mir mal den Assembler code ansehen.
Ich schreibe das Ergebnis, sobald ich es habe!
Viele Grüße,
Daniel
Daniel G. schrieb:
> Die Daten auf die die checksum routine zugreift ist weder cached noch> buffered, nämlich der EMAC TX buffer Bereich des AT91SAM9260.
Damit wird jeder Zugriff bestenfalls einen Buszyklus dauern. Wären die
Daten cacheable, würden die Daten der folgenden Schleifeniteration
durch den line-fill dem Core bereits vorliegen sobald dieser sie
benötigt. Können wir davon ausgehen, dass ein Block à acht Worte
(32bit) in diesem Bereich nicht mehr verändert wird, während darauf
lesend zugegriffen wird?
> Ich weiß noch nicht, in wie weit es Sinn macht, in die Optimierung Zeit> zu stecken (weil die Latenz möglicherweise schon klein genug ist), aber> das mit dem zusammenrechnen der 32-bit wörter werde ich mal probieren!> Auch werde ich mir mal den Assembler code ansehen.
Versuchs erstmal mit dem Speicher. Wenn Du dann noch Spaß hast, weiter
zu optimieren kannst Du Dich mit dem Rest beschäftigen.
Gruß
Marcus
Marcus Harnisch schrieb:
> Weitere Tips: http://tools.ietf.org/html/rfc1071#section-2
Habe anhand des Dokuments mal meinen Code überarbeitet. Suche seit
gestern unermüdlich den Fehler: Die Checksumme wird nur für manche
Pakete richtig berechnet und teilweise nur genau jedes Zweite mal.
Konnte leider keine weiteren "Regeln" finden.
Vielleicht kann ja mal jemand in den Code schauen:
Daniel G. schrieb:
> Vielleicht kann ja mal jemand in den Code schauen:
Beim Überfliegen ist mir erstmal nichts aufgefallen.
Allerdings kann man das ganze auch elegant mit einem 64bit Akkumulator
in C implementieren. Das obere Wort summiert die Carries (deferred
carry). Ein guter Compiler setzt das so um, dass Du Dir dieses
scheußliche GCC-Inline Assembler Geraffel sparen kannst. Am Ende high
und low word addieren, und dann noch mal high und low half-word. Oder
habe ich hier einen Denkfehler gemacht?
Das deferred Carry hat noch einen anderen Vorteil: Die Abhängigkeiten
zwischen den Iterationen verschwinden. Damit kann das auf performanteren
CPUs unter Ausnutzung der SIMD Einheiten vektorisiert werden. Für den
Cortex-A9 übersetzt (mit 32 Bit Akku) sieht das schon ganz lecker aus.
Aber viel wichtiger: DEN CACHE EINSCHALTEN! Da es um Lesezugriffe geht,
reicht ja WT Konfiguration.
Gruß
Marcus
Marcus Harnisch schrieb:
> Allerdings kann man das ganze auch elegant mit einem 64bit Akkumulator> in C implementieren. Das obere Wort summiert die Carries (deferred> carry). Ein guter Compiler setzt das so um, dass Du Dir dieses> scheußliche GCC-Inline Assembler Geraffel sparen kannst. Am Ende high> und low word addieren, und dann noch mal high und low half-word. Oder> habe ich hier einen Denkfehler gemacht?
Vielen Dank für den guten Tipp! Ist GCC denn einer dieser guten
Compiler? :-) Wie du es sagst, müsste es gehen, so habe ich den
Algorithmus auch verstanden.
Ich bin den gesamten Assembler code instruction für instruction
durchgegangen und alles ist genau so wie ich es wollte. Vielleicht ist
doch etwas an meinem Algorithmus falsch:
1. Die Wörter (32bit) zusammenrechnen und nach jeder addition das carry
bit, wenn vorhanden, aufaddieren.
2. beide halbwörter addieren und carry dazu addieren.
3. invertieren
Oder funktioniert das mit dem carry aufaddieren im asm code nicht wie
ich denke?
Um caches kümmere ich mich danach. erstmal diese Baustelle beenden.
Vielleicht ist es sinnvoll den Titel zu ändern und zumindest "UDP
checksum" mit reinzunehmen, wegen der suchfunktion.
Vorschlag: "UDP checksum Geschwindigkeit auf ARM9"
Kann das mal ein Admin bitte machen?
Daniel G. schrieb:
> Ist GCC denn einer dieser guten Compiler? :-)
Bei oberflächlicher Inspektion sieht der erzeugte Code OK aus. Für
tiefergehende Untersuchungen hab ich gerade keine Zeit.
Gruß
Marcus
Habe den Fehler noch finden können: Die Daten auf die ich 32-bit
Wort-weise zugreifen wollte waren nicht aligned :-| Wenn C durch
Assembler ersetzt wird, ist das natürlich fatal. Wie konnte mir das nur
passieren?!?!?!
Werden den Code nun überarbeiten und nur halbe Wörter mit deferred carry
zusammenzählen. Melde mich dann wieder!
Viele Grüße
Daniel
Daniel G. schrieb:
> Habe den Fehler noch finden können: Die Daten auf die ich 32-bit> Wort-weise zugreifen wollte waren nicht aligned :-| Wenn C durch> Assembler ersetzt wird, ist das natürlich fatal.
Das ist immer fatal -- auch in C (wenn der compiler es nicht
mitbekommt). Du kannst es bei Deinem Core wenigstens durch einen Fault
abfangen:
http://infocenter.arm.com/help/topic/com.arm.doc.ddi0198e/Babcjcbe.html
Hast Du Einfluss auf das Aligment der Daten?
> Werden den Code nun überarbeiten und nur halbe Wörter mit deferred carry> zusammenzählen.
Halbe Worte? Nun als Referenz vielleicht. Aber denke daran, dass Du
damit etwa doppelt so viele Speicherzugriffe hast, die wohlgemerkt alle
über den Systembus und nicht etwa vom Cache :^) bedient werden.
--
Marcus
Marcus Harnisch schrieb:
> Das ist immer fatal -- auch in C (wenn der compiler es nicht> mitbekommt). Du kannst es bei Deinem Core wenigstens durch einen Fault> abfangen:> http://infocenter.arm.com/help/topic/com.arm.doc.d...
DANKE!!! Deine Tipps sind ja Gold wert!
Werde mich heute nochmal an den Code machen und mir eine elegante Lösung
überlegen. Vielleicht auch mit viel asm weil dann die carries durch adcs
gleich mit der wort-addition aufaddiert werden können.
ja, ja, die caches mach ich auch an! Muss mich vorher nur noch
cache-clean kümmern, bevor EMAC per DMA auf die Daten zugreift. :-)
Habe die Routine nochmal überarbeitet. Nun erreiche ich ohne Caches (die
ich dann auch noch anschalte) eine Berechnungszeit von etwa 30us für die
UDP checksum bei einer Paketgröße von 450B.
Der Code besteht aus zwei Teilen: einer Routine (eth_chksum_sum) die die
one's complement sum berechnet und für UDP und IP benutzt werden kann.
Und dann eben für IP und UDP die hauptroutine. Hier der Code:
Daniel G. schrieb:
> Habe die Routine nochmal überarbeitet. Nun erreiche ich ohne Caches (die> ich dann auch noch anschalte) eine Berechnungszeit von etwa 30us für die> UDP checksum bei einer Paketgröße von 450B.
Sehr schön :-)
> Der Code besteht aus zwei Teilen: einer Routine (eth_chksum_sum) die die> one's complement sum berechnet und für UDP und IP benutzt werden kann.> Und dann eben für IP und UDP die hauptroutine.
Ich dachte, das wäre der selbe Algorithmus. Warum zwei Funktionen?
> Hier der Code:
Das mit dem Alignment würde ich viel pragmatischer angehen (falls das in
Deinem System möglich ist). Das Aligment des Datenzeigers sollte eine
Vorbedingung sein. Du könntest Dir einen entsprehenden Typ definieren
und einen Parameter dieses Typs übergeben.
Warum so viel Assembler. Das ist doch schlecht wartbar und vor allem
nicht portabel. Vorteile neuer CPUs werden gar nicht ausgenutzt, da die
Instruktionen hart verdrahtet sind. Der pipeline interlock zwischen dem
Laden des Halbworts und Summieren ist ebenfalls hart verdrahtet. Ein
Compiler würde durch geeignete Maßnahmen versuchen, das zu entspannen.
Versuch mal das hier. Sollte nicht erheblich langsamer sein (wenn
überhaupt) und ist mehr oder weniger lesbar:
Ich schrieb:
> In beiden Fällen hilft inline Assembler, mit dem man Zugriff auf das Carry> Flag hat. Ist schon einige Takte flotter als das was man mit einem C> compiler hinbekommt.
Wer schreibt denn so einen Mist ;-)
(Ich hatte zu dieser Zeit noch nicht die Idee mit dem 64bit Akkumulator,
die sowohl von GCC als auch von RVCT sehr effizient umgesetzt wird)
Gruß
Marcus
Marcus Harnisch schrieb:
> Ich dachte, das wäre der selbe Algorithmus. Warum zwei Funktionen?
Hast mich nur falsch verstanden: Der algorithmus ist derselbe nur
aufgerufen wird er von jeweils einer anderen Routine für IP und UDP. Die
zu sendenden daten liegen noch verstreut und werden erst nach der
checksummenberechnung zusammengesetzt. Daher kann die checksumme auch
nicht am stück berechnet werden.
Marcus Harnisch schrieb:
> Der pipeline interlock zwischen dem> Laden des Halbworts und Summieren ist ebenfalls hart verdrahtet. Ein> Compiler würde durch geeignete Maßnahmen versuchen, das zu entspannen.
Was meinst Du? Ich lade doch kein Halbwort in asm?! Oder meinst du, dass
ich nach dem addieren wieder zum laden springe? Wie könnte ein compiler
das besser machen? die anzahl steht ja nicht fest, so das die schleife
nicht abgewickelt werden kann.
Danke für den Code, ich werde mich Montag damit auseinander setzen.
Grüße
Daniel
Daniel G. schrieb:
> Was meinst Du? Ich lade doch kein Halbwort in asm?!
Entschuldige, das Wort natürlich. Es gibt zusätzlich zur externen
Speicherlatenz einen sogenannten load-use-interlock zwischen dem LDR
und der anschließenden Addition. Ich fürchte, das der Compiler hier
nichts mehr optimieren kann. Das ist hier nur ein Taktzyklus, aber ein
gutes Beispiel für eine Optimierungs-Sackgasse durch zu starke
Einschränkung (Vorgabe der Assembler Befehle).
Das ist im Einzelfall durchaus sinnvoll, aber bei diesem einfachen
Algorithmus eher hinderlich wie sich im Laufe dieses Threads
herausstellte.
> Wie könnte ein compiler das besser machen? die anzahl steht ja nicht> fest, so das die schleife nicht abgewickelt werden kann.
Klar kann sie. Nur für die übrigbleibenden Iterationen muss dann
weiterer Code ausgeführt werden. Du rollst ja die Schleife selbst
schon dadurch ab, dass Du ganze Worte lädst und dadurch zwei Halbworte
pro Iteration bearbeitest. Und am Ende bearbeitest Du das
abschließende Halbwort, falls vorhanden. Der Compiler macht das
ähnlich, nur eben für mehrere Worte.
Bei dem vom RealView Compiler erzeugten Code werden hier zwei Worte
pro Iteration bearbeitet. Durch die nun längere lineare Code Sequenz
hat der Compiler mehr Möglichkeiten die Befehle so umzusortieren, dass
Wartezeiten vermieden werden (z.B. durch Maximierung des Abstands
zwischen Laden und Bearbeiten eines Wortes). So stellt man zum
Beispiel fest, dass das erste Wort bereits vor Eintritt in die
Schleife geladen wird, während in der Schleife bereits das Wort der
nächsten Iteration aus dem Speicher geholt wird. Dadurch lassen sich
Latenzen auch gut verstecken.
Schönes Wochenende
Marcus
Guten Morgen!
Habe soeben die TX Buffer als write-through eingeschaltet und latenzen
gemessen. Dabei ist mir aufgefallen, dass ich bei der letzten Messung
vergessen hatte die compiler optimierung wieder einzuschalten. Die
genannten 35us bezogen sich also auf ohne cache und ohne compiler
optimierung.
Mit compiler opt. und cache komme ich jetzt auf etwa 8us für UDP
checksum erzeugung.
8us sind etwa 1400 prozessor zyklen. 5 instruktionen per wort in der
schleife in der die wörter zusammengerechnet werden, das sind bei einer
paketgröße von 450B etwa 550 instruktionen. ldr braucht ein bisschen um
daten zu holen, vor allem bei einem cache miss. Ein prozessor takt ist
5,6ns lang. viel ist da wohl nicht mehr rauszuholen.
Ich muss mich erstmal wieder um andere Dinge kümmern, aber wenn ich hier
noch weitere messungen oder experimente gemacht habe, schreib ich
wieder!
Nur mal so als kleiner Hinweis, wenn es dir auf ein Maximum an
Geschwindigkeit ankommt, dann lass die Checksumme weg. Siehe RFC 768,
die Prüfsumme ist bei UDP optional, zum weglassen das Feld im Header auf
0 setzen.
Andreas
Andreas Ferber schrieb:
> Nur mal so als kleiner Hinweis, wenn es dir auf ein Maximum an> Geschwindigkeit ankommt, dann lass die Checksumme weg. Siehe RFC 768,> die Prüfsumme ist bei UDP optional, zum weglassen das Feld im Header auf> 0 setzen.
Ich weiß, aber ich will wissen ob meine Daten richtig ankommen :-)