Копирование данных с шагом в C++

У меня есть два массива, и я хочу скопировать один массив в другой с некоторым шагом. Например, у меня есть

A A A A A A A A ...

B B B B B B B B ...

и я хочу скопировать каждые три элемента B в A, чтобы получить

B A A B A A B A ...

Из сообщения "Существует ли стандартная, расширенная версия memcpy?", кажется, в C такой возможности нет.

Однако я убедился, что в некоторых случаях memcpy работает быстрее, чем копия на основе цикла for.

Мой вопрос; Есть ли способ эффективно выполнять пошаговое копирование памяти в C++, выполняя хотя бы стандартный цикл for?

Большое тебе спасибо.

РЕДАКТИРОВАНИЕ – ПОЯСНЕНИЕ ПРОБЛЕМЫ

Чтобы сделать проблему более ясной, давайте обозначим два массива под рукой a и b. У меня есть функция, которая выполняет уникальный следующий цикл for

for (int i=0; i<NumElements, i++)
    a_[i] = b_[i];

где оба [] являются перегруженными операторами (я использую технику шаблонов выражений), так что они могут быть фактически означающими, например

 a[3*i]=b[i];

person Vitality    schedule 13.06.2013    source источник
comment
Стандартный цикл for работает, по крайней мере, так же быстро, как и стандартный цикл... Сарказм в сторону, это зависит от структуры хранения данных, которую вы используете. Для массивов я не думаю, что вы можете сделать что-то лучше, чем цикл for, увеличенный на ваш модуль.   -  person ChrisCM    schedule 13.06.2013
comment
memcpy иногда работает быстрее, чем цикл for из-за оптимизации, которую он может выполнять, потому что память, с которой он работает, непрерывна. Эти оптимизации не могут быть сделаны здесь.   -  person Collin Dauphinee    schedule 13.06.2013
comment
@dauphic Но тогда почему у CUDA есть cudaMemcpy2D, который копирует с шагом?   -  person Vitality    schedule 14.06.2013
comment
@JackOLantern: CUDA работает параллельно.   -  person Collin Dauphinee    schedule 14.06.2013
comment
@JackOLantern Потому что cudaMemcpy2D выполняется параллельно на графическом процессоре, установленном на устройстве, а memcpy выполняется на самом устройстве.   -  person Captain Obvlious    schedule 14.06.2013
comment
@dauphic @CaptainObvlious В параллельном программировании на графических процессорах термин «устройство» обычно означает «ГП», а «хост» — «ЦП» :-) Но в любом случае я пытаюсь сказать, что транзакции памяти в CUDA оптимизированы для шаблона объединенного доступа, а не для пошагового доступа и, тем не менее, пошаговый доступ доступен cudaMemcpy2D.   -  person Vitality    schedule 14.06.2013


Ответы (2)


Есть ли способ эффективно выполнять копирование памяти с шагом в C++, выполняя по крайней мере стандарт цикла for?

Редактировать 2: в библиотеках C++ нет функции пошагового копирования.

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

Предполагая стандартный цикл for, вы можете повысить производительность, используя развертку цикла. Некоторые компиляторы имеют опции для развертывания циклов; это не "стандартный" вариант.

Учитывая стандартный цикл for:

#define RESULT_SIZE 72
#define SIZE_A 48
#define SIZE_B 24

unsigned int A[SIZE_A];
unsigned int B[SIZE_B];
unsigned int result[RESULT_SIZE];

unsigned int index_a = 0;
unsigned int index_b = 0;
unsigned int index_result = 0;
for (index_result = 0; index_result < RESULT_SIZE;)
{
   result[index_result++] = B[index_b++];
   result[index_result++] = A[index_a++];
   result[index_result++] = A[index_a++]; 
}

Развертывание цикла повторит содержимое «стандартного» цикла for:

for (index_result = 0; index_result < RESULT_SIZE;)
{
   result[index_result++] = B[index_b++];
   result[index_result++] = A[index_a++];
   result[index_result++] = A[index_a++]; 

   result[index_result++] = B[index_b++];
   result[index_result++] = A[index_a++];
   result[index_result++] = A[index_a++]; 
}

В развернутой версии количество петель уменьшено вдвое.

Улучшение производительности может быть незначительным по сравнению с другими вариантами. Следующие проблемы влияют на производительность, и каждая из них может иметь различные улучшения скорости:

  • Кэш обработки данных отсутствует
  • Перезагрузка конвейера инструкций (зависит от процессора)
  • Операционная система обменивается памятью с диском
  • Другие задачи, выполняемые одновременно
  • Параллельная обработка (зависит от процессора/платформы)

Одним из примеров параллельной обработки является то, что один процессор копирует элементы B в новый массив, а другой процессор копирует элементы A в новый массив.

person Thomas Matthews    schedule 13.06.2013
comment
Спасибо за добрый ответ. Я отредактировал свой пост, чтобы лучше объяснить проблему. Считаете ли вы, что к #pragma unroll у меня есть шанс улучшить ситуацию? Я не знаю, так как все о копии известно во время выполнения. - person Vitality; 14.06.2013
comment
Как я уже сказал, зависит от процессора. В некоторых процессорах ветвь очищает конвейер инструкций, и процессор должен перезагрузить его. У некоторых современных процессоров достаточно кеша конвейера инструкций, чтобы они содержали простой цикл for в кеше инструкций и не нуждались в перезагрузке. - person Thomas Matthews; 14.06.2013
comment
Большинство процессоров предпочитают выполнять последовательные инструкции и предпочли бы не сталкиваться с инструкциями ветвления. - person Thomas Matthews; 14.06.2013
comment
Я предпочитаю не использовать компилятор pragma, а разворачивать циклы. Это позволяет использовать более переносимый код, и я могу контролировать множественность контента. В своей работе я развернул цикл, обрабатывающий 32-слотовый FIFO с 32 отдельными операторами; работает очень быстро. - person Thomas Matthews; 14.06.2013
comment
Я понимаю вашу точку зрения, что pragma может вызвать проблемы с переносимостью кода, но в целом я мало знаю об упомянутом цикле for, поэтому я не могу выполнить развертывание цикла вручную. Не могли бы вы отредактировать свой пост, указав короткое и окончательное заявление о том, что нет встроенной функции C ++ для выполнения пошаговых копий, чтобы я мог принять ваш ответ? Спасибо. - person Vitality; 14.06.2013

Может быть, это слишком конкретный ответ, но на платформе ARM, поддерживающей NEON, можно использовать векторизацию NEON, чтобы сделать пошаговое копирование еще быстрее. Это может спасти жизнь в среде, где ресурсы относительно более ограничены, и, вероятно, именно поэтому ARM в первую очередь используется в таких условиях. Ярким примером является Android, где большинство устройств по-прежнему используют архитектуру ARM v7a, поддерживающую NEON.

Следующие примеры демонстрируют это. Это цикл для копирования полуплоской UV-плоскости изображения YUV420sp в плоскую UV-плоскость изображения YUV420p. Размеры исходного и целевого буферов равны 640*480/2 байтам. Все примеры скомпилированы с помощью g++ 4.8 внутри Android NDK r9d. Они выполнены на процессоре Samsung Exynos Octa 5420:

Уровень 1: обычный

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride)
{
    for(int i=0;i<stride;i++){
        dstptr[i]           = srcptr[i*2];
        dstptr[i + stride]  = srcptr[i*2 + 1];
    }
}

Скомпилировано только с -O3, занимает в среднем около 1,5 мс.

Уровень 2: развернуто и немного сжато с помощью движущихся указателей.

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride)
{
    unsigned char* endptr = dstptr + stride;
    while(dstptr<endptr){
        *(dstptr + 0)             = *(srcptr + 0);
        *(dstptr + stride + 0)    = *(srcptr + 1);
        *(dstptr + 1)             = *(srcptr + 2);
        *(dstptr + stride + 1)    = *(srcptr + 3);
        *(dstptr + 2)             = *(srcptr + 4);
        *(dstptr + stride + 2)    = *(srcptr + 5);
        *(dstptr + 3)             = *(srcptr + 6);
        *(dstptr + stride + 3)    = *(srcptr + 7);
        *(dstptr + 4)             = *(srcptr + 8);
        *(dstptr + stride + 4)    = *(srcptr + 9);
        *(dstptr + 5)             = *(srcptr + 10);
        *(dstptr + stride + 5)    = *(srcptr + 11);
        *(dstptr + 6)             = *(srcptr + 12);
        *(dstptr + stride + 6)    = *(srcptr + 13);
        *(dstptr + 7)             = *(srcptr + 14);
        *(dstptr + stride + 7)    = *(srcptr + 15);
        srcptr+=16;
        dstptr+=8;
    } 
}

Скомпилировано только с -O3, занимает в среднем около 1,15 мс. Согласно другому ответу, это, вероятно, так же быстро, как и в обычной архитектуре.

Уровень 3: обычная + автоматическая векторизация NEON GCC.

void convertUVsp2UVp(
    unsigned char* __restrict srcptr, 
    unsigned char* __restrict dstptr, 
    int stride)
{
    for(int i=0;i<stride;i++){
        dstptr[i]           = srcptr[i*2];
        dstptr[i + stride]  = srcptr[i*2 + 1];
    }
}

Скомпилировано с помощью -O3 -mfpu=neon -ftree-vectorize -ftree-vectorizer-verbose=1 -mfloat-abi=softfp, занимает в среднем около 0,6 мс. Для справки, memcpy из 640*480 байтов, или вдвое больше, чем протестировано здесь, занимает в среднем около 0,6 мс.

В качестве примечания: второй код (развернутый и с указателем), скомпилированный с указанными выше параметрами NEON, занимает примерно столько же времени, 0,6 мс.

person Ayberk Özgür    schedule 07.10.2014