LINUX.ORG.RU

Какая-то странная реализация memcpy в glibc

 , ,


1

2

Взял мастер, т.е. всё более-менее свежее.

Имеем:

void *
MEMCPY (void *dstpp, const void *srcpp, size_t len)
{
  unsigned long int dstp = (long int) dstpp;
  unsigned long int srcp = (long int) srcpp;

  /* Copy from the beginning to the end.  */

  /* If there not too few bytes to copy, use word copy.  */
  if (len >= OP_T_THRES)
    {
      /* Copy just a few bytes to make DSTP aligned.  */
      len -= (-dstp) % OPSIZ;
      BYTE_COPY_FWD (dstp, srcp, (-dstp) % OPSIZ);

      /* Copy whole pages from SRCP to DSTP by virtual address manipulation,
	 as much as possible.  */

      PAGE_COPY_FWD_MAYBE (dstp, srcp, len, len);

      /* Copy from SRCP to DSTP taking advantage of the known alignment of
	 DSTP.  Number of bytes remaining is put in the third argument,
	 i.e. in LEN.  This number may vary from machine to machine.  */

      WORD_COPY_FWD (dstp, srcp, len, len);

      /* Fall out and copy the tail.  */
    }

  /* There are just a few bytes to copy.  Use byte memory operations.  */
  BYTE_COPY_FWD (dstp, srcp, len);

  return dstpp;
}

Вроде всё self-documented, но поясню: Смотрим, достаточно ли байтов, чтобы копировать пачкой или не стоит мараться. Если достаточно, копируем следующим образом:

  1. Сначала копируем байтами, чтобы выровнять адресацию до 8, чтобы можно было копировать по выровненному адресу сразу по страницам. (BYTE_COPY_FWD)
  2. Копируем по 8 байт чтобы добить до размера страницы (внутри PAGE_COPY_FWD_MAYBE)
  3. Копируем по страницам сколько можем (внутри PAGE_COPY_FWD_MAYBE)
  4. Копируем по 8 байт сколько можем (WORD_COPY_FWD)
  5. Копируем байтами остатки. (BYTE_COPY_FWD)

Так вот. Писал я себе подобный алгоритм для своей либы, вот только в режиме самой ядерной проверки компилем и асаном он мне говорил, что в граничных случаях такой алгоритм говно и я ничего не понимаю в алгоритмах, потому что:

(Для простоты опустим копирование страницами, для общего понимания проблемы оно сейчас не важно, тем более что в текущей реализации glibc для i386 оно ВНЕЗАПНО не реализовано) Положим на вход идут адреса 0x7f и 0x17f и нужно скопировать 20 байт.

  1. Чтобы добить до границы выравнивания по 8 копируем один байт из 0x17f в 0x7f (L теперь 19)
  2. Теперь копируем по 8 while (L>8).
  3. Добиваем остаток байтами.

Ура, мы скопировали. Никакого misalign access и все асаны-сасаны молчат.

А теперь берем адреса 0xf7 и 0x181

  1. Добиваем до границы 8 байт - копируем 1 байт. Теперь адреса 0x80 и 0x182
  2. Ииии… Всё, дальше копировать блоками нельзя - мы либо будем читать по невыравненному адресу либо писать, если попытаемся выровняться по источнику. ASAN и в том и другом случае мне жужжит, что если я хочу быть пай-мальчиком, то так нельзя.

Вопросы:

  • Почему у glibc это канает? Я прожамкал их сорцы, там нет решения этой проблемы.

Чтобы качнуть сорцы: git clone git://sourceware.org/git/glibc.git

P.S. Если знаете кого-то, кто на форуме может теоретически ответить - кастаните в тред, плиз. P.P.S. Это, конечно, забавно, что gcc на -O0/1/2 игнорит оптимизацию алгоритма и разворачивает его только на -О3 (xmm во все поля): https://gcc.godbolt.org/z/LpJg6s , но странно, что такой же реализации нет в glibc - одна из них все равно медленнее, почему не заменить более медленную реализацию более быстрой в одном из источников?

★★★★★

Если надо через uint64_t копировать из одного места в другое, притом если оно в одном месте выровнено, в другом нет, то тут может помочь typedef __attribute__((aligned(1), may_alias)) uint64_t My_uint64_t; - вот указателем на такой My_uint64_t можешь спокойно писать и читать по невыровненным адресам

SZT ★★★★★
()
Последнее исправление: SZT (всего исправлений: 1)

А это точно та реализация, которая используется на x86-64, а не fallback-версия? Там же есть директория sysdeps/, в которой лежат архитектурно-специфичные реализации и у них приоритет больше.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 1)
Ответ на: комментарий от xaizek

Код выше - базовый код для всех архитектур. Архитектурно-зависимые только развертки макросов, поэтому в целом алгоритм один и тот же.

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от SZT

можешь спокойно писать и читать по невыровненным адресам

Не могу. На x86 сработает, но будет дрочить проц по misalign access, на остальных арках - зависит от удачи - может упасть, может работать. По факту misalign никуда не девается в вашем примере.

Из ссылки выше: «Кстати, начиная с архитектуры «Nehalem», невыровненый доступ в память, с которым борется вышеприведенный код, больше не является важнейшей причиной тормозов, хотя и не бесплатен.»

Поэтому такой доступ будет лагать

PPP328 ★★★★★
() автор топика
Последнее исправление: PPP328 (всего исправлений: 1)
Ответ на: комментарий от SZT
Как неоднократно просили в комментах, прогнал простую микробенчмарку с memcpy пары килобайт со всеми комбинациями выравнивания в цикле.
Цифра — во сколько раз самый продвинутый SSE4.1 код быстрее, чем std::memcpy, реализованный через rep movs
Bulldozer — 1.22x (спасибо stepmex за данные)
Penryn — 1.6x
Nehalem — 1.5x
Sandy Bridge — 1.008x

Ну то есть вся статья - свистёж чистой воды, реализация на SSE всё еще быстрее.

PPP328 ★★★★★
() автор топика
Последнее исправление: PPP328 (всего исправлений: 1)
Ответ на: комментарий от PPP328

Ну если там при копировании чтение-запись происходит по неразрешенным смещениям, ну типа

addr|00|01|02|03|04|05|06|07|..|A0|A1|A2|A3|A4|A5|A6|A7|
    | uint32t_1 | uint32t_2 |..| uint32t_3 | uint32t_4 |
    |  |  |  |  |  |  |  |  |..|  |  |  |  |  |  |  |  |
Из  |  |^^|^^|^^|^^|^^|^^|^^|..|  |  |  |  |  |  |  |  |
В   |  |  |  |  |  |  |  |  |..|  |  |^^|^^|^^|^^|^^|^^|

То тоже можно что-нибудь придумать, например читать uint32t_1, маской убрать четвертый байт, сдвинуть потом на 1 байт вправо (и сохраняем, этот байт нам потом понадобится) и записать в uint32t_3, после чего читаем uint32t_2, сдвигаем вправо на 8(сохранив опять-таки вытолкнутый байт), и тот старый байт что был сохранен на предыдущем этапе из uint32t_1 - мы его суем в старший байт этой хрени, и потом записываем в uint32_4 ... в общем сложная возня со сдвигами которая не факт что будет быстро работать

SZT ★★★★★
()
Последнее исправление: SZT (всего исправлений: 1)
Ответ на: комментарий от PPP328

Ну то есть вся статья - свистёж чистой воды, реализация на SSE всё еще быстрее.

Быстрее на 1.008x - это вообще мизерная разница

SZT ★★★★★
()
Последнее исправление: SZT (всего исправлений: 1)
Ответ на: комментарий от SZT

Я пробовал копировать так в своей реализации - даже на голом асме оно лагало почти так же как простой неоптимизаированный *d++=*s++

PPP328 ★★★★★
() автор топика
Ответ на: комментарий от PPP328

даже на голом асме оно лагало

Попробуй инструкцию SHLD заюзать. Можно еще на SSE насочинять какого-то бреда

SZT ★★★★★
()
Ответ на: комментарий от PPP328

Да и при копировании у тебя наверняка упрется в пропускную способность шины оперативной памяти (или той шины, которая связана с кеш-памятью процессора), и ты никакими крутыми ассемблерными инструкциями ничего не сможешь ускорить.

А еще можно через DMA из памяти в память копировать https://mkprog.ru/mikrokontrollery-stm32/stm32-dlya-nachinayushhih-urok-6-dma...

SZT ★★★★★
()

Для asan норма не пропустить код glibc. при работе asan используется свой memcpy, strcmp, strcpy - все они если их скопироваиь к себе код будут валиться на asan. glibc учитывает что границы доступной для чтения памяти в glibc всегда выравнены по 8, а asan нет

mittorn ★★★★★
()
Ответ на: комментарий от PPP328

Я так не думаю. Например, в sysdeps/i386/i686/multiarch/memcpy.S определяется символ memcpy:

ENTRY(memcpy)

В остальных файлах аналогичное. PAGE_COPY_FWD_MAYBE и подобное это «лёгкая» кастомизация для архитектур, где реализации с общей структурой хватает. Остальные переопределяют реализацию memcpy полностью.

xaizek ★★★★★
()
Ответ на: комментарий от SZT

в общем сложная возня со сдвигами которая не факт что будет быстро работать

Медленнее, скорее всего. Просто за счёт увеличенного числа инструкций и дополнительных ветвлений или раздутого кода.

Кстати, под большим вопросом снижение скорости невыровненной записи. Пишем же в кеш, который потом сбрасывается в память уже полными линиями.

i-rinat ★★★★★
()

Ииии… Всё, дальше копировать блоками нельзя - мы либо будем читать по невыравненному адресу либо писать, если попытаемся выровняться по источнику. ASAN и в том и другом случае мне жужжит, что если я хочу быть пай-мальчиком, то так нельзя.

Почему у glibc это канает? Я прожамкал их сорцы, там нет решения этой проблемы.

Видимо, ты плохо жамкал.

https://code.woboq.org/userspace/glibc/sysdeps/generic/memcopy.h.html#_M/WORD_COPY_FWD

#define WORD_COPY_FWD(dst_bp, src_bp, nbytes_left, nbytes)                      \
  do                                                                              \
    {                                                                              \
      if (src_bp % OPSIZ == 0)                                                      \
        _wordcopy_fwd_aligned (dst_bp, src_bp, (nbytes) / OPSIZ);              \
      else                                                                      \
        _wordcopy_fwd_dest_aligned (dst_bp, src_bp, (nbytes) / OPSIZ);              \
      src_bp += (nbytes) & -OPSIZ;                                              \
      dst_bp += (nbytes) & -OPSIZ;                                              \
      (nbytes_left) = (nbytes) % OPSIZ;                                              \
    } while (0)

_wordcopy_fwd_dest_aligned разруливает случай, когда только dest выровнен.

Сильно не вникал, но, похоже, что тут читается из сорца выровненными кусочками из которых с помощью битовых сдвигов (MERGE) собирается значение для записи в dest.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.