Frage

In einem Open-Source Programm I schrieb , ich bin Ints Binärdaten (von einem anderen Programm geschrieben) das Lesen aus einer Datei und zum Ausgeben, Doppel-, und verschiedene andere Datentypen. Eine der Herausforderungen besteht darin, dass es braucht, um laufen auf 32-Bit- und 64-Bit-Maschinen beiden Endianess, was bedeutet, dass ich am Ende mit ziemlich viel Low-Level-Bit-Fummeln zu tun. Ich weiß, ein (sehr) wenig über Art punning und strenge Aliasing und wollen sicherstellen, dass ich bin Dinge zu tun, den richtigen Weg.

Im Grunde ist es einfach, von einem char * in einen int in verschiedenen Größen zu konvertieren:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    return *(int64_t *) buf;
}

, und ich habe eine Besetzung von Support-Funktionen Bytereihenfolgen tauschen je nach Bedarf, wie zum wie:

int64_t swappedint64_t(const int64_t wrongend)
{
    /* Change the endianness of a 64-bit integer */
    return (((wrongend & 0xff00000000000000LL) >> 56) |
            ((wrongend & 0x00ff000000000000LL) >> 40) |
            ((wrongend & 0x0000ff0000000000LL) >> 24) |
            ((wrongend & 0x000000ff00000000LL) >> 8)  |
            ((wrongend & 0x00000000ff000000LL) << 8)  |
            ((wrongend & 0x0000000000ff0000LL) << 24) |
            ((wrongend & 0x000000000000ff00LL) << 40) |
            ((wrongend & 0x00000000000000ffLL) << 56));
}

Zur Laufzeit erkennt das Programm die endianness der Maschine und Abtretungs eine der oben auf einen Funktionszeiger:

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}

Jetzt kommt der schwierige Teil, wenn ich versuche, eine char * zu einem Doppel zu werfen. Ich würde gerne wieder verwenden, den Endian-Swapping-Code wie folgt:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);

Allerdings könnten einige Compiler optimieren entfernt die „int64todouble.i“ assignment und brechen Sie das Programm. Gibt es einen sicheren Weg, dies zu tun, unter Berücksichtigung dass dieses Programm muss bleiben für die Leistung optimiert, und auch, dass ich würde lieber nicht einen parallelen Satz von Transformationen zu werfen char * schreiben verdoppeln direkt? Wenn die Vereinigung Methode punning sicher ist, sollte ich sein meine Funktionen wie snativeint64_t neu zu schreiben, es zu benutzen?


mit Steve Jessops Antwort

ich am Ende, weil die Umwandlungsfunktionen neu geschrieben Memcpy zu verwenden, etwa so:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    int64_t output;
    memcpy(&output, buf, 8);
    return output;
}

in die exakt gleichen Assembler wie mein Original-Code kompiliert:

snativeint64_t:
        movq    (%rdi), %rax
        ret

Von den beide die Memcpy Version mehr explizit zum Ausdruck bringt, was ich versuche zu tun, auch und soll funktioniere auf die naiven Compiler.

Adam, war die Antwort auch wunderbar und ich lernte von ihm eine Menge. Vielen Dank für die Buchung!

War es hilfreich?

Lösung

Da Sie scheinen über Ihre Implementierung genug zu wissen, um sicher zu sein, dass int64_t und Doppel sind gleich groß und haben geeignete Speicherdarstellungen, können Sie eine Memcpy Gefahr. Dann haben Sie nicht einmal über Aliasing denken.

Da Sie einen Funktionszeiger für eine Funktion verwenden, die leicht inline sein könnte, wenn Sie bereit sind, mehrere Binärdateien zu veröffentlichen, Leistung muss kein großes Problem sowieso, aber Sie vielleicht wissen, dass einige Compiler recht teuflisch sein kann memcpy Optimierung - für kleine ganze Größen eine Reihe von Lade- und Speicher inlined werden können, und Sie können auch die Variablen wegoptimiert vollständig und der Compiler tut die „Kopie“ einfach die Stack-Slots sein finden Neuzuweisung es für die Variablen ist, genau wie eine Vereinigung.

int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);

Überprüfen Sie den resultierenden Code, oder einfach nur das Profil. Die Chancen stehen gut, selbst im schlimmsten Fall wird es nicht langsam sein.

In der Regel aber etwas zu tun zu klug, mit den Ergebnissen in Portabilitätsproblemen byteswapping. Es gibt ABIs mit mittlerem Endian-Doppelzimmer, wo jedes Wort Little-Endian ist, aber das große Wort an erster Stelle.

Normalerweise könnte man bedenkt, Ihre Doppel Speicherung mit sprintf und sscanf, sondern auch für Ihr Projekt der Dateiformate sind nicht unter Ihrer Kontrolle. Aber wenn Ihre Anwendung schaufeln nur IEEE verdoppelt sich von einer Eingabedatei in einem Format in eine Ausgabedatei in ein anderes Format (nicht sicher, ob es ist, da ich die Datenbankformate in Frage nicht kennen, aber wenn ja), dann sind Sie vielleicht kann über die Tatsache vergessen, dass es eine doppelte ist, da man es nicht für die Arithmetik sowieso verwenden. behandeln Sie es einfach als opaken char [8], erfordern byteswapping nur dann, wenn die Dateiformate unterscheiden.

Andere Tipps

ich sehr empfehlen Sie lesen Verständnis Strict Aliasing . Insbesondere sieht die Abschnitte mit der Bezeichnung „durch eine Vereinigung Casting“. Es hat eine Reihe von sehr guten Beispielen. Während der Artikel auf einer Website über den Cell-Prozessor ist und verwendet Beispiele PPC Montage, fast alles ist gleichermaßen auf andere Architekturen, einschließlich x86.

Die Norm sagt, dass eine Gewerkschaft zu einem Feld zu schreiben und daraus sofort undefiniertes Verhalten zu lesen ist. Also, wenn Sie von der Regel Buch gehen, die Vereinigung basierte Methode wird nicht funktionieren.

Makros ist in der Regel eine schlechte Idee, aber dies könnte eine Ausnahme von der Regel sein. Es sollte möglich sein Template-ähnliches Verhalten in C erhält eine Reihe von Makros mit den Eingangs- und Ausgangstypen als Parameter angeben kann.

Als ein sehr kleinen Unter Vorschlag, ich schlage vor, Sie untersuchen, ob Sie die Maskierung tauschen können und die Verschiebung in dem 64-Bit-Fall. Da die Operation Bytes tauschen, sollten Sie in der Lage sein, immer weg mit einer Maske von nur 0xff zu bekommen. Dies sollte zu einer schnelleren, kompakteren Code führen, es sei denn, der Compiler ist intelligent genug, dass man, um herauszufinden, selbst.

Kurz gesagt, ändert dies:

(((wrongend & 0xff00000000000000LL) >> 56)

in diesen:

((wrongend >> 56) & 0xff)

sollte das gleiche Ergebnis erzeugen.

Edit:
Entfernt Kommentare in Bezug auf, wie man effektiv Daten speichern immer Big-Endian und Swapping Maschine Endianess, als Fragesteller ein anderes Programm nicht erwähnt hat, schreibt seine Daten (die ist eine wichtige Information).
Dennoch, wenn die Datenkonvertierung von jedem Endian braucht zu groß und von big-endian-Host, ntohs / ntohl / htons / htonl sind die besten Methoden, eleganteste und unschlagbar in der Geschwindigkeit (wie sie Aufgabe in der Hardware durchführen wird, wenn die CPU unterstützt, dass, dass Sie nicht zu schlagen).


In Bezug auf Doppel / float, nur speichern, um sie zu Ints durch Speicher Casting:

double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);

Wickeln Sie es in eine Funktion

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

Frager diesen Link zu finden:

http: // cocoawithlove .com / 2008/04 / mit-Zeiger-to-Neufassung-in-c-is-bad.html

als beweisen, dass Casting schlecht ist ... leider kann ich nur stark mit den meisten dieser Seite nicht einverstanden ist. Zitate und Kommentare:

  

Wie häufig wie über einen Zeiger Gießen   ist es wirklich schlechte Praxis und   potenziell riskanter Code. Guss   über einen Zeiger hat das Potenzial,   erstellen Fehler wegen Typ punning.

Es ist nicht riskant überhaupt und es ist auch nicht schlecht Praxis. Es hat nur eine mögliche Fehler verursachen, wenn Sie es falsch machen, so wie die Programmierung in C das Potenzial zu verursachen Fehler hat, wenn Sie es falsch tun, tut dies jede Programmierung in jeder Sprache. Dieses Argument muss man ganz Programmieren beenden.

  

Typ
eine Form der Zeiger punning   Aliasing, wo zwei Zeiger und verweisen   an die gleiche Stelle im Speicher, aber   repräsentieren, die Lage als unterschiedliche   Typen. Der Compiler behandeln beide   „Kalauer“ als unabhängiger Zeiger. Art   punning hat das Potenzial zu verursachen   Abhängigkeitsprobleme für alle Daten   Zugriff durch beide Zeiger.

Das ist wahr, aber leider völlig unabhängig von meinem Code .

Was er verweist Code wie folgt lautet:

int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;

Jetzt doublePointer und intPointer beide auf den gleichen Speicherplatz, aber dies als die gleiche Art zu behandeln. Dies ist die Situation, die Sie mit einer Union lösen sollen in der Tat, alles, was ziemlich schlecht ist. Bad das ist nicht das, was mein Code macht!

Mein Code Kopien von Wert , nicht von Hinweis . Ich warf einen Doppel zu int64 Zeiger (oder umgekehrt) und sofort Achtung es. Sobald die Funktionen geben, gibt es keinen Zeiger auf etwas gehalten. Es gibt eine int64 und ein Doppel und diese sind völlig unabhängig von den Eingangsparameter der Funktionen. Ich kopiere nie einen Zeiger auf einen Zeiger eines anderen Typs (wenn Sie dies in meinem Codebeispiel sah, Sie stark den C-Code falsch verstanden Ich schrieb), I übertragen nur den Wert einer Variablen von einem anderen Typ (in einem eigenen Speicherplatz) . So ist die Definition des Typs punning gilt jedoch nicht, wie er sagt, „an der gleichen Stelle im Speicher verweisen“ und nichts hier bezieht sich auf den gleichen Speicherplatz.

int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;

Mein Code ist nichts anderes als eine Speicherkopie, geschrieben in C ohne externe Funktion.

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

Könnte geschrieben werden

int64_t doubleToInt64(double d)
{
    int64_t result;
    memcpy(&result, &d, sizeof(d));
    return result;
}

Es ist nichts anderes als das, so gibt es keine Art punning auch in den Augen überall. Und dieser Vorgang ist auch absolut sicher, so sicher wie eine Operation kann in C A doppelt so hoch sein wird definiert immer 64 Bit sein (im Gegensatz zu int es nicht in der Größe variiert, ist es bei 64 Bit festgelegt ist), daher wird es immer fit in eine int64_t Größe variabel ist.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top