Я заинтересовался написанием memcpy() в качестве учебного упражнения. Я не буду писать целый трактат о том, что я сделал и о чем не думал, но здесь о реализации парней:
__forceinline   //因为通常Size已知,内联后编译器可以优化掉大部分无用代码
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;
}
Комментарий переводится как "Размер обычно известен, поскольку компилятор может оптимизировать встроенный код наиболее бесполезно".
Я хотел бы улучшить, если это возможно, эту реализацию, но, возможно, не так много улучшений. Я вижу, что он использует SSE/AVX для больших кусков памяти, а затем вместо цикла по последним <32 байтам выполняется эквивалент развертывания вручную с некоторыми изменениями. Итак, вот мои вопросы:
- Зачем развернуть цикл для последних нескольких байтов, но не частично развернуть первый (и теперь единственный) цикл?
- Как насчет вопросов выравнивания? Разве они не важны? Должен ли я по-разному обрабатывать первые несколько байтов вплоть до некоторого кванта выравнивания, а затем выполнять 256-битные операции для выровненных последовательностей байтов? И если да, то как определить соответствующий квант выравнивания?
- Какая самая важная недостающая особенность в этой реализации (если есть)?
Особенности/принципы, упомянутые в ответах до сих пор
-  Вы должны __restrict__ваши параметры. (@Chux)
- Пропускная способность памяти является ограничивающим фактором; сравните вашу реализацию с этим. (@Zboson)
- Для небольших массивов вы можете ожидать приблизиться к пропускной способности памяти; для больших массивов - не так много. (@Zboson)
- Несколько потоков (может быть | есть) необходимы для насыщения пропускной способности памяти. (@Zboson)
- Вероятно, целесообразно по-разному оптимизировать копии больших и малых размеров. (@Zboson)
- (Выравнивание важно? Не указано явно!)
- Компилятор должен быть более четко осведомлен о "очевидных фактах", которые он может использовать для оптимизации (например, тот факт, что Size <32 после первого цикла). (@Chux)
- Существуют аргументы для развертывания ваших вызовов SSE/AVX (@BenJackson, здесь) и аргументы против этого (@PaulR)
- Временные передачи (с помощью которых вы говорите ЦПУ, что вам не нужно кэшировать целевое местоположение) должны быть полезны для копирования больших буферов. (@Zboson)
