Boost.Compute медленнее, чем обычный процессор?

Я только начал играть с Boost.Compute, чтобы увидеть, какую скорость он может нам принести, я написал простую программу:

#include <iostream>
#include <vector>
#include <algorithm>
#include <boost/foreach.hpp>
#include <boost/compute/core.hpp>
#include <boost/compute/platform.hpp>
#include <boost/compute/algorithm.hpp>
#include <boost/compute/container/vector.hpp>
#include <boost/compute/functional/math.hpp>
#include <boost/compute/types/builtin.hpp>
#include <boost/compute/function.hpp>
#include <boost/chrono/include.hpp>

namespace compute = boost::compute;

int main()
{
    // generate random data on the host
    std::vector<float> host_vector(16000);
    std::generate(host_vector.begin(), host_vector.end(), rand);

    BOOST_FOREACH (auto const& platform, compute::system::platforms())
    {
        std::cout << "====================" << platform.name() << "====================\n";
        BOOST_FOREACH (auto const& device, platform.devices())
        {
            std::cout << "device: " << device.name() << std::endl;
            compute::context context(device);
            compute::command_queue queue(context, device);
            compute::vector<float> device_vector(host_vector.size(), context);

            // copy data from the host to the device
            compute::copy(
                host_vector.begin(), host_vector.end(), device_vector.begin(), queue
            );

            auto start = boost::chrono::high_resolution_clock::now();
            compute::transform(device_vector.begin(),
                       device_vector.end(),
                       device_vector.begin(),
                       compute::sqrt<float>(), queue);

            auto ans = compute::accumulate(device_vector.begin(), device_vector.end(), 0, queue);
            auto duration = boost::chrono::duration_cast<boost::chrono::milliseconds>(boost::chrono::high_resolution_clock::now() - start);
            std::cout << "ans: " << ans << std::endl;
            std::cout << "time: " << duration.count() << " ms" << std::endl;
            std::cout << "-------------------\n";
        }
    }
    std::cout << "====================plain====================\n";
    auto start = boost::chrono::high_resolution_clock::now();
    std::transform(host_vector.begin(),
                host_vector.end(),
                host_vector.begin(),
                [](float v){ return std::sqrt(v); });

    auto ans = std::accumulate(host_vector.begin(), host_vector.end(), 0);
    auto duration = boost::chrono::duration_cast<boost::chrono::milliseconds>(boost::chrono::high_resolution_clock::now() - start);
    std::cout << "ans: " << ans << std::endl;
    std::cout << "time: " << duration.count() << " ms" << std::endl;

    return 0;
}

А вот пример вывода на моей машине (win7 64-bit):

====================Intel(R) OpenCL====================
device: Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
ans: 1931421
time: 64 ms
-------------------
device: Intel(R) HD Graphics 4600
ans: 1931421
time: 64 ms
-------------------
====================NVIDIA CUDA====================
device: Quadro K600
ans: 1931421
time: 4 ms
-------------------
====================plain====================
ans: 1931421
time: 0 ms

Мой вопрос: почему простая (не opencl) версия быстрее?

3 ответа

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

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

Кроме того, вместо запуска transform() а также accumulate() в качестве отдельных операций, вы должны использовать сплавленный transform_reduce() алгоритм, который выполняет как преобразование, так и сокращение с одним ядром. Код будет выглядеть так:

float ans = 0;
compute::transform_reduce(
    device_vector.begin(),
    device_vector.end(),
    &ans,
    compute::sqrt<float>(),
    compute::plus<float>(),
    queue
);
std::cout << "ans: " << ans << std::endl;

Вы также можете скомпилировать код, используя Boost.Compute с -DBOOST_COMPUTE_USE_OFFLINE_CACHE который включит автономный кеш ядра (для этого необходимо связать с boost_filesystem). Тогда используемые вами ядра будут сохранены в вашей файловой системе и скомпилированы только при первом запуске приложения (по умолчанию NVIDIA в Linux уже делает это).

Я вижу одну из возможных причин большой разницы. Сравните процессор и поток данных графического процессора:

CPU              GPU

                 copy data to GPU

                 set up compute code

calculate sqrt   calculate sqrt

sum              sum

                 copy data from GPU

Учитывая это, кажется, что чип Intel - это всего лишь мусор при общих вычислениях, NVidia, вероятно, страдает от дополнительного копирования данных и настройки графического процессора для выполнения вычислений.

Вы должны попробовать ту же самую программу, но с гораздо более сложной операцией - sqrt и sum слишком просты, чтобы преодолеть дополнительные издержки использования GPU. Например, вы можете попробовать рассчитать баллы Mandlebrot.

В вашем примере перемещение лямбды в накопитель будет быстрее (один проход по памяти против двух проходов)

Вы получаете плохие результаты, потому что вы неправильно измеряете время.

OpenCL Device имеет свои собственные счетчики времени, которые не связаны со счетчиками хостов. Каждое задание OpenCL имеет 4 состояния, таймеры для которых можно запросить: (с веб-сайта Khronos)

  1. CL_PROFILING_COMMAND_QUEUEDкогда команда, идентифицированная событием, ставится в очередь в очередь команд хостом
  2. CL_PROFILING_COMMAND_SUBMITкогда команда, идентифицированная событием, которое было поставлено в очередь, передается хостом устройству, связанному с очередью команд.
  3. CL_PROFILING_COMMAND_STARTкогда команда, идентифицированная событием, начинает выполнение на устройстве.
  4. CL_PROFILING_COMMAND_ENDкогда команда, идентифицированная событием, завершила выполнение на устройстве.

Примите во внимание, что таймеры на стороне устройства. Итак, чтобы измерить производительность ядра и очереди команд, вы можете запросить эти таймеры. В вашем случае нужны 2 последних таймера.

В вашем примере кода вы измеряете время хоста, которое включает в себя время передачи данных (как сказал Skizz) плюс все время, потраченное на обслуживание очереди команд.

Итак, чтобы узнать реальную производительность ядра, вам нужно либо передать cl_event в ваше ядро ​​(не знаю, как это сделать в boost::compute) и запросить это событие для счетчиков производительности, либо сделать ваше ядро ​​действительно огромным и сложным, чтобы скрыть все накладные расходы.

Другие вопросы по тегам