Несоответствие производительности Windows и Ubuntu для реализации виртуального массива на основе видеопамяти

Я реализовал виртуальный массив использование OpenCL и графических карт в качестве резервного хранилища, с кэшированием некоторых данных в ОЗУ с использованием алгоритма LRU и доступом к данным через несколько простых установщиков / получателей. Доступ к элементу массива происходит через блокировку страницы (с использованием массива мьютексов для всех независимых страниц), затем кеш LRU, затем передачу данных pcie в случае вытеснения и возврат с данными (или редактирование данных страницы, если это запись).

#include "GraphicsCardSupplyDepot.h"
#include "VirtualMultiArray.h"
#include "PcieBandwidthBenchmarker.h"
#include "CpuBenchmarker.h"

// testing
#include <random>
#include <iostream>
#include "omp.h"

constexpr bool TEST_BANDWIDTH=true;
constexpr bool TEST_LATENCY=false;
constexpr bool testType = TEST_BANDWIDTH;

// a test object for virtual array
// testing bandwidth: 512kB size
// testing latency: 8byte size
class Object
{
public:
    Object():id(-1){}
    Object(int p):id(p){}
    const int getId() const {return id;}
private:
    char data[testType?(1024*512 - 4):(4)];
    int id;
};

int main()
{
    // number of elements per cache page
    const long long pageSize = 1;

    // number of elements of array
    const long long n = pageSize*(testType?1000:100000);

    // number of benchmark runs
    const int numTestsPerThread = 25;

    // virtual array of objects in video memory
    VirtualMultiArray<Object> test(n,GraphicsCardSupplyDepot().requestGpus(),pageSize,3,PcieBandwidthBenchmarker().bestBandwidth(10));

    // heating cpu to get precise benchmark results
    #pragma omp parallel for
    for(long long j=0;j<n;j++)
    {
            test.set(j,Object(j));
    }

    // test for single thread, 2 threads, .. 64 threads
    for(int i=1;i<=64;i++)
    {
        // benchmark set method
        {
            CpuBenchmarker bench(i*numTestsPerThread*sizeof(Object),std::string("scalar set, ")+std::to_string(i)+std::string("threads"),i*numTestsPerThread);
            #pragma omp parallel for num_threads(i)
            for(long long j=0;j<i;j++)
            {
                // random-access to data
                std::random_device rd;
                std::mt19937 rng(rd());
                std::uniform_real_distribution<float> rnd(0,n-1);

                for(int k=0;k<numTestsPerThread;k++)
                {
                    int rndv = rnd(rng);
                    test.set(rndv,Object(rndv));
                }
            }
        }

        // benchmark get method
        {
            CpuBenchmarker bench(i*numTestsPerThread*sizeof(Object),std::string("scalar get, ")+std::to_string(i)+std::string("threads"),i*numTestsPerThread);
            #pragma omp parallel for num_threads(i)
            for(long long j=0;j<i;j++)
            {
                // random-access to data
                std::random_device rd;
                std::mt19937 rng(rd());
                std::uniform_real_distribution<float> rnd(0,n-1);

                for(int k=0;k<numTestsPerThread;k++)
                {
                    int rndv = rnd(rng);
                    const auto obj = test.get(rndv);
                    if(obj.getId()!=rndv)
                    {
                        throw std::invalid_argument("Error: set/get");
                    }
                }
            }
        }

        std::cout<<"==================================================================="<<std::endl;
    }
    return 0;
}

Когда я тестирую его на Ubuntu 18.04, он использует до 3/4 совокупной пропускной способности системы pcie, но в Windows 10 он использует в основном 1/4 (что составляет всего 2 ГБ / с) пикового значения.

Моя система имеет следующие характеристики:

  • fx8150
  • 4 ГБ одноканальной оперативной памяти ddr3 (аппаратная пиковая скорость 10,6 ГБ / с)
  • 3 видеокарты начального уровня (gt1030, 2xk420), поддерживающие OpenCL (пиковая скорость аппаратного обеспечения 8 ГБ / с)
    • Режим драйвера TCC включен для 2 карт, но первая карта не может работать в режиме TCC
  • g ++ — 10 для Ubuntu C ++ 17
  • MSVC 2019 для Windows C ++ 17

Я попытался сделать его настолько независимым от платформы, насколько это возможно, но в одной части мне пришлось разветвлять некоторый код для выравнивания распределений. Что я делаю не так на стороне Windows? (помимо выровненных выделений)

Например, в Ubuntu я получаю такой вывод:

scalar set, 1threads: 12182103 nanoseconds     (bandwidth = 1075.94 MB/s)      (throughput = 487284.12 nanoseconds per iteration) 
scalar get, 1threads: 10874521 nanoseconds     (bandwidth = 1205.31 MB/s)      (throughput = 434980.84 nanoseconds per iteration) 
===================================================================
scalar set, 2threads: 15642385 nanoseconds     (bandwidth = 1675.86 MB/s)      (throughput = 312847.70 nanoseconds per iteration) 
scalar get, 2threads: 16902450 nanoseconds     (bandwidth = 1550.92 MB/s)      (throughput = 338049.00 nanoseconds per iteration) 
===================================================================
scalar set, 3threads: 15004388 nanoseconds     (bandwidth = 2620.67 MB/s)      (throughput = 200058.51 nanoseconds per iteration) 
scalar get, 3threads: 16687201 nanoseconds     (bandwidth = 2356.39 MB/s)      (throughput = 222496.01 nanoseconds per iteration) 
...
...
===================================================================
scalar set, 63threads: 212283324 nanoseconds     (bandwidth = 3889.87 MB/s)      (throughput = 134783.06 nanoseconds per iteration) 
scalar get, 63threads: 136367146 nanoseconds     (bandwidth = 6055.37 MB/s)      (throughput = 86582.31 nanoseconds per iteration) 
===================================================================
scalar set, 64threads: 229655008 nanoseconds     (bandwidth = 3652.70 MB/s)      (throughput = 143534.38 nanoseconds per iteration) 
scalar get, 64threads: 149184573 nanoseconds     (bandwidth = 5622.97 MB/s)      (throughput = 93240.36 nanoseconds per iteration) 
===================================================================

но в Windows он достигает максимальной скорости 2 ГБ / с с гораздо меньшим количеством потоков. Тестировал его и без кеширования LRU (просто прямое отображение страниц во vram), но была такая же разница в производительности.

Редактировать:

Согласно профилировщику производительности Visual Studio, генератор случайных чисел не является узким местом:

введите описание изображения здесь

Когда я иду по горячему пути, он заканчивается командой OpenCL, которая запрашивает событие:

 clGetEventInfo(evt, evtInf,sizeof(cl_int), &evtStatus0, nullptr)

в заголовке PageCache, строка 248.

введите описание изображения здесь

Это вне проекта, может быть, это связано с тем, что моя система устарела? Или, может быть, дело в неправильном использовании API в другом месте, например, в каких-то неправильных флагах в построении буфера opencl?

Изменить 2:

Для Windows я удалил clGetEventInfo () и использовал блокирующую версию команд opencl для чтения / записи:

clEnqueueReadBuffer(q->getQueue(), gpu->getMem(), CL_TRUE

это дало + 25% производительности (2,5 ГБ / с вместо 2,0 ГБ / с) для теста в Windows, но теперь есть еще одно узкое место:

введите описание изображения здесь

накладные расходы, связанные с ядром, почти равны накладным расходам pcie-i / o! Когда я иду по горячему пути, опять же, часть ввода-вывода является наиболее узким местом:

введите описание изображения здесь

Означает ли это, что эти операции чтения / записи буфера OpenCL также проходят через ядро ​​Windows? Я думаю, у них внутри занято-ожидание с какой-то блокировкой мьютекса, которая работает медленно? Когда я нажимаю «включить внешний код», он добавляет «nvopencl64.dll» в список горячих путей с тегом «ядро» в своей строке. Означает ли это, что драйвер nvidia использует некоторое время ожидания при синхронизации OpenCL, что снижает масштабируемость потокового ввода-вывода из-за конфликтов блокировки, связанных с Windows? В Ubuntu он в 2 раза быстрее с тем же графическим процессором nvidia и на 25% быстрее с явным циклом ожидания-ожидания с использованием событий. Итак, (я думаю) это больше связано с проблемой Windows, а не с Nvidia. Но не уверен.

Когда я не использую все 3 видеокарты одновременно, я получаю следующие результаты:

  • GT1030: 1 ГБ / с
  • K420 на 8-кратном слоте: 2 ГБ / с
  • K420 с 4 разъемами: 1 ГБ / с

но когда я использую все 3 со многими потоками, я получаю всего 2,5 ГБ / с. Это не то же самое с Ubuntu, которая достигает 6 ГБ / с. Если бы существовал способ указать OpenCL использовать цикл ожидания-ожидания вместо цикла «занято-ожидание», тогда потоки фактически эффективно перекрывали бы ввод-вывод. Но в Windows они не только перестают масштабироваться при 8 потоках, но и уменьшаются после дополнительных потоков. В Ubuntu он постоянно увеличивается до 64 потоков. Может быть, проблема масштабируемости вызвана конфликтом из-за блокировки страниц внутри драйвера OpenCL?

Редактировать-3:

Когда я тестирую простой пример мьютекса:

int main()
{

    std::mutex m;
    for (int i = 0; i < 100; i++)
    {
        CpuBenchmarker bench;
        for (int j = 0; j < 1000; j++)
        {
            std::unique_lock<std::mutex> l(m);
        }
    }
    return 0;
}

профилировщик говорит, что 97% накладных расходов приходится на пространство ядра, а тестер выдает ~ 70 наносекунд на длительность блокировки. Процессор — FX8150 на 2,1 ГГц.

Наконец, выглядит ли структура кода нормально?

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *