Я уверен, что на этот вопрос уже был дан ответ, но я не могу найти хорошего объяснения.
Я пишу графическую программу, в которой часть конвейера копирует данные вокселей в память 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 не имеет большого значения.
Я почти уверен, что вы сталкиваетесь с кешем. Если вы заполняете кэш данными, которые вы записали, в следующий раз понадобятся некоторые данные, кэш должен будет прочитать эти данные из памяти, но ПЕРВЫЙ должен найти место в кеше — потому что все данные [ или, по крайней мере, многое из этого] «грязно», потому что оно было записано, его нужно записать в ОЗУ. Затем мы записываем новый бит данных в кеш, который выбрасывает другой бит данных, которые являются грязными (или что-то, что мы читали ранее).
В ассемблере мы можем преодолеть это с помощью «невременной» инструкции перемещения. Инструкция 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: я должен объяснить, что это быстрее, потому что вы повторно используете один и тот же бит кэша для загрузки данных каждый раз, и, не смешивая чтение и запись, мы получаем лучшую производительность из самой памяти.
Других решений пока нет …