Понимание реализации memcpy()

Я смотрел реализацию memcpy.c, я нашел другой memcpy-код. Я не мог понять, почему они делают ((ADDRESS) s) | ((ADDRESS) d) | c) и (sizeof (UINT) - 1)

#if !defined(__MACHDEP_MEMFUNC)

#ifdef _MSC_VER
#pragma function(memcpy)
#undef __MEMFUNC_ARE_INLINED
#endif

#if !defined(__MEMFUNC_ARE_INLINED)
/* Copy C bytes from S to D.
 * Only works if non-overlapping, or if D < S.
 */
EXTERN_C void * __cdecl memcpy(void *d, const void *s, size_t c)
{
    if ((((ADDRESS) s) | ((ADDRESS) d) | c) & (sizeof(UINT) - 1)) {

        BYTE *pS = (BYTE *) s;
        BYTE *pD = (BYTE *) d;
        BYTE *pE = (BYTE *) (((ADDRESS) s) + c);

        while (pS != pE)
            *(pD++) = *(pS++);
    }
    else {
        UINT *pS = (UINT *) s;
        UINT *pD = (UINT *) d;
        UINT *pE = (UINT *) (BYTE *) (((ADDRESS) s) + c);

        while (pS != pE)
            *(pD++) = *(pS++);
    }
    return d;
}

#endif /* ! __MEMFUNC_ARE_INLINED */
#endif /* ! __MACHDEP_MEMFUNC */

Ответ 1

Код проверяет, правильно ли выравниваются адреса для UINT. Если это так, код копирует с помощью объектов UINT. Если нет, код копирует с помощью объектов BYTE.

Тест работает, сначала выполнив побитовое ИЛИ двух адресов. Любой бит, который включен в любом из адресов, будет включен в результат. Затем тест выполняет побитовое И с sizeof(UINT) - 1. Ожидается, что размер a UINT является некоторой степенью двух. Тогда размер минус один имеет все нижние бит. Например, если размер 4 или 8, то один меньше, чем в двоичном формате 11 2 или 111 2. Если какой-либо адрес не кратен размеру UINT, тогда он будет иметь один из этих битов, и тест покажет его. (Как правило, наилучшее выравнивание для целочисленного объекта такое же, как и его размер. Это не обязательно так. Современная реализация этого кода должна использовать _Alignof(UINT) - 1 вместо размера.)

Копирование с объектами UINT выполняется быстрее, потому что на аппаратном уровне одна команда загрузки или хранения загружает или сохраняет все байты UINT (вероятно, четыре байта). Процессоры, как правило, копируют быстрее при использовании этих инструкций, чем при использовании в четыре раза больше однобайтовых инструкций загрузки или хранения.

Этот код, конечно, зависит от реализации; он требует поддержки реализации C, которая не является частью базового стандарта C, и зависит от конкретных особенностей процессора, в котором он выполняется.

Более продвинутая реализация memcpy может содержать дополнительные функции, такие как:

  • Если один из адресов выровнен, а другой - нет, используйте специальные инструкции, не привязанные к нагрузке, для загрузки нескольких байтов с одного адреса с регулярными инструкциями магазина на другой адрес.
  • Если процессор имеет инструкции с несколькими инструкциями Single Instruction Multiple Data, используйте эти инструкции для загрузки или хранения большого количества байтов (часто 16, возможно, больше) в одной команде.

Ответ 2

Код

((((ADDRESS) s) | ((ADDRESS) d) | c) & (sizeof(UINT) - 1))

Проверяет, не совпадают ли теги s, d или c с размером UINT.

Например, если s = 0x7ff30b14, d = 0x7ffa81d8, c = 256 и sizeof(UINT) == 4, то:

s         = 0b1111111111100110000101100010100
d         = 0b1111111111110101000000111011000
c         = 0b0000000000000000000000100000000
s | d | c = 0b1111111111110111000101111011100
(s | d | c) & 3 =                        0b00

Итак, оба указателя выровнены. Легче копировать память между указателями, которые выровнены, и это делает это только с одной ветвью.

Во многих архитектурах *(UINT *) ptr выполняется намного быстрее, если ptr правильно выровнен по ширине a UINT. На некоторых архитектурах *(UINT *) ptr на самом деле сбой, если ptr неправильно выровнен.