Schnellste de-interleave-operation in C?
Habe ich einen Zeiger auf ein array von bytes mixed
enthält die verschachtelte bytes der zwei unterschiedliche arrays array1
und array2
. Sagen mixed
sieht ungefähr so aus:
a1b2c3d4...
Was ich tun müssen, ist de-verschachteln der bytes, so bekomme ich array1 = abcd...
und array2 = 1234...
. Ich weiß, die Länge von mixed
vor der Zeit, und die Längen der array1
und array2
sind gleichwertig, beide gleich mixed /2
.
Hier ist meine aktuelle Umsetzung (array1
und array2
sind bereits zugeordnet):
int i, j;
int mixedLength_2 = mixedLength / 2;
for (i = 0, j = 0; i < mixedLength_2; i++, j += 2)
{
array1[i] = mixed[j];
array2[i] = mixed[j+1];
}
Dies vermeidet teure Multiplikation oder division Operationen, aber immer noch nicht schnell genug laufen. Ich hoffe, dass es so etwas wie memcpy
braucht man ein indexer, der die low-level-block-copy-Vorgänge, um den Prozess zu beschleunigen. Gibt es eine schnellere Umsetzung, als das, was ich derzeit habe?
Bearbeiten
Die Zielplattform ist Objective-C für iOS und Mac. Eine schnelle operation ist wichtiger für iOS-Geräte, also eine Lösung, die sich an iOS-spezifisch wäre besser als nichts.
Update
Danke an alle für die Antworten, vor allem Stephen Canon, Graham Lee, und Mecki. Hier ist mein "master" - Funktion, die verwendet Stephen ' s NEON Interna, wenn verfügbar und sonst Graham union Cursor mit einer reduzierten Anzahl von Iterationen wie vorgeschlagen von Mecki.
void interleave(const uint8_t *srcA, const uint8_t *srcB, uint8_t *dstAB, size_t dstABLength)
{
#if defined __ARM_NEON__
//attempt to use NEON intrinsics
//iterate 32-bytes at a time
div_t dstABLength_32 = div(dstABLength, 32);
if (dstABLength_32.rem == 0)
{
while (dstABLength_32.quot --> 0)
{
const uint8x16_t a = vld1q_u8(srcA);
const uint8x16_t b = vld1q_u8(srcB);
const uint8x16x2_t ab = { a, b };
vst2q_u8(dstAB, ab);
srcA += 16;
srcB += 16;
dstAB += 32;
}
return;
}
//iterate 16-bytes at a time
div_t dstABLength_16 = div(dstABLength, 16);
if (dstABLength_16.rem == 0)
{
while (dstABLength_16.quot --> 0)
{
const uint8x8_t a = vld1_u8(srcA);
const uint8x8_t b = vld1_u8(srcB);
const uint8x8x2_t ab = { a, b };
vst2_u8(dstAB, ab);
srcA += 8;
srcB += 8;
dstAB += 16;
}
return;
}
#endif
//if the bytes were not aligned properly
//or NEON is unavailable, fall back to
//an optimized iteration
//iterate 8-bytes at a time
div_t dstABLength_8 = div(dstABLength, 8);
if (dstABLength_8.rem == 0)
{
typedef union
{
uint64_t wide;
struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; uint8_t a3; uint8_t b3; uint8_t a4; uint8_t b4; } narrow;
} ab8x8_t;
uint64_t *dstAB64 = (uint64_t *)dstAB;
int j = 0;
for (int i = 0; i < dstABLength_8.quot; i++)
{
ab8x8_t cursor;
cursor.narrow.a1 = srcA[j ];
cursor.narrow.b1 = srcB[j++];
cursor.narrow.a2 = srcA[j ];
cursor.narrow.b2 = srcB[j++];
cursor.narrow.a3 = srcA[j ];
cursor.narrow.b3 = srcB[j++];
cursor.narrow.a4 = srcA[j ];
cursor.narrow.b4 = srcB[j++];
dstAB64[i] = cursor.wide;
}
return;
}
//iterate 4-bytes at a time
div_t dstABLength_4 = div(dstABLength, 4);
if (dstABLength_4.rem == 0)
{
typedef union
{
uint32_t wide;
struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; } narrow;
} ab8x4_t;
uint32_t *dstAB32 = (uint32_t *)dstAB;
int j = 0;
for (int i = 0; i < dstABLength_4.quot; i++)
{
ab8x4_t cursor;
cursor.narrow.a1 = srcA[j ];
cursor.narrow.b1 = srcB[j++];
cursor.narrow.a2 = srcA[j ];
cursor.narrow.b2 = srcB[j++];
dstAB32[i] = cursor.wide;
}
return;
}
//iterate 2-bytes at a time
div_t dstABLength_2 = div(dstABLength, 2);
typedef union
{
uint16_t wide;
struct { uint8_t a; uint8_t b; } narrow;
} ab8x2_t;
uint16_t *dstAB16 = (uint16_t *)dstAB;
for (int i = 0; i < dstABLength_2.quot; i++)
{
ab8x2_t cursor;
cursor.narrow.a = srcA[i];
cursor.narrow.b = srcB[i];
dstAB16[i] = cursor.wide;
}
}
void deinterleave(const uint8_t *srcAB, uint8_t *dstA, uint8_t *dstB, size_t srcABLength)
{
#if defined __ARM_NEON__
//attempt to use NEON intrinsics
//iterate 32-bytes at a time
div_t srcABLength_32 = div(srcABLength, 32);
if (srcABLength_32.rem == 0)
{
while (srcABLength_32.quot --> 0)
{
const uint8x16x2_t ab = vld2q_u8(srcAB);
vst1q_u8(dstA, ab.val[0]);
vst1q_u8(dstB, ab.val[1]);
srcAB += 32;
dstA += 16;
dstB += 16;
}
return;
}
//iterate 16-bytes at a time
div_t srcABLength_16 = div(srcABLength, 16);
if (srcABLength_16.rem == 0)
{
while (srcABLength_16.quot --> 0)
{
const uint8x8x2_t ab = vld2_u8(srcAB);
vst1_u8(dstA, ab.val[0]);
vst1_u8(dstB, ab.val[1]);
srcAB += 16;
dstA += 8;
dstB += 8;
}
return;
}
#endif
//if the bytes were not aligned properly
//or NEON is unavailable, fall back to
//an optimized iteration
//iterate 8-bytes at a time
div_t srcABLength_8 = div(srcABLength, 8);
if (srcABLength_8.rem == 0)
{
typedef union
{
uint64_t wide;
struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; uint8_t a3; uint8_t b3; uint8_t a4; uint8_t b4; } narrow;
} ab8x8_t;
uint64_t *srcAB64 = (uint64_t *)srcAB;
int j = 0;
for (int i = 0; i < srcABLength_8.quot; i++)
{
ab8x8_t cursor;
cursor.wide = srcAB64[i];
dstA[j ] = cursor.narrow.a1;
dstB[j++] = cursor.narrow.b1;
dstA[j ] = cursor.narrow.a2;
dstB[j++] = cursor.narrow.b2;
dstA[j ] = cursor.narrow.a3;
dstB[j++] = cursor.narrow.b3;
dstA[j ] = cursor.narrow.a4;
dstB[j++] = cursor.narrow.b4;
}
return;
}
//iterate 4-bytes at a time
div_t srcABLength_4 = div(srcABLength, 4);
if (srcABLength_4.rem == 0)
{
typedef union
{
uint32_t wide;
struct { uint8_t a1; uint8_t b1; uint8_t a2; uint8_t b2; } narrow;
} ab8x4_t;
uint32_t *srcAB32 = (uint32_t *)srcAB;
int j = 0;
for (int i = 0; i < srcABLength_4.quot; i++)
{
ab8x4_t cursor;
cursor.wide = srcAB32[i];
dstA[j ] = cursor.narrow.a1;
dstB[j++] = cursor.narrow.b1;
dstA[j ] = cursor.narrow.a2;
dstB[j++] = cursor.narrow.b2;
}
return;
}
//iterate 2-bytes at a time
div_t srcABLength_2 = div(srcABLength, 2);
typedef union
{
uint16_t wide;
struct { uint8_t a; uint8_t b; } narrow;
} ab8x2_t;
uint16_t *srcAB16 = (uint16_t *)srcAB;
for (int i = 0; i < srcABLength_2.quot; i++)
{
ab8x2_t cursor;
cursor.wide = srcAB16[i];
dstA[i] = cursor.narrow.a;
dstB[i] = cursor.narrow.b;
}
}
- Nun, wenn die Eingabe tatsächlich interleaved, dann kann man nicht wirklich block-Kopie...
- Welche Plattform[s] Sie sind targeting? Viele gut optimierte Bibliothek-Funktionen für die Ausführung dieser Operationen. Es gibt nichts in der C-standard-Bibliothek, jedoch.
- Objective-C für iOS/Mac. Diese Optimierung ist besonders wichtig für iOS.
- was bedeutet iOS und OS X, oder haben Sie Sorge, über den anderen Plattformen auch?
- Bearbeitet mein Kommentar zu klären - iOS und OS X.
- memcpy nicht funktioniert, aber ich bin der Hoffnung, für etwas gleich schnell sind.
- Sollte nicht sein, dass viel Verbesserung, aber statt
i < mixedLength / 2
schreiben Siej < mixedLength
und speichern Sie eine division pro iteration ohne eine temporäre variable. - Danke, hab ich aktualisiert den code entsprechend. Du hast Recht - es ist nicht genug von einer Verbesserung.
- Sie können versuchen, das Lesen der Quell-array als ein array von kurzen (2-byte-Mengen) oder vielleicht sogar 4-oder 8-byte-Ganzzahlen. Store durch Extraktion von geraden und ungeraden Hälften mit Schichten und Masken. Nicht wirklich portabel, sondern Sie sollten etwas beschleunigen.
- Die de-interleaved bytes übergeben werden, in eine third-party Bibliothek. Könnte ich das evtl ändern Sie das third-party-Bibliothek, sodass es Indizes anders, aber das wäre eine "alles-andere-ist-gescheitert" last resort".
- Sie brauchen nicht zu ändern Ihre Schnittstelle. So etwas wie
short a=((short*)mixed)[i]; array1[i] = a&0xFF; array2[i] = a>>8;
. - Haben Sie schaute auf die Accelerate-framework-API? Sie werden zweifellos finden, was Sie nach gibt.
- Ich denke, dass Sie verwenden können
vunzp.8
für ein NEON-Teil des Programms. Es sieht aus wie Stephen gab es Sie unten. Siehe auch Codierung für NEON - Teil 5: Neuanordnung von Vektoren.
Du musst angemeldet sein, um einen Kommentar abzugeben.
Aus der Spitze von meinem Kopf, ich weiß nicht, der eine library-Funktion für de-interleaving 2-Kanal-byte-Daten. Allerdings lohnt es sich, die Einreichung einer bug-report bei Apple auf Anfrage eine solche Funktion.
In der Zwischenzeit, es ist ziemlich einfach zu Vektorisieren solche Funktion mit NEON-oder SSE-Interna. Insbesondere auf den ARM, die Sie verwenden möchten
vld1q_u8
laden ein Vektor von jeder Quelle im arrayvuzpq_u8
de-verschachteln Sie, undvst1q_u8
zum speichern der resultierenden Vektoren; hier ist eine grobe Skizze, die ich noch nicht getestet, oder sogar versucht zu bauen, aber es sollte verdeutlichen die Allgemeine Idee. Mehr anspruchsvolle Implementierungen sind definitiv möglich (insbesondere, NEON-laden/speichern zwei 16B Register in einer einzigen Anweisung, die der compiler kann nicht tun dies und einige Menge von pipelining und/oder abrollen kann vorteilhaft sein, je nachdem, wie lang die Puffer sind):float
undint
, ich hätte die gleichen sorgen wie die OP in dieser Frage bei der Verwendung vonfloat
Vektor-Anweisungen zu mischenint
s, multipliziert mit so vielen Plattformen wie der Accelerate-framework ist für. Die Antwort ist subtil nur für die x86-Architektur. stackoverflow.com/questions/4996384/...vuzpq_u8
sogar noch schneller, in einigen Prozessoren.vuzpq_u8
alsvld2q_u8
auf ein iPhone X von etwa 1,5 x (nicht wissenschaftlich).Habe ich nur getestet, leicht, aber es schien mindestens doppelt so schnell wie Ihre version:
Merke, dass ich war nicht vorsichtig mit Struktur packen, aber dass Sie in diesem Fall auf dieser Architektur das ist nicht ein problem. Beachten Sie auch, jemand könnte sich beschweren bei meiner Wahl der Benennung
top
undbottom
; ich nehme an, Sie wissen, die Hälfte von ganzen zahlen, die Sie brauchen.Okay, hier ist dein original-Methode:
Mit 10 Millionen Einträge und
-O3
(compiler darf optimieren, um maximale Geschwindigkeit), kann ich diese 154-mal pro Sekunde auf meinem Mac.Hier ist mein Erster Vorschlag:
Gleichen Zählung und-Optimierung nach wie vor, ich bekomme 193 läuft pro Sekunde.
Nun die Anregung von Graham Lee:
Gleiche setup wie vorher, 198 läuft pro Sekunde (HINWEIS: Diese Methode ist nicht-endian-sicher, das Ergebnis hängt von der CPU-endian Typ. In Ihrem Fall array1 und array2 sind vermutlich vertauscht, da die ARM little-endian, so würden Sie haben, um Sie zu tauschen in den code).
Hier ist meine beste bisher:
Gleiche setup wie oben, 219-mal in einer Sekunde und es sei denn, ich einen Fehler gemacht, sollte funktionieren entweder mit endian Typ.
Ich empfehle Graham Lösung, aber wenn das wirklich ist die Geschwindigkeit entscheidend, und Sie sind bereit zu gehen, Assembler, kann man sogar noch schneller.
Die Idee ist diese:
Lesen eine ganze 32bit ganze Zahl von
mixed
. Sie erhalten 'a1b2'.Drehen Sie die unteren 16-bit von 8 bits zu erhalten "1ab2'(wir sind über little endian, da dies der Standardeinstellung in den ARM und daher von Apple Ein#, also die ersten beiden bytes sind die unteren sind).
Drehen Sie die gesamte 32bit register rechts(ich denke, es ist richtig...) von 8 bits zu erhalten, '21ab'.
Drehen Sie die unteren 16-bit von 8 bits zu erhalten, '12ab'
Schreiben der unteren 8 bits zu
array2
.Drehen Sie den gesamten 32-bit-register von 16 bit.
Schreiben der unteren 8 bits zu
array1
Voraus
array1
von 16bitarray2
von 16bit undmixed
von 32bit.Wiederholen.
Wir haben gehandelt 2 Speicher liest(vorausgesetzt, wir verwenden den Graham ' s version oder äquivalent) und 4-Speicher mit einer Speicherkarte zu Lesen, zwei Speicher schreibt und 4-register-Operationen. Während die Zahl der Operationen hat sich von 6 auf 7, - register-Operationen sind schneller als Speicher-Operationen, so ist es effizienter, die Art und Weise. Auch da Lesen wir von
mixed
32bit zu einer Zeit statt 16, wir schneiden iteration management um die Hälfte.PS: Theoretisch ist dies auch für die 64bit Architektur, aber das tun alle diese Drehungen für 'a1b2c3d4' fahren Sie zum Wahnsinn.
Für x86-SSE, die
pack
undpunpck
Anweisungen sind, was Sie brauchen. Beispiele für die Verwendung von AVX für die Bequemlichkeit der nicht-destruktive 3-Operanden-Instruktionen. (Nicht mit AVX2-256b-Breite Anweisungen, weil die 256b pack/unpck Anweisungen zwei 128b entpackt in den low-und high-128b-Bahnen, so müssten Sie shuffle, um die Dinge in die richtige Abschluss der Bestellung.)Ein-Interna-version der folgenden würde die gleiche Arbeit. Asm-Anweisungen sind kürzer zu geben, die für nur zu schreiben, eine schnelle Antwort.
Interleave:
abcd
und1234
->a1b2c3d4
:De-interleave:
a1b2c3d4
->abcd
und1234
Diese können natürlich auch ein viel weniger registriert. Ich vermied die Wiederverwendung von Registern für die Lesbarkeit, nicht die Leistung. Hardware-register umbenennen macht die Wiederverwendung ein nicht-Problem, wie lange, wie Sie mit etwas beginnen, das hängt nicht von den vorherigen Wert. (z.B.
movd
, nichtmovss
oderpinsrd
.)Deinterleave ist so viel mehr Arbeit, weil die
pack
Anweisungen signed oder unsigned Sättigung, also die Obere 8b jeder 16b element gelöscht werden ersten.Alternative wäre die Verwendung
pshufb
zu packen, gerade oder ungerade Worte von einem single-source-reg in den niedrigen 64 ein register. Jedoch außerhalb der AMD XOP-Befehlssatz istVPPERM
gibt es nicht einen shuffle können, wählen Sie Byte von 2 Register auf einmal (wie Altivec ist viel-liebtevperm
). Also nur mit SSE/AVX, müssten Sie 2 mischt für jede 128b von interleaved Daten. Und da store-port-Verwendung könnte der Engpass sein, einepunpck
zum kombinieren von zwei 64-bit-Blöcken vona
in einem einzigen register einzurichten, 128b speichern.Mit AMD XOP, deinterleave wäre 2x128b Lasten, 2
VPPERM
, und 2x128b speichert.vorzeitige Optimierung ist schlecht
dem compiler ist wahrscheinlich besser zu optimieren, als Sie sind.
Sagte, es sind Dinge, die Sie tun können, um zu helfen out der compiler, weil Sie semantische Kenntnis von Ihren Daten, dass ein compiler nicht haben:
Lesen und schreiben so viele bytes, wie Sie können, bis auf die native word size - Speicher-Operationen sind teuer, so haben Manipulationen im Register, wo möglich
entrollen von Schleifen - look in "Duff' s Device".
FWIW, ich produzierte zwei Versionen der copy loop, einem viel die gleichen wie deine, die zweite mit dem, was die meisten denken würden, "optimale" (wenn auch simple) C-code:
Mit
gcc -O3 -S
auf der Intel x86-beide produziert fast identische Assembler-code. Hier sind die inneren Schleifen:und
Beide haben die gleiche Anzahl von Anweisungen, die den Unterschied ausmachten, nur weil die erste version zählt bis zu
n /2
, und die zweite nach unten zählt auf null.BEARBEITEN hier ist eine bessere version:
ergibt:
ist man weniger Unterricht, da dauert es Vorteil der sofortigen Zugang zu
%cl
und%ch
.