самый быстрый способ преобразовать буфер изображения в смещение xy другого буфера в C ++ на архитектуре amd64

У меня есть буферы изображений произвольного размера, которые я копирую в буферы равного или большего размера со смещением x, y. Цветовое пространство - BGRA. Мой текущий метод копирования:

void render(guint8* src, guint8* dest, uint src_width, uint src_height, uint dest_x, uint dest_y, uint dest_buffer_width) {
    bool use_single_memcpy = (dest_x == 0) && (dest_y == 0) && (dest_buffer_width == src_width);

    if(use_single_memcpy) {
        memcpy(dest, src, src_width * src_height * 4);
    }
    else {
        dest += (dest_y * dest_buffer_width * 4);
        for(uint i=0;i < src_height;i++) {
            memcpy(dest + (dest_x * 4), src, src_width * 4);
            dest += dest_buffer_width * 4;
            src += src_width * 4;
        }
    }
}

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


person mpr    schedule 30.04.2015    source источник


Ответы (2)


Ваш use_single_memcpy тест слишком строгий. Небольшая перестановка позволяет убрать требование dest_y == 0.

void render(guint8* src, guint8* dest,
            uint src_width, uint src_height, 
            uint dest_x, uint dest_y,
            uint dest_buffer_width)
{
    bool use_single_memcpy = (dest_x == 0) && (dest_buffer_width == src_width);
    dest_buffer_width <<= 2;
    src_width <<= 2;
    dest += (dest_y * dest_buffer_width);

    if(use_single_memcpy) {
        memcpy(dest, src, src_width * src_height);
    }
    else {
        dest += (dest_x << 2);
        while (src_height--) {
            memcpy(dest, src, src_width);
            dest += dest_buffer_width;
            src += src_width;
        }
    }
}

Я также изменил цикл на обратный отсчет (который может быть более эффективным), удалил бесполезную временную переменную и отменил повторяющиеся вычисления.

Вполне вероятно, что вы можете еще лучше использовать встроенные функции SSE для копирования 16 байтов за раз вместо 4, но тогда вам нужно будет беспокоиться о выравнивании и кратных 4 пикселям. Хорошая реализация memcpy уже должна делать это.

person Ben Voigt    schedule 30.04.2015

Один популярный ответ на StackOverflow, в котором используется сборка x86-64 и SSE, можно найти здесь: Очень быстрый memcpy для обработки изображений?. Если вы все-таки используете этот код, вам нужно убедиться, что ваши буферы выровнены по 128-битному разряду. Основное объяснение этого кода:

  • Используются невременные хранилища, поэтому ненужные записи в кэш могут быть пропущены, а записи в основную память могут быть объединены.
  • Чтение и запись чередуются только очень большими порциями (выполняется много чтений, а затем много записей). Выполнение множества операций чтения друг за другом обычно дает лучшую производительность, чем отдельные шаблоны чтения-записи-чтения-записи.
  • Используются регистры гораздо большего размера (128-битные регистры SSE).
  • Инструкции предварительной выборки включены в качестве подсказок для конвейерной обработки ЦП.

Я нашел этот документ - Оптимизация доступа ЦП к памяти на рабочих станциях SGI Visual 320 и 540, который похоже, вдохновляет приведенный выше код, но для более старых поколений процессоров; тем не менее, в нем содержится значительное количество дискуссий о том, как это работает.

Например, рассмотрим это обсуждение объединяющих запись / невременных хранилищ:

Кэш-память ЦП Pentium II и III работает с блоками размером 32 байта со строкой кэша. Когда данные записываются в (кэшированную) память или считываются из нее, считываются или записываются целые строки кеша. Хотя это обычно увеличивает производительность ЦП и памяти, в некоторых случаях это может привести к ненужной выборке данных. В частности, рассмотрим случай, когда ЦП будет хранить 8-байтовое хранилище регистров MMX: movq. Поскольку это только четверть строки кэша, с точки зрения кеша это будет рассматриваться как операция чтения-изменения-записи; строка целевого кэша будет загружена в кэш, после чего произойдет 8-байтовая запись. В случае копирования в память эти извлеченные данные не нужны; последующие записи перезапишут оставшуюся часть строки кэша. Поведения чтения-изменения-записи можно избежать, если ЦП собирает все записи в строку кэша, а затем выполняет одну запись в память. Объединение отдельных записей в одну запись в строке кэша называется объединением записи. Объединение записи происходит, когда память, в которую выполняется запись, явно помечена как объединение записи (в отличие от кэширования или некэширования), или когда используется инструкция невременного сохранения MMX. Память обычно помечается как объединяющая запись только тогда, когда она используется в буферах кадра; память, выделенная VirtualAlloc, либо не кэшируется, либо кэшируется (но не с объединением записи). Команды MMX movntps и movntq невременного хранения инструктируют ЦП записывать данные непосредственно в память, минуя кеши L1 и L2. В качестве побочного эффекта он также позволяет комбинировать записи, если целевая память кэшируется.

Если вы предпочитаете использовать memcpy, рассмотрите возможность изучения исходного кода для реализации memcpy, которую вы используете. Некоторые реализации memcpy ищут буферы, выровненные по собственным словам, для повышения производительности за счет использования полного размера регистра; другие будут автоматически копировать как можно больше с выравниванием по родному слову, а затем убирать остатки. Обеспечение выравнивания буферов по 8 байт облегчит эти механизмы.

Некоторые реализации memcpy содержат массу предварительных условий, чтобы сделать его эффективным для небольших буферов (‹512) - вы можете рассмотреть возможность копирования и вставки кода с удаленными фрагментами, поскольку вы, вероятно, не работаете с небольшими буферами .

person antiduh    schedule 30.04.2015