CPUSets untuk pengembangan game

Pengantar

Platform Windows Universal (UWP) adalah inti dari berbagai perangkat elektronik konsumen. Dengan demikian, diperlukan API tujuan umum untuk mengatasi kebutuhan semua jenis aplikasi dari game hingga aplikasi yang disematkan ke perangkat lunak perusahaan yang berjalan di server. Dengan memanfaatkan informasi yang tepat yang disediakan oleh API, Anda dapat memastikan game Anda berjalan dengan maksimal pada perangkat keras apa pun.

CPUSets API

API CPUSets menyediakan kontrol atas set CPU mana yang tersedia untuk utas yang akan dijadwalkan. Dua fungsi tersedia untuk mengontrol di mana utas dijadwalkan:

  • SetProcessDefaultCpuSets – Fungsi ini dapat digunakan untuk menentukan kumpulan CPU mana yang dapat dijalankan utas baru jika tidak ditetapkan ke set CPU tertentu.
  • SetThreadSelectedCpuSets – Fungsi ini memungkinkan Anda membatasi Rangkaian CPU yang mungkin berjalan pada utas tertentu.

Jika fungsi SetProcessDefaultCpuSets tidak pernah digunakan, utas yang baru dibuat dapat dijadwalkan pada kumpulan CPU apa pun yang tersedia untuk proses Anda. Bagian ini menjelaskan dasar-dasar API CPUSets.

GetSystemCpuSetInformation

API pertama yang digunakan untuk mengumpulkan informasi adalah fungsi GetSystemCpuSetInformation . Fungsi ini mengisi informasi dalam array objek SYSTEM_CPU_SET_INFORMATION yang disediakan oleh kode judul. Memori untuk tujuan harus dialokasikan oleh kode game, yang ukurannya ditentukan dengan memanggil GetSystemCpuSetInformation itu sendiri. Ini memerlukan dua panggilan ke GetSystemCpuSetInformation seperti yang ditunjukkan dalam contoh berikut.

unsigned long size;
HANDLE curProc = GetCurrentProcess();
GetSystemCpuSetInformation(nullptr, 0, &size, curProc, 0);

std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);

PSYSTEM_CPU_SET_INFORMATION cpuSets = reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(buffer.get());
  
GetSystemCpuSetInformation(cpuSets, size, &size, curProc, 0);

Setiap instans SYSTEM_CPU_SET_INFORMATION yang dikembalikan berisi informasi tentang satu unit pemrosesan unik, juga dikenal sebagai set CPU. Ini tidak selalu berarti bahwa ia mewakili perangkat keras fisik yang unik. CPU yang menggunakan hyperthreading akan memiliki beberapa inti logis yang berjalan pada satu inti pemrosesan fisik. Menjadwalkan beberapa utas pada inti logis yang berbeda yang berada di inti fisik yang sama memungkinkan pengoptimalan sumber daya tingkat perangkat keras yang sebaliknya akan memerlukan pekerjaan tambahan untuk dilakukan pada tingkat kernel. Dua utas yang dijadwalkan pada inti logis terpisah pada inti fisik yang sama harus berbagi waktu CPU, tetapi akan berjalan lebih efisien daripada jika dijadwalkan ke inti logis yang sama.

SYSTEM_CPU_SET_INFORMATION

Informasi dalam setiap contoh struktur data ini yang dikembalikan dari GetSystemCpuSetInformation berisi informasi tentang unit pemrosesan unik tempat utas dapat dijadwalkan. Mengingat kemungkinan rentang perangkat target, banyak informasi dalam struktur data SYSTEM_CPU_SET_INFORMATION mungkin tidak berlaku untuk pengembangan game. Tabel 1 memberikan penjelasan tentang anggota data yang berguna untuk pengembangan game.

Tabel 1. Anggota data berguna untuk pengembangan game.

Nama anggota Jenis data Deskripsi
Jenis CPU_SET_INFORMATION_TYPE Jenis informasi dalam struktur. Jika nilai ini bukan CpuSetInformation, nilai tersebut harus diabaikan.
Id panjang tidak ditandatangani ID dari set CPU yang ditentukan. Ini adalah ID yang harus digunakan dengan fungsi set CPU seperti SetThreadSelectedCpuSets.
Grup short tidak bertanda Menentukan "grup prosesor" dari set CPU. Grup prosesor memungkinkan PC memiliki lebih dari 64 inti logis, dan memungkinkan pertukaran CPU panas saat sistem berjalan. Tidak jarang melihat PC yang bukan server dengan lebih dari satu grup. Kecuali Anda menulis aplikasi yang dimaksudkan untuk berjalan di server besar atau farm server, yang terbaik adalah menggunakan set CPU dalam satu grup karena sebagian besar PC konsumen hanya akan memiliki satu grup prosesor. Semua nilai lain dalam struktur ini relatif terhadap Grup.
LogicalProcessorIndex char yang tidak bertanda Indeks relatif grup dari set CPU
CoreIndex char yang tidak bertanda Indeks relatif grup inti CPU fisik tempat set CPU berada
LastLevelCacheIndex char yang tidak bertanda Indeks relatif grup dari cache terakhir yang terkait dengan set CPU ini. Ini adalah cache paling lambat kecuali sistem menggunakan simpul NUMA, biasanya cache L2 atau L3.

Anggota data lainnya memberikan informasi yang tidak mungkin menggambarkan CPU di PC konsumen atau perangkat konsumen lainnya dan tidak mungkin berguna. Informasi yang diberikan oleh data yang dikembalikan kemudian dapat digunakan untuk mengatur utas dengan berbagai cara. Bagian Pertimbangan untuk pengembangan game dari laporan resmi ini merinci beberapa cara untuk memanfaatkan data ini untuk mengoptimalkan alokasi utas.

Berikut ini adalah beberapa contoh jenis informasi yang dikumpulkan dari aplikasi UWP yang berjalan pada berbagai jenis perangkat keras.

Tabel 2. Informasi yang dikembalikan dari aplikasi UWP yang berjalan di Microsoft Lumia 950. Ini adalah contoh sistem yang memiliki beberapa cache tingkat terakhir. Lumia 950 memiliki proses Qualcomm 808 Snapdragon yang berisi Arm Cortex A57 core ganda dan CPU Arm Cortex A53 quad core.

Tabel 2

Tabel 3. Informasi yang dikembalikan dari aplikasi UWP yang berjalan pada PC biasa. Ini adalah contoh sistem yang menggunakan hyperthreading; setiap inti fisik memiliki dua inti logis ke utas mana yang dapat dijadwalkan. Dalam hal ini, sistem berisi Intel Xenon CPU E5-2620.

Tabel 3

Tabel 4. Informasi yang dikembalikan dari aplikasi UWP yang berjalan pada Surface Pro 4 Microsoft quad core. Sistem ini memiliki CPU Intel Core i5-6300.

Tabel 4

SetThreadSelectedCpuSets

Sekarang informasi tentang set CPU tersedia, itu dapat digunakan untuk mengatur utas. Handel utas yang dibuat dengan CreateThread diteruskan ke fungsi ini bersama dengan array ID set CPU tempat utas dapat dijadwalkan. Salah satu contoh penggunaannya ditunjukkan dalam kode berikut.

HANDLE audioHandle = CreateThread(nullptr, 0, AudioThread, nullptr, 0, nullptr);
unsigned long cores [] = { cpuSets[0].CpuSet.Id, cpuSets[1].CpuSet.Id };
SetThreadSelectedCpuSets(audioHandle, cores, 2);

Dalam contoh ini, utas dibuat berdasarkan fungsi yang dideklarasikan sebagai AudioThread. Utas ini kemudian diizinkan untuk dijadwalkan pada salah satu dari dua set CPU. Kepemilikan utas set CPU tidak eksklusif. Utas yang dibuat tanpa dikunci ke set CPU tertentu mungkin membutuhkan waktu dari AudioThread. Demikian juga, utas lain yang dibuat juga dapat dikunci ke salah satu atau kedua set CPU ini di lain waktu.

SetProcessDefaultCpuSets

Sebaliknya ke SetThreadSelectedCpuSets adalah SetProcessDefaultCpuSets. Ketika utas dibuat, utas tidak perlu dikunci ke dalam set CPU tertentu. Jika Anda tidak ingin utas ini berjalan pada set CPU tertentu (yang digunakan oleh utas render atau utas audio Anda misalnya), Anda dapat menggunakan fungsi ini untuk menentukan inti mana utas ini diizinkan untuk dijadwalkan.

Pertimbangan untuk pengembangan game

Seperti yang telah kita lihat, API CPUSets memberikan banyak informasi dan fleksibilitas dalam hal menjadwalkan utas. Alih-alih mengambil pendekatan bottom-up untuk mencoba menemukan kegunaan untuk data ini, lebih efektif untuk mengambil pendekatan top-down untuk menemukan bagaimana data dapat digunakan untuk mengakomodasi skenario umum.

Bekerja dengan utas kritis waktu dan hyperthreading

Metode ini efektif jika game Anda memiliki beberapa utas yang harus berjalan secara real time bersama dengan utas pekerja lain yang membutuhkan waktu CPU yang relatif sedikit. Beberapa tugas, seperti musik latar belakang berkelanjutan, harus berjalan tanpa gangguan untuk pengalaman bermain yang optimal. Bahkan satu bingkai kelaparan untuk utas audio dapat menyebabkan popping atau glitching, sehingga sangat penting bahwa ia menerima jumlah waktu CPU yang diperlukan setiap bingkai.

Menggunakan SetThreadSelectedCpuSets bersama dengan SetProcessDefaultCpuSets dapat memastikan utas berat Anda tetap tidak terganggu oleh utas pekerja apa pun. SetThreadSelectedCpuSets dapat digunakan untuk menetapkan utas berat Anda ke set CPU tertentu. SetProcessDefaultCpuSets kemudian dapat digunakan untuk memastikan utas yang tidak ditetapkan yang dibuat diletakkan pada set CPU lainnya. Dalam kasus CPU yang menggunakan hyperthreading, penting juga untuk memperhitungkan inti logis pada inti fisik yang sama. Utas pekerja tidak boleh diizinkan untuk berjalan pada inti logis yang memiliki inti fisik yang sama dengan utas yang ingin Anda jalankan dengan responsivitas real time. Kode berikut menunjukkan cara menentukan apakah PC menggunakan hyperthreading.

unsigned long retsize = 0;
(void)GetSystemCpuSetInformation( nullptr, 0, &retsize,
    GetCurrentProcess(), 0);
 
std::unique_ptr<uint8_t[]> data( new uint8_t[retsize] );
if ( !GetSystemCpuSetInformation(
    reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>( data.get() ),
    retsize, &retsize, GetCurrentProcess(), 0) )
{
    // Error!
}
 
std::set<DWORD> cores;
std::vector<DWORD> processors;
uint8_t const * ptr = data.get();
for( DWORD size = 0; size < retsize; ) {
    auto info = reinterpret_cast<const SYSTEM_CPU_SET_INFORMATION*>( ptr );
    if ( info->Type == CpuSetInformation ) {
         processors.push_back( info->CpuSet.Id );
         cores.insert( info->CpuSet.CoreIndex );
    }
    ptr += info->Size;
    size += info->Size;
}
 
bool hyperthreaded = processors.size() != cores.size();

Jika sistem menggunakan hyperthreading, penting bahwa kumpulan set CPU default tidak menyertakan inti logis pada inti fisik yang sama dengan utas real time apa pun. Jika sistem tidak hyperthreading, hanya perlu untuk memastikan bahwa set CPU default tidak menyertakan inti yang sama dengan set CPU yang menjalankan utas audio Anda.

Contoh pengorganisasian utas berdasarkan inti fisik dapat ditemukan dalam sampel CPUSets yang tersedia di repositori GitHub yang ditautkan di bagian Sumber daya tambahan .

Mengurangi biaya koherensi cache dengan cache tingkat terakhir

Koherensi cache adalah konsep bahwa memori cache sama di beberapa sumber daya perangkat keras yang bertindak pada data yang sama. Jika utas dijadwalkan pada inti yang berbeda, tetapi bekerja pada data yang sama, mereka mungkin mengerjakan salinan terpisah dari data tersebut di cache yang berbeda. Untuk mendapatkan hasil yang benar, cache ini harus tetap koheren satu sama lain. Mempertahankan koherensi antara beberapa cache relatif mahal, tetapi perlu bagi sistem multi-inti untuk beroperasi. Selain itu, itu benar-benar di luar kendali kode klien; sistem yang mendasar bekerja secara independen untuk menjaga cache tetap terbaru dengan mengakses sumber daya memori bersama antar inti.

Jika game Anda memiliki beberapa utas yang berbagi sejumlah besar data, Anda dapat meminimalkan biaya koherensi cache dengan memastikan bahwa mereka dijadwalkan pada set CPU yang berbagi cache tingkat terakhir. Cache tingkat terakhir adalah cache paling lambat yang tersedia untuk inti pada sistem yang tidak menggunakan simpul NUMA. Pc game sangat jarang digunakan untuk menggunakan simpul NUMA. Jika inti tidak berbagi cache tingkat terakhir, mempertahankan koherensi akan memerlukan akses tingkat yang lebih tinggi, dan karenanya lebih lambat, sumber daya memori. Mengunci dua utas untuk memisahkan set CPU yang berbagi cache dan inti fisik dapat memberikan performa yang lebih baik daripada menjadwalkannya pada inti fisik terpisah jika tidak memerlukan lebih dari 50% dari waktu dalam bingkai tertentu.

Contoh kode ini menunjukkan cara menentukan apakah utas yang sering berkomunikasi dapat berbagi cache tingkat terakhir.

unsigned long retsize = 0;
(void)GetSystemCpuSetInformation(nullptr, 0, &retsize,
    GetCurrentProcess(), 0);
 
std::unique_ptr<uint8_t[]> data(new uint8_t[retsize]);
if (!GetSystemCpuSetInformation(
    reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(data.get()),
    retsize, &retsize, GetCurrentProcess(), 0))
{
    // Error!
}
 
unsigned long count = retsize / sizeof(SYSTEM_CPU_SET_INFORMATION);
bool sharedcache = false;
 
std::map<unsigned char, std::vector<SYSTEM_CPU_SET_INFORMATION>> cachemap;
for (size_t i = 0; i < count; ++i)
{
    auto cpuset = reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(data.get())[i];
    if (cpuset.Type == CPU_SET_INFORMATION_TYPE::CpuSetInformation)
    {
        if (cachemap.find(cpuset.CpuSet.LastLevelCacheIndex) == cachemap.end())
        {
            std::pair<unsigned char, std::vector<SYSTEM_CPU_SET_INFORMATION>> newvalue;
            newvalue.first = cpuset.CpuSet.LastLevelCacheIndex;
            newvalue.second.push_back(cpuset);
            cachemap.insert(newvalue);
        }
        else
        {
            sharedcache = true;
            cachemap[cpuset.CpuSet.LastLevelCacheIndex].push_back(cpuset);
        }
    }
}

Tata letak cache yang diilustrasikan dalam Gambar 1 adalah contoh jenis tata letak yang mungkin Anda lihat dari sistem. Gambar ini adalah ilustrasi cache yang ditemukan di Microsoft Lumia 950. Komunikasi antar alur yang terjadi antara CPU 256 dan CPU 260 akan menimbulkan overhead yang signifikan karena akan mengharuskan sistem untuk menjaga cache L2 mereka tetap koheren.

Gambar 1. Arsitektur cache ditemukan pada perangkat Microsoft Lumia 950.

Cache Lumia 950

Ringkasan

API CPUSets yang tersedia untuk pengembangan UWP menyediakan sejumlah besar informasi dan kontrol atas opsi multithreading Anda. Kompleksitas tambahan dibandingkan dengan API multithreaded sebelumnya untuk pengembangan Windows memiliki beberapa kurva pembelajaran, tetapi peningkatan fleksibilitas pada akhirnya memungkinkan performa yang lebih baik di berbagai PC konsumen dan target perangkat keras lainnya.

Sumber Daya Tambahan: