Быстрое (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).