Размер массива и производительность копирования

Я уверен, что на этот вопрос уже был дан ответ, но я не могу найти хорошего объяснения.

Я пишу графическую программу, в которой часть конвейера копирует данные вокселей в память OpenCL (закрепленную). Я обнаружил, что эта процедура копирования является узким местом, и провел некоторые измерения производительности простого std::copy, Данные являются плавающими, и каждый фрагмент данных, который я хочу скопировать, имеет размер около 64 МБ.

Это мой оригинальный код перед любыми попытками сравнительного анализа:

std::copy(data, data+numVoxels, pinnedPointer_[_index]);

куда data это указатель с плавающей точкой, numVoxels является неподписанным Int и pinnedPointer_[_index] это указатель с плавающей точкой, ссылающийся на закрепленный буфер OpenCL.

Поскольку у меня была низкая производительность, я решил вместо этого скопировать меньшие части данных и посмотреть, какую пропускную способность я получил. Я использовал boost :: cpu_timer для синхронизации. Я пытался запустить его в течение некоторого времени, а также в среднем за пару сотен прогонов, получая аналогичные результаты. Вот соответствующий код вместе с результатами:

boost::timer::cpu_timer t;
unsigned int testNum = numVoxels;
while (testNum > 2) {
t.start();
std::copy(data, data+testNum, pinnedPointer_[_index]);
t.stop();
boost::timer::cpu_times result = t.elapsed();
double time = (double)result.wall / 1.0e9 ;
int size = testNum*sizeof(float);
double GB = (double)size / 1073741842.0;
// Print results
testNum /= 2;
}

Copied 67108864 bytes in 0.032683s, 1.912315 GB/s
Copied 33554432 bytes in 0.017193s, 1.817568 GB/s
Copied 16777216 bytes in 0.008586s, 1.819749 GB/s
Copied 8388608 bytes in 0.004227s, 1.848218 GB/s
Copied 4194304 bytes in 0.001886s, 2.071705 GB/s
Copied 2097152 bytes in 0.000819s, 2.383543 GB/s
Copied 1048576 bytes in 0.000290s, 3.366923 GB/s
Copied 524288 bytes in 0.000063s, 7.776913 GB/s
Copied 262144 bytes in 0.000016s, 15.741867 GB/s
Copied 131072 bytes in 0.000008s, 15.213149 GB/s
Copied 65536 bytes in 0.000004s, 14.374742 GB/s
Copied 32768 bytes in 0.000003s, 10.209962 GB/s
Copied 16384 bytes in 0.000001s, 10.344942 GB/s
Copied 8192 bytes in 0.000001s, 6.476566 GB/s
Copied 4096 bytes in 0.000001s, 4.999603 GB/s
Copied 2048 bytes in 0.000001s, 1.592111 GB/s
Copied 1024 bytes in 0.000001s, 1.600125 GB/s
Copied 512 bytes in 0.000001s, 0.843960 GB/s
Copied 256 bytes in 0.000001s, 0.210990 GB/s
Copied 128 bytes in 0.000001s, 0.098439 GB/s
Copied 64 bytes in 0.000001s, 0.049795 GB/s
Copied 32 bytes in 0.000001s, 0.049837 GB/s
Copied 16 bytes in 0.000001s, 0.023728 GB/s

Наблюдается четкий пик пропускной способности при копировании фрагментов в 65536-262144 байта, и пропускная способность намного выше, чем при копировании всего массива (15 против 2 ГБ / с).

Зная это, я решил попробовать еще одну вещь и скопировал весь массив, но используя повторные вызовы std::copy где каждый вызов просто обрабатывается частью массива. Пробуя разные размеры чанка, вот мои результаты:

unsigned int testNum = numVoxels;
unsigned int parts = 1;
while (sizeof(float)*testNum > 256) {
t.start();
for (unsigned int i=0; i<parts; ++i) {
std::copy(data+i*testNum,
data+(i+1)*testNum,
pinnedPointer_[_index]+i*testNum);
}
t.stop();
boost::timer::cpu_times result = t.elapsed();
double time = (double)result.wall / 1.0e9;
int size = testNum*sizeof(float);
double GB = parts*(double)size / 1073741824.0;
// Print results
parts *= 2;
testNum /= 2;
}

Part size 67108864 bytes, copied 0.0625 GB in 0.0331298s, 1.88652 GB/s
Part size 33554432 bytes, copied 0.0625 GB in 0.0339876s, 1.83891 GB/s
Part size 16777216 bytes, copied 0.0625 GB in 0.0342558s, 1.82451 GB/s
Part size 8388608 bytes, copied 0.0625 GB in 0.0334264s, 1.86978 GB/s
Part size 4194304 bytes, copied 0.0625 GB in 0.0287896s, 2.17092 GB/s
Part size 2097152 bytes, copied 0.0625 GB in 0.0289941s, 2.15561 GB/s
Part size 1048576 bytes, copied 0.0625 GB in 0.0240215s, 2.60184 GB/s
Part size 524288 bytes, copied 0.0625 GB in 0.0184499s, 3.38756 GB/s
Part size 262144 bytes, copied 0.0625 GB in 0.0186002s, 3.36018 GB/s
Part size 131072 bytes, copied 0.0625 GB in 0.0185958s, 3.36097 GB/s
Part size 65536 bytes, copied 0.0625 GB in 0.0185735s, 3.365 GB/s
Part size 32768 bytes, copied 0.0625 GB in 0.0186523s, 3.35079 GB/s
Part size 16384 bytes, copied 0.0625 GB in 0.0187756s, 3.32879 GB/s
Part size 8192 bytes, copied 0.0625 GB in 0.0182212s, 3.43007 GB/s
Part size 4096 bytes, copied 0.0625 GB in 0.01825s, 3.42465 GB/s
Part size 2048 bytes, copied 0.0625 GB in 0.0181881s, 3.43631 GB/s
Part size 1024 bytes, copied 0.0625 GB in 0.0180842s, 3.45605 GB/s
Part size 512 bytes, copied 0.0625 GB in 0.0186669s, 3.34817 GB/s

Кажется, что уменьшение размера чанка действительно имеет существенный эффект, но я все еще не могу получить где-то около 15 ГБ / с.

Я использую 64-битную Ubuntu, оптимизация GCC не имеет большого значения.

  1. Почему размер массива влияет на пропускную способность таким образом?
  2. Играет ли OpenCL закрепленная память?
  3. Каковы стратегии оптимизации копирования большого массива?

4

Решение

Я почти уверен, что вы сталкиваетесь с кешем. Если вы заполняете кэш данными, которые вы записали, в следующий раз понадобятся некоторые данные, кэш должен будет прочитать эти данные из памяти, но ПЕРВЫЙ должен найти место в кеше — потому что все данные [ или, по крайней мере, многое из этого] «грязно», потому что оно было записано, его нужно записать в ОЗУ. Затем мы записываем новый бит данных в кеш, который выбрасывает другой бит данных, которые являются грязными (или что-то, что мы читали ранее).

В ассемблере мы можем преодолеть это с помощью «невременной» инструкции перемещения. Инструкция SSE movntps например. Эта инструкция «позволит избежать хранения вещей в кеше».

Редактирование: Вы также можете получить более высокую производительность, не смешивая операции чтения и записи — используйте небольшой буфер [массив фиксированного размера], скажем, 4-16 КБ, и скопируйте данные в этот буфер, а затем запишите этот буфер в новое место, где вы хотите. Опять же, в идеале следует использовать не временные записи, поскольку это улучшит пропускную способность даже в этом случае — но использование «блоков» для чтения и последующей записи, а не для чтения одной записи одной, будет выполняться намного быстрее.

Что-то вроде этого:

   float temp[2048];
int left_to_do = numVoxels;
int offset = 0;

while(left_to_do)
{
int block = min(left_to_do, sizeof(temp)/sizeof(temp[0]);
std::copy(data+offset, data+offset+block, temp);
std::copy(temp, temp+block, pinnedPointer_[_index+offet]);
offset += block;
left_to_do -= block;
}

Попробуйте и посмотрите, улучшится ли это. Это не может …

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

5

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]