Быстрое (est) способ записи последовательности целого в глобальную память?

Задача очень проста, выписывая последовательность целых переменных в память:

Исходный код:

for (size_t i=0; i<1000*1000*1000; ++i)
{
   data[i]=i;
};

Параллельный код:

    size_t stepsize=len/N;

#pragma omp parallel num_threads(N)
    {
        int threadIdx=omp_get_thread_num();

        size_t istart=stepsize*threadIdx;
        size_t iend=threadIdx==N-1?len:istart+stepsize;
#pragma simd
        for (size_t i=istart; i<iend; ++i)
            x[i]=i;
    };

Производительность отстойная, требуется 1,6 сек для записи переменных 1G uint64 (что равно 5 ГБ в секунду), путем простого распараллеливания (open mp parallel) вышеуказанного кода, скорости увеличение abit, но производительность все еще отстойная, возьмите 1,4 с с 4 потоками и 1,35 с 6 потоками на i7 3970.

Пространственная пропускная способность памяти моей платформы (i7 3970/64G DDR3-1600) 51,2 ГБ/с, для приведенного выше примера достигнутая пропускная способность памяти около 1/10 теоретической полосы пропускания, даже через приложение в значительной степени ограничено пропускной способностью.

Кто-нибудь знает, как улучшить код?

Я написал много кода с привязкой к памяти на GPU, довольно просто для GPU, чтобы в полной мере использовать пропускную способность памяти устройства GPU (например, 85% + теоретической полосы пропускания).

EDIT:

Код компилируется Intel ICC 13.1, до 64-битного двоичного кода и с максимальным оптимизацией (O3) и AVX-кодом, а также с автоматической автолизацией.

UPDATE:

Я пробовал все коды ниже (спасибо Paul R), ничего особенного не происходит, я считаю, что компилятор полностью способен выполнять оптимизацию simd/vectorization.

Что касается того, почему я хочу заполнить цифры там, ну, длинный рассказ короткий:

Его часть высокопроизводительного гетерогенного вычисления algorthim со стороны устройства algorthim очень эффективна до такой степени, что набор с несколькими GPU настолько быстр, что я обнаружил, что узкое место производительности случается, когда CPU пытается напишите несколько секунд в памяти.

Из-за этого, зная, что процессор сосет при заполнении номеров (в отличие от этого, графический процессор может заполнять последовательность номеров с очень близкой скоростью ( 238 ГБ/с из 288 ГБ/с на GK110 против жалкого 5 ГБ/сек из 51,2 ГБ/сек на CPU) до теоретической пропускной способности глобальной памяти GPU), я мог бы немного изменить свой algorthim, но то, что заставляет меня задаться вопросом, почему CPU так плохо всасывает при заполнении количества номеров здесь.

Что касается пропускной способности памяти моей установки, я считаю, что ширина полосы пропускания (51,2 ГБ) примерно соответствует правилу на основе теста memcpy(), достигнутая ширина полосы пропускания составляет около 80% + теоретической полосы пропускания (> 40GB/сек).

Ответ 1

Предполагая, что это x86, и что вы уже не насыщаете свою доступную пропускную способность DRAM, вы можете попробовать использовать SSE2 или AVX2 для записи 2 или 4 элемента за раз:

SSE2:

#include "emmintrin.h"

const __m128i v2 = _mm_set1_epi64x(2);
__m128i v = _mm_set_epi64x(1, 0);

for (size_t i=0; i<1000*1000*1000; i += 2)
{
    _mm_stream_si128((__m128i *)&data[i], v);
    v = _mm_add_epi64(v, v2);
}

AVX2:

#include "immintrin.h"

const __m256i v4 = _mm256_set1_epi64x(4);
__m256i v = _mm256_set_epi64x(3, 2, 1, 0);

for (size_t i=0; i<1000*1000*1000; i += 4)
{
    _mm256_stream_si256((__m256i *)&data[i], v);
    v = _mm256_add_epi64(v, v4);
}

Обратите внимание, что data необходимо соответствующим образом выровнять (граница 16 байт или 32 байта).

AVX2 доступен только на Intel Haswell и позже, но SSE2 в наши дни довольно универсален.


FWIW Я собрал тестовую жгуту со скалярной петлей, а вышеописанные петли SSE и AVX скомпилировали ее с помощью clang и протестировали ее на Haswell MacBook Air (1600 МГц LPDDR3 DRAM). Я получил следующие результаты:

# sequence_scalar: t = 0.870903 s = 8.76033 GB / s
# sequence_SSE: t = 0.429768 s = 17.7524 GB / s
# sequence_AVX: t = 0.431182 s = 17.6941 GB / s

Я также попробовал это на настольном ПК Linux с 3,6 ГГц Haswell, компилируя с gcc 4.7.2 и получив следующее:

# sequence_scalar: t = 0.816692 s = 9.34183 GB / s
# sequence_SSE: t = 0.39286 s = 19.4201 GB / s
# sequence_AVX: t = 0.392545 s = 19.4357 GB / s

Таким образом, похоже, что реализации SIMD дают 2x или более улучшение по сравнению с 64-битным скалярным кодом (хотя 256-битный SIMD, похоже, не улучшает более 128 бит SIMD), и эта типичная пропускная способность должна быть намного быстрее, чем 5 ГБ/с.

Я предполагаю, что что-то не так с системой OP или кодом сравнения, что приводит к явно уменьшенной пропускной способности.

Ответ 2

Есть ли какая-то причина, по которой вы ожидаете, что все data[] будут находиться на страницах с включенным ОЗУ?

Предварительная выборка DDR3 будет правильно прогнозировать большинство обращений, но частые границы страницы x86-64 могут быть проблемой. Вы пишете в виртуальную память, поэтому на каждой границе страницы есть потенциальное ошибочное предсказание предзахватчика. Вы можете значительно уменьшить это, используя большие страницы (например, MEM_LARGE_PAGES в Windows).