Ich muss den Inhalt eines Puffers um einige Bytes nach unten verschieben. Da Quell- und Zielbereich überlappen, ist memcpy() nicht nutzbar. Als Alternative gibt es in der Clib die Funktion memmove().
Allerdings scheint die recht aufwändig implementiert zu sein. Die Daten werden zunächst in einen temporären Zwischenspeicher kopiert und dann ins Zielarray geschrieben:
https://pubs.opengroup.org/onlinepubs/000095399/functions/memmove.html
Warum wird das nicht in situ gemacht? Man muss doch nur schauen, ob man beim Kopieren oben oder unten beginnt: Ist die Zieladresse kleiner als die Quelladresse, wird von niedrigen zu hohen Adressen hin ietriert, sonst umgekeht.
In der tat, memmove wird meisten so umgesetzt, dass es jenachdem von hinten kopiert.
Wobei, wenn ich die selbe Page mit mmap mehrfach hintereinander Mappe, würde das ja nicht mehr immer so herauskommen als ob man das in ein Zwischenarray kopiert hätte. Ist diese Art es zu Implementieren also technisch gesehen falsch?
Ich muss den Inhalt eines Puffers um einige Bytes nach unten
verschieben. Da Quell- und Zielbereich überlappen, ist memcpy() nicht
nutzbar. Als Alternative gibt es in der Clib die Funktion memmove().
Allerdings scheint die recht aufwändig implementiert zu sein. Die Daten
werden zunächst in einen temporären Zwischenspeicher kopiert und dann
ins Zielarray geschrieben:
Warum schreibst Du die paar Zeilen Code nicht einfach selber?
Warum schreibst Du die paar Zeilen Code nicht einfach selber?
Wenn du dir ein mal die Implementierung von memmove in einer seriösen Library angesehen hättest, dann wüsstest du, warum die komplex sind.
Es wird, je nach Blocklänge, versucht eine optimal schnelle Kopiermethode zu finden, ob man also Einzelbytetransfer oder 32/64 bit Worttransfer nutzt und wie man an Anfang und Ende paddet.
Ich erinnere mich beim 8088 auch wie man segment aliasing erkannt hat, und bei virtual memory wie man page aliasing erkannt hat.
Das ganze scheint noch etwas komplexer zu sein, als ich es mir vorgestellt habe. Ich habe mal ein Programm erstellt, bei dem das Vorgehen zu checken, ob src < dest + Kopierrichtung, nicht ausreichend ist, um falsche Resultate zu vermeiden. Zusätzlich hab ich auch noch eine eigene memmove Variante mit Zwischenpuffer erstellt: https://godbolt.org/z/bT8vbje6d
Beim Intel Compiler tritt der erwartete Fehler auf. Bei GCC und Clang nicht. Bei clang ist noch interessant, dass es meine memmove Funktion durch einen Call zu seiner eigenen ersetzt.
Ich weiss nicht, wie das GCC und Clang machen. Hat mich ziemlich überrascht, das Resultat.
Ich weiss nicht, wie das GCC und Clang machen. Hat mich ziemlich
überrascht, das Resultat.
Ich habe es mal schnell getestet. Bei mir liefern alle Compiler (cl, clang-cl, clang und gcc) ein falsches Ergebnis wenn memcpy verwendet wird.
Wegen mmap: Das ist etwa so wie wenn ein zweiter Prozess den Speicher klaut. Da gibt es sicher eine Klausel im Standard, die dir sagt dass du fair spielen sollst.
Warum schreibst Du die paar Zeilen Code nicht einfach selber?
Eine optimierte memcpy Funktion ist bei grösseren Blöcken deutlich schneller als eine C Schleife (bei 8kB Blöcken etwa 28x). Die asm Funktion braucht gerade mal 80 Bytes:
Ich weiss nicht, wie das GCC und Clang machen. Hat mich ziemlich
überrascht, das Resultat.
Ich habe es mal schnell getestet. Bei mir liefern alle Compiler (cl,
clang-cl, clang und gcc) ein falsches Ergebnis wenn memcpy verwendet
wird.
Schon klar, aber hier geht es um memmove, nicht um memcpy.
Wegen mmap: Das ist etwa so wie wenn ein zweiter Prozess den Speicher
klaut. Da gibt es sicher eine Klausel im Standard, die dir sagt dass du
fair spielen sollst.
Soweit ich weiss, gibt es das nicht. Es gibt das Aliasing Zeugs, das sich darauf bezieht, ob 2 Pointer auf das selbe Objekt zeigen können, aber meines Wissens steht nirgends, dass 2 Pointer auf das selbe Objekt auch den selben Wert enthalten müssen, oder dass sie eindeutig sein müssten, und auch nicht, dass sich zwei unterschiedliche Objekte nicht beeinflussen könnten.
Man muss in der Regel sogar annehmen, das unter anderem 2 Pointer vom selben Typ auf das selbe Objekt zeigen könnte.
Ausserdem würde ich doch meinen, das memory mapping etwas ziemlich fundamentales ist, womit allerhand C programme umgehen können müssen. Ob das jetzt für IPC ist, für embedded Sachen, in einem Kernel, oder sonst wo. Wenn da der Compiler einfach Blödsinn produzieren dürfte, sobald man etwas memory mapping macht, wäre das doch recht problematisch.
Eine optimierte memcpy Funktion ist bei grösseren Blöcken deutlich
schneller als eine C Schleife (bei 8kB Blöcken etwa 28x).
Wobei es eine echte Kunst ist, memcpy/memmove so zu schreiben, dass es unter allen Randbedingungen optimal arbeitet. Also ob im Cache, bzw in welchen Cache, oder lieber am Cache vorbei, mit Lookahead/Prefetch automatisch oder zu Fuss, Alignment berücksichtigend, ... Und das dann über alle x86-Implementierungen seit der Sintflut. Es ist/war auch nicht gesichert, dass der x86 Microcode das gut macht.
Überlappende Kopien setzen da nochmal eins drauf, weil das Zugriffsverhalten in der Implementierung durch die Speicherhierarchie davon erheblich betroffen sein kann, zumal wenn Quelle und Ziel im gleichen Cacheblock oder Write-Buffer liegen.
Beim Intel Compiler tritt der erwartete Fehler auf. Bei GCC und Clang
nicht. Bei clang ist noch interessant, dass es meine memmove Funktion
durch einen Call zu seiner eigenen ersetzt.
Ich weiss nicht, wie das GCC und Clang machen. Hat mich ziemlich
überrascht, das Resultat.
gcc (und sicherlich auch clang) hat einen ganzen Haufen Standardfunktionen bereits eingebaut und kann daher jeden Aufruf nochmal entsprechend den Gegebenheiten optimieren, ohne die Funktion aus der Library aufrufen zu müssen. Unter https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/Library-Builtins.html findet man eine Liste.
Wegen mmap: Das ist etwa so wie wenn ein zweiter Prozess den Speicher
klaut. Da gibt es sicher eine Klausel im Standard, die dir sagt dass du
fair spielen sollst.
Soweit ich weiss, gibt es das nicht.
Das, was die MMU beim Memory Mapping tut, ist nicht Gegenstand des
Standards und spielt sich deswegen außerhalb der "Abstract Machine" des
Standards ab. Das ist vergleichbar mit I/O-Registern, deren Inhalte
ebenfalls von außerhalb beeinflusst werden. Gleiches gilt auch für
Shared Memory, der mit anderen Prozessen geteilt wird.
Um die Zugriffe auf solche Dinge zu steuern, werden diese üblicherweise
als volatile deklariert. Das kannst du auch bei den mgemappten
Speicherbereichen tun, dann bekommst du bei den Aufrufen von memcpy()
und memmove() sogar Warnungen angezeigt.
Beim Intel Compiler tritt der erwartete Fehler auf. Bei GCC und Clang
nicht.
Der ICC verwendet andere Bibliotheksfunktionen als GCC und Clang:
ICC:
1
call _intel_fast_memmove
GCC und Clang:
1
call memmove@PLT
Letztere mündet in einen Aufruf der entsprechenden Funktion in glibc.
Warum bei dieser Funktion der Effekt des Überschreibens der kopierten
Daten nicht auftritt, weiß ich auch nicht. Ich könnte mir aber
vorstellen, dass das Kopieren durch DMA im physikalischen Adressraum
unterstützt wird. Wenn die Entscheidung, ob aufwärts oder abwärts
kopiert werden soll, erst nach der Konvertierung der virtuellen
Startadressen in physikalische erfolgt, wird nichts überschrieben.
Das, was die MMU beim Memory Mapping tut, ist nicht Gegenstand des
Standards und spielt sich deswegen außerhalb der "Abstract Machine" des
Standards ab.
Soweit kann ich noch zustimmen.
Das ist vergleichbar mit I/O-Registern, deren Inhalte
ebenfalls von außerhalb beeinflusst werden. Gleiches gilt auch für
Shared Memory, der mit anderen Prozessen geteilt wird.
Hier aber dann nicht mehr. Die Änderung des Speichers / des Objekts erfolgt hier nicht ausserhalb des Programms, sondern durch das Programm selbst. Der Compiler muss annehmen, dass a und b in meinem Beispiel auf das selbe Objekt verweisen könnten, gemäss den aliasing regeln. Dass das hier wegen dem memory mapping so ist, ist irrelevant.
Ich sehe das ähnlich wie z.B. bei der x86 memory segmentation. Auch da können unterschiedliche Adressen auf das selbe Objekt zeigen, und auch das ist etwas, was ausserhalb des Scopes der "Abstract Machine" liegt.
Das ändert aber nichts an den Regeln, die der Standard vorgibt, und an die sich der Compiler halten muss. Das Programm muss sich trotzdem so verhalten, wie die beschriebene "Abstract Machine".
Der Fall mit den I/O-Registern ist anders. Denn der Fall wird ja genau vom Standard abgedeckt. Die Änderungen sind ausserhalb des Programms, also nimmt man dort volatile.
Ich denke ausserdem, dass in meinem Beispiel mit mmap oben, volatile falsch wäre, aus den selben Gründen, warum volatile bei mehreren Threads statt _Atomic falsch wäre. Eben weil es keine Änderung ausserhalb des Programms ist, und daher all die üblichen Regeln weiterhin gelten.
Die Änderung des Speichers / des Objekts erfolgt hier nicht ausserhalb
des Programms,
Doch, nämlich durch die MMU, die in diesem Zusammenhang als
Peripherieeinheit betrachtet werden muss.
sondern durch das Programm selbst.
Das Programm bzw. die CPU greift nicht direkt auf den Speicher zu,
sondern veranlasst durch das Senden einer (virtuelle) Adresse und eines
Datenworts an die MMU, das Datenwort in den Speicher zu schreiben.
Entsprechend laufen auch Lesezugriffe auf den Speicher über die MMU. Die
CPU sieht somit nur die MMU, nicht aber den eigentlichen Speicher.
Jede einzelne Speicherzelle präsentiert sich der CPU damit als eine Art
I/O-Register mit der Eigenschaft, dass das Beschreiben eines dieser
Register möglicherweise die Änderung des Inhalts eines anderen Registers
zu Folge hat.
Übertragen auf die abstrakte Maschine des C-Standards bedeutet dies,
dass dies gar keinen Speicher, sondern nur I/O-Space hat und deswegen
auf einem Rechner mit MMU (scheinbar) gar kein konformer C-Compiler
möglich ist.
Dieses Problem zeigt sich nicht nur im Zusammenhang mit memmove, sondern
auch schon in ganz banalen Dingen wie Pointer-Vergleichen. Laut Standard
liefert der ==-Operator beim Vergleich zweier Pointer genau dann 1, wenn
beide auf dasselbe Objekt zeigen. In deinem Beispiel von oben müsste
also a==b gleich 1 sein, was aber weder beim ICC noch beim GCC noch beim
Clang der Fall ist. Ein standardkonformer Compiler für eine Rechner mit
MMU müsste dafür sorgen, dass bei Pointer-Vergleichen für die beiden
Pointer die physikalischen Adressen aus der Page Table ausgelesen und
und diese verglichen werden.
Was aber, wenn das Programm gar keinen Zugriff auf die Page Table hat?
Müssen wir dann auf modernen PCs auf C komplett verzichten?
Eine praktikable Möglichkeit, dennoch ein standardkonformes C zu
realisieren, besteht gemäß der as-if-Regel darin, alle für ein Programm
zugänglichen Speicher-"I/O-Register" in ihrer Gesamtheit für die CPU wie
ein direkt angeschlossener Speicher aussehen zu lassen und die MMU damit
unsichtbar zu machen. Dazu muss die MMU so konfiguriert werden, dass
innerhalb des virtuellen Adressraums des Programms eine eindeutige
Zuordnung von physikalischen zu virtuellen Adressen besteht.
Dieser Zustand wird beim Start des Programms durch das Betriebssystem
garantiert, kann aber durch böswilliges Herumspielen mit mmap() zerstört
werden. Deswegen lässt man dies besser bleiben :)