Как мне лучше всего инициализировать массив локальной памяти до 0?

(Есть аналогичные вопросы о массивах памяти устройств (глобальных), например, мой собственный вопрос.)

Предположим, у меня есть код ядра CUDA, который выглядит следующим образом:

my_arr[MyCompileTimeConstant];

/* ... */

for(unsigned i = 0; i < foo(); i++) {
   my_arr[bar(i)] += some_value;
}

Теперь я хочу инициализировать my_arr всеми нулями, прежде чем я начну добавлять к его записям. Могу ли я сделать лучше, чем тривиальный цикл

for(unsigned i = 0; i < MyCompileTimeConstant; i++) {
   my_arr[i] = 0;
}

?

Примечание. Я специально сделал константы диапазона цикла и размера массива известными во время компиляции. Вопрос был бы немного другим, если бы они были переданы во время выполнения. Конечно, это может не изменить ответ для CUDA, как для кода, работающего на ЦП.


person einpoklum    schedule 17.05.2014    source источник
comment
Я считаю, что memset также будет работать, хотя это не официально задокументировано AFAIK. Лучше или нет, не знаю.   -  person Robert Crovella    schedule 17.05.2014
comment
@RobertCrovella: Как и в случае с memcpy, это будет хуже, потому что компилятор создает простые циклы, выполняющие передачу размера байта.   -  person talonmies    schedule 17.05.2014
comment
@talonmies Я не знаю о генерации кода графическим процессором, но, по крайней мере, для кода ЦП, в целом это неверно. Компиляторы распознают memcpy и тому подобное. Для небольших постоянных размеров они встраиваются, разворачиваются и оптимизируются так же, как и любое обычное присваивание. Для больших динамических размеров реализация в библиотеке времени выполнения обычно векторизуется с минимальными накладными расходами (необходимо проверить alignemnt и, возможно, скопировать несколько байтов вручную, чтобы исправить это).   -  person    schedule 17.05.2014
comment
@delnan: я конкретно имел в виду компилятор NVIDIA GPU, как показано здесь: stackoverflow.com/a/10468720/681865 . Это вопрос CUDA, и я имею в виду только это.   -  person talonmies    schedule 17.05.2014


Ответы (1)


Простой цикл должен быть «лучшим» подходом (но см. окончательный комментарий ниже). В качестве примера используем следующее ядро:

template<int version>
__global__
void tkernel(int *A, int *B, int *C, int n)
{
    int biglocal[100];

    switch(version) {
        case 1:
            for(int i=0; i<100; i++) {
                biglocal[i] = 0;
            };

            break;

        case 2:
            memset(&biglocal[0], 0, 100*sizeof(int));
            break;


        case 3:
            const int4 zero = {0, 0, 0, 0};
            int4 *p = reinterpret_cast<int4*>(&biglocal[0]);
#pragma unroll
            for(int i=0; i<100/4; i++) {
                p[i] = zero;
            }

            break;
    }

    if (n>0) {
        for(int i=0; i<100; i++) {
            biglocal[A[threadIdx.x*i]] += B[threadIdx.x*i];
        }
        C[threadIdx.x] = biglocal[n];
    }
}

template __global__ void tkernel<1>(int *, int *, int *, int);
template __global__ void tkernel<2>(int *, int *, int *, int);
template __global__ void tkernel<3>(int *, int *, int *, int);

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

Глядя на PTX, выдаваемые для целей Compute 2.1 с помощью компилятора выпуска CUDA 6, обе версии 1 и 3 выглядят следующим образом:

.local .align 4 .b8     __local_depot0[400];
.reg .b64   %SP;
.reg .b64   %SPL;
.reg .pred  %p<3>;
.reg .s32   %r<67>;
.reg .s64   %rd<73>;


mov.u64     %SPL, __local_depot0;
ld.param.u64    %rd4, [_Z7tkernelILi1EEvPiS0_S0_i_param_0];
ld.param.u64    %rd5, [_Z7tkernelILi1EEvPiS0_S0_i_param_1];
ld.param.u64    %rd6, [_Z7tkernelILi1EEvPiS0_S0_i_param_2];
ld.param.u32    %r21, [_Z7tkernelILi1EEvPiS0_S0_i_param_3];
add.u64     %rd7, %SPL, 0;
mov.u32     %r66, 0;
st.local.u32    [%rd7], %r66;
st.local.u32    [%rd7+4], %r66;
st.local.u32    [%rd7+8], %r66;
st.local.u32    [%rd7+12], %r66;
st.local.u32    [%rd7+16], %r66;
st.local.u32    [%rd7+20], %r66; 

    // etc

т.е. компилятор развернул цикл и выдал строку 32-битных инструкций сохранения. Уловка int4 в версии 3 давала такой же код, как и простой цикл, что немного удивительно. Однако версия 2 получает следующее:

.local .align 4 .b8     __local_depot1[400];
.reg .b64   %SP;
.reg .b64   %SPL;
.reg .pred  %p<4>;
.reg .s16   %rs<2>;
.reg .s32   %r<66>;
.reg .s64   %rd<79>;


mov.u64     %SPL, __local_depot1;
ld.param.u64    %rd7, [_Z7tkernelILi2EEvPiS0_S0_i_param_0];
ld.param.u64    %rd8, [_Z7tkernelILi2EEvPiS0_S0_i_param_1];
ld.param.u64    %rd9, [_Z7tkernelILi2EEvPiS0_S0_i_param_2];
ld.param.u32    %r21, [_Z7tkernelILi2EEvPiS0_S0_i_param_3];
add.u64     %rd11, %SPL, 0;
mov.u64     %rd78, 0;

BB1_1:
add.s64     %rd12, %rd11, %rd78;
mov.u16     %rs1, 0;
st.local.u8     [%rd12], %rs1;
add.s64     %rd78, %rd78, 1;
setp.lt.u64 %p1, %rd78, 400;
@%p1 bra    BB1_1;

т.е. цикл, который выполняет 8-битную запись (комментарии указывают, что простая инициализация списка также дает этот тип цикла копирования). Последний будет намного медленнее, чем первый. Помимо разницы в размерах хранилищ, развернутый поток операций записи полностью независим и может выполняться в любом порядке, что обеспечит заполнение конвейера инструкций и должно привести к более высокой пропускной способности инструкций. Я не верю, что в развернутом случае удастся превзойти компилятор, и простой цикл, похоже, даст тот же код, что и простая попытка векторизации. Если вы действительно заинтересованы, я думаю, вы могли бы попробовать встроенный PTX для создания более широких хранилищ. Я не знаю, будет ли при этом какое-либо преимущество в производительности.

person talonmies    schedule 18.05.2014
comment
Для полноты картины я также попробовал списки инициализации, такие как 'int biglocal[100] = {0};' который дает тот же ptx, что и вариант memset... - person kunzmi; 18.05.2014
comment
@kunzmi: Спасибо за дополнительные данные, которые полезно знать. Я отредактировал примечание к вопросу, отражающему это. Предположительно, код стиля memset использует 32-битное слово, а не 8-битное слово? - person talonmies; 18.05.2014
comment
Я перепроверил это, и это зависит от того, что вы на самом деле делаете. Важным моментом здесь является то, что приведенный список не является полным. Данные элементы массива будут установлены как в методе с циклами (с 32-битными словами), тогда как неопределенные элементы массива будут установлены в ноль в «мемсет-стиле» с использованием 8-битных слов. Как и в моем предыдущем примере, заданное значение также равно нулю, первый шаг оптимизируется компилятором, и остается только часть, установленная на ноль. - person kunzmi; 18.05.2014