Оптимизация в реализации Memcpy

Ниже приведен код, который я получил при поиске оптимизированной реализации memcpy. Вот ссылка на сайт

void *memcpy(void *dst, void const *src, size_t len) {
    long *plDst = (long *)dst;
    long const *plSrc = (long const *)src;

    if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC)) {
        while (len >= 4) {
            *plDst++ = *plSrc++;
            len -= 4;
        }
    }

    char *pcDst = (char *)plDst;
    char const *pcDst = (char const *)plSrc;

    while (len--) {
         *pcDst++ = *pcSrc++;
    }

    return (dst);
}

Может кто-нибудь объяснить мне строчку ниже?

if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC))

Здесь они хотят проверить, совпадают ли адреса src и dst с границей 4 byte или нет. Но почему они используют !, поскольку он каждый раз будет вызывать состояние false?

Во-вторых, есть ли возможности для дополнительной оптимизации в приведенном выше коде?

Как вы думаете, почему условие может быть ложным каждый раз?

Gerhardh 24.07.2018 11:12

Похоже на опечатку в статье - наверное, должно быть: if (!(src & ~0xFFFFFFFC) && !(dst & ~0xFFFFFFFC)).

Paul R 24.07.2018 11:14

Для проверки выравнивания вы можете использовать !(src&3) && !(dst&3). Условие во фрагменте проверяет что-то другое.

Gerhardh 24.07.2018 11:14

Также первый комментарий к этой статье замечает эту опечатку.

Karsten Koop 24.07.2018 11:16

Спасибо за комментарий. Расклад теперь ясен. Можно ли еще немного оптимизировать код, чтобы он работал быстрее? Что, если бы мы не проверяли выравнивание по 4 байтам?

Abhi 24.07.2018 11:53

Если ваш процессор не разрешает 32-битный доступ для чтения к невыровненным адресам, вы можете получить SEGFAULT или неправильный контент. Или вы можете столкнуться с огромным штрафом в скорости доступа к памяти ....

Gerhardh 24.07.2018 12:18

Чтобы быть действительно оптимизированным для больших передач, он должен проверить, выровнено ли начало - скопировать байт за байтом невыровненный, скопировать исходную длину

0___________ 24.07.2018 12:24

@Abhi, вы можете расширить код, используя больше особых случаев (например, для 64-битной версии), но вы должны быть осторожны здесь, поскольку компилятор также может оптимизировать код.

Domso 24.07.2018 12:25

Хорошо. Адрес должен быть выровнен, но требуется ли выравнивание как src, так и dest. В противном случае мы можем использовать метод, указанный @ P__J__

Abhi 24.07.2018 12:39
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
9
1 140
1

Ответы 1

Эта статья, посвященная интересной теме, не дает правильных примеров. Опубликованный код упоминается как Исходный код новой библиотеки GNU. И проект GNU, и команда newlib будут удивлены, узнав об этом неожиданном заявлении о конвергенции! newlib не является проектом GNU, и большая часть его исходного кода не находится под лицензией GPL.

Эта реализация memcpyоптимизированный непереносима, неоптимальна и во многих аспектах неверна.

Тест if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC)) пытается определить, совпадают ли адреса src и dst на границах long. Это громоздко и непереносимо по нескольким причинам и, как вы заметили, совершенно неверно:

  • неявное преобразование из void * в int некрасиво и определяется реализацией. Указатели должны иметь вид (uintptr_t) для лучшей переносимости.
  • 0xFFFFFFFC предполагает, что тип long составляет 4 байта. Это может быть неверно, и действительно, тип long имеет длину 8 байт в 64-битных системах Linux и Mac.
  • src & 0xFFFFFFFC не является проверкой на выравнивание и вряд ли будет 0, предполагаемый тест на выравнивание по 4-байтовым границам - это src & 3.

Кроме того, код не может оптимизировать случай, когда src и dst имеют одинаковое выравнивание, но не выравниваются по границам long.

Другие возможные улучшения включают развертывание циклов, использование переключателя для малых значений len, объединение байтов, считанных из src, в long для записи в dst после его выравнивания по границам long ...

Вот улучшенная альтернатива:

#include <stdint.h>

void *memcpy(void *dst, void const *src, size_t len) {
    unsigned char *pcDst = (unsigned char *)dst;
    unsigned char const *pcSrc = (unsigned char const *)src;

    if (len >= sizeof(long) * 2
    &&  ((uintptr_t)src & (sizeof(long) - 1)) == ((uintptr_t)dst & (sizeof(long) - 1))) {
        while (((uintptr_t)pcSrc & (sizeof(long) - 1)) != 0) {
            *pcDst++ = *pcSrc++;
            len--;
        }
        long *plDst = (long *)pcDst;
        long const *plSrc = (long const *)pcSrc;
        /* manually unroll the loop */
        while (len >= sizeof(long) * 4) {
            plDst[0] = plSrc[0];
            plDst[1] = plSrc[1];
            plDst[2] = plSrc[2];
            plDst[3] = plSrc[3];
            plSrc += 4;
            plDst += 4;
            len -= sizeof(long) * 4;
        }
        while (len >= sizeof(long)) {
            *plDst++ = *plSrc++;
            len -= sizeof(long);
        }
        pcDst = (unsigned char *)plDst;
        pcSrc = (unsigned char const *)plSrc;
    }
    while (len--) {
         *pcDst++ = *pcSrc++;
    }
    return dst;
}

Обратите внимание, что приведение из void * не требуется в C, но требуется в C++.

Вот некоторые важные моменты, о которых следует помнить при попытке оптимизировать код для повышения скорости:

  • никаких компромиссов в отношении правильности. Быстрый код, который не работает даже в пограничных случаях, бесполезен.
  • остерегайтесь проблем с переносимостью: решения, зависящие от размера типа и / или порядка байтов, могут создавать проблемы с переносимостью.
  • бенчмаркинг покажет, что работает, а что нет: вы должны тщательно рассчитать время для различных альтернатив в различных тестовых примерах.
  • идеального решения не существует: более быстрый код на одной архитектуре может быть медленнее на другой, в том числе и наиболее проблематично на будущей. Тестирование на разных архитектурах.
  • бороться со сложностью: избегайте запутанных решений, которые не приносят существенных улучшений, их правильность сложнее доказать и поддерживать.
  • memcpy обычно оптимизируется в сборке или реализуется как встроенный в современных компиляторах.

Я бы посоветовал проверить наличие len <= 8, прежде чем делать что-либо еще, и использовать оператор switch для обработки коротких операций копирования. Это позволит избежать необходимости проверять, можете ли вы копировать данные до точки, в которой источник и / или место назначения совпадают. Я бы посоветовал копировать данные до тех пор, пока место назначения не будет выровнено, а затем, если источник не выровнен, а копия «большая», иметь основной цикл примерно как x = *adjustedSrc++; *dest++ = (x << shift1) | (y >> shift2); y=x;, что позволяет избежать необходимости разбивать операции на более мелкие фрагменты.

supercat 27.07.2018 22:46

@supercat: хорошие моменты. Что касается объединения невыровненных исходных байтов в long для сохранения в выровненный адрес назначения, легко упустить из виду проблему порядка байтов. В зависимости от архитектуры это может быть даже не быстрее. Фактически, на целевых объектах x86, где разрешены невыровненные операции чтения и записи, простой цикл чтения / записи через 64-битный тип может быть самым быстрым решением. Бенчмаркинг - это судья.

chqrlie 28.07.2018 11:03

Не говоря уже о том, что оптимизация memcpy() очень зависит от процессора. На x86 часто лучше всего или, по крайней мере, достаточно разумно использовать memcpy-opcode.

Deduplicator 28.07.2018 11:28

Другие вопросы по теме