Ich interessiere mich für das Schreiben von a memcpy()
als pädagogische Übung. Ich werde keine ganze Abhandlung darüber schreiben, was ich tat und woran ich nicht dachte, aber hier ist es
die Implementierung eines Typen:
__forceinline // Since Size is usually known,
// most useless code will be optimized out
// if the function is inlined.
void* myMemcpy(char* Dst, const char* Src, size_t Size)
{
void* start = Dst;
for ( ; Size >= sizeof(__m256i); Size -= sizeof(__m256i) )
{
__m256i ymm = _mm256_loadu_si256(((const __m256i* &)Src)++);
_mm256_storeu_si256(((__m256i* &)Dst)++, ymm);
}
#define CPY_1B *((uint8_t * &)Dst)++ = *((const uint8_t * &)Src)++
#define CPY_2B *((uint16_t* &)Dst)++ = *((const uint16_t* &)Src)++
#define CPY_4B *((uint32_t* &)Dst)++ = *((const uint32_t* &)Src)++
#if defined _M_X64 || defined _M_IA64 || defined __amd64
#define CPY_8B *((uint64_t* &)Dst)++ = *((const uint64_t* &)Src)++
#else
#define CPY_8B _mm_storel_epi64((__m128i *)Dst, _mm_loadu_si128((const __m128i *)Src)), ++(const uint64_t* &)Src, ++(uint64_t* &)Dst
#endif
#define CPY16B _mm_storeu_si128((__m128i *)Dst, _mm_loadu_si128((const __m128i *)Src)), ++(const __m128i* &)Src, ++(__m128i* &)Dst
switch (Size) {
case 0x00: break;
case 0x01: CPY_1B; break;
case 0x02: CPY_2B; break;
case 0x03: CPY_1B; CPY_2B; break;
case 0x04: CPY_4B; break;
case 0x05: CPY_1B; CPY_4B; break;
case 0x06: CPY_2B; CPY_4B; break;
case 0x07: CPY_1B; CPY_2B; CPY_4B; break;
case 0x08: CPY_8B; break;
case 0x09: CPY_1B; CPY_8B; break;
case 0x0A: CPY_2B; CPY_8B; break;
case 0x0B: CPY_1B; CPY_2B; CPY_8B; break;
case 0x0C: CPY_4B; CPY_8B; break;
case 0x0D: CPY_1B; CPY_4B; CPY_8B; break;
case 0x0E: CPY_2B; CPY_4B; CPY_8B; break;
case 0x0F: CPY_1B; CPY_2B; CPY_4B; CPY_8B; break;
case 0x10: CPY16B; break;
case 0x11: CPY_1B; CPY16B; break;
case 0x12: CPY_2B; CPY16B; break;
case 0x13: CPY_1B; CPY_2B; CPY16B; break;
case 0x14: CPY_4B; CPY16B; break;
case 0x15: CPY_1B; CPY_4B; CPY16B; break;
case 0x16: CPY_2B; CPY_4B; CPY16B; break;
case 0x17: CPY_1B; CPY_2B; CPY_4B; CPY16B; break;
case 0x18: CPY_8B; CPY16B; break;
case 0x19: CPY_1B; CPY_8B; CPY16B; break;
case 0x1A: CPY_2B; CPY_8B; CPY16B; break;
case 0x1B: CPY_1B; CPY_2B; CPY_8B; CPY16B; break;
case 0x1C: CPY_4B; CPY_8B; CPY16B; break;
case 0x1D: CPY_1B; CPY_4B; CPY_8B; CPY16B; break;
case 0x1E: CPY_2B; CPY_4B; CPY_8B; CPY16B; break;
case 0x1F: CPY_1B; CPY_2B; CPY_4B; CPY_8B; CPY16B; break;
}
#undef CPY_1B
#undef CPY_2B
#undef CPY_4B
#undef CPY_8B
#undef CPY16B
return start;
}
Der Kommentar bedeutet übersetzt “Größe ist normalerweise bekannt, da der Compiler den Code inline am nutzlosesten optimieren kann”.
Ich würde diese Implementierung gerne verbessern, wenn möglich – aber vielleicht gibt es nicht viel zu verbessern. Ich sehe, dass es SSE/AVX für die größeren Speicherblöcke verwendet, dann anstelle einer Schleife über die letzten <32 Bytes das Äquivalent des manuellen Entrollens mit einigen Optimierungen. Hier also meine Fragen:
- Warum die Schleife für die letzten paar Bytes aufrollen, aber die erste (und jetzt einzelne) Schleife nicht teilweise aufrollen?
- Was ist mit Ausrichtungsproblemen? Sind sie nicht wichtig? Sollte ich die ersten paar Bytes bis zu einem gewissen Ausrichtungsquantum anders handhaben und dann die 256-Bit-Operationen an ausgerichteten Bytesequenzen ausführen? Und wenn ja, wie bestimme ich das passende Ausrichtungsquantum?
- Was ist das wichtigste fehlende Feature in dieser Implementierung (falls vorhanden)?
Bisher in den Antworten erwähnte Merkmale/Prinzipien
- Du solltest
__restrict__
Ihre Parameter. (@chux) - Die Speicherbandbreite ist ein limitierender Faktor; Messen Sie Ihre Implementierung daran.(@Zboson)
- Bei kleinen Arrays können Sie davon ausgehen, dass Sie sich der Speicherbandbreite annähern; für größere Arrays – nicht so viel. (@Zboson)
- Mehrere Threads (möglicherweise | sind) sind erforderlich, um die Speicherbandbreite zu sättigen. (@Zboson)
- Es ist wahrscheinlich ratsam, für große und kleine Kopiengrößen unterschiedlich zu optimieren. (@Zboson)
- (Ausrichtung ist wichtig? Nicht explizit angesprochen!)
- Der Compiler sollte expliziter auf “offensichtliche Tatsachen” aufmerksam gemacht werden, die er zur Optimierung nutzen kann (zB die Tatsache, dass Size < 32 nach der ersten Schleife). (@chux)
- Es gibt Argumente dafür, Ihre SSE/AVX-Aufrufe abzubrechen (@BenJackson, hier) und Argumente dagegen (@PaulR)
- Nicht-temporäre Übertragungen (mit denen Sie der CPU mitteilen, dass Sie den Zielspeicherort nicht zwischenspeichern müssen) sollten zum Kopieren größerer Puffer nützlich sein. (@Zboson)
@MichaelDorgan: Ich dachte auch, dass er/sie etwas Arkanes und Magisches tut, aber bei näherer Betrachtung ist es ziemlich einfach. Es sah für mich aus wie ein Pfeifenorgel-Arrangement …
– einpoklum
7. Oktober 2014 um 22:19 Uhr
Das ausdrucksstarke Arrangement gefällt mir sehr gut
switch
Geäst. Sieht ganz nett aus. 10/10 würde zugesagt 🙂– dom0
7. Oktober 2014 um 22:25 Uhr
“Wichtiges fehlendes Feature in dieser Implementierung” ist eine falsche Signatur. Erwartete Übereinstimmung mit:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
– chux – Wiedereinsetzung von Monica
7. Oktober 2014 um 22:31 Uhr
Selbst mit einem optimierenden Compiler kann man das nicht erkennen
switch (Size)
mit seinen 32 Fällen passtSize
Angebot0<=Size<32
. Vielleichtswitch (Size&31)
? Vermeiden Sie das intern Generierteif size > 31
.– chux – Wiedereinsetzung von Monica
7. Oktober 2014 um 22:34 Uhr
Beachten Sie, dass “restrict” nur für die Teile Ihres Codes ohne Intrinsic hilft. Einschränken mit Intrinsic ist nutzlos.
– Z-Boson
8. Oktober 2014 um 18:00 Uhr